diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 64a4c5b92118e65cc97e8b4718fb47bd276896c9..b167edb54a9cf6bee68156173a61e95f7c1cf56f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,11 +1,22 @@ -image: maven:eclipse-temurin +image: maven:latest -build: - stage: build +stages: + - checkstyle + - compile + - test + +checkstyle: + stage: checkstyle + script: + - mvn clean prettier:check + +compile: + stage: compile script: - mvn compile + - mvn clean test: stage: test script: - - mvn clean test + - mvn clean test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..be881a74dc9a4d32c28efc3e9862e955365c96ff --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Paths +A graphical game for the course Programming 2 IDATT2001. +Made by Nicolai H. Brand and Trym H. Gudvangen. + + +## Installation and run + +### Dependencies + +- `git`: to download the repository +- `JDK`: Java Development Kiy version 1.7 or higher +- `Maven`: Build tool + + +### Installation + +```sh +git clone git@gitlab.stud.idi.ntnu.no:nicolahb/paths.git +``` + +### Run + +```sh +cd paths +``` + +```sh +mvn javafx:run +``` diff --git a/pom.xml b/pom.xml index 9533ef90ef5f4f320d47517749a374fd2887cbc6..94f005ab10d162deed04ddffe5cc4dea7ad23c7d 100644 --- a/pom.xml +++ b/pom.xml @@ -13,11 +13,31 @@ <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <plugin.prettier.goal>write</plugin.prettier.goal> </properties> <build> <plugins> -<!-- Apache maven compiler plugin--> +<!-- Prettier, a code formatter --> + <plugin> + <groupId>com.hubspot.maven.plugins</groupId> + <artifactId>prettier-maven-plugin</artifactId> + <version>0.19</version> + <configuration> + <prettierJavaVersion>2.0.0</prettierJavaVersion> + <printWidth>120</printWidth> + <tabWidth>4</tabWidth> + <useTabs>false</useTabs> + <ignoreConfigFile>true</ignoreConfigFile> + <ignoreEditorConfig>true</ignoreEditorConfig> + <inputGlobs> + <inputGlob>src/main/java/**/*.java</inputGlob> + <inputGlob>src/test/java/**/*.java</inputGlob> + </inputGlobs> + </configuration> + </plugin> + + <!-- Apache maven compiler plugin--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> @@ -36,6 +56,18 @@ <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> <version>0.0.8</version> + +<!-- Configuration of the Main class for the javafx application --> + <executions> + <execution> + <id>default-cli</id> + <configuration> + <mainClass> + edu.ntnu.idatt2001.group_30.paths.Main + </mainClass> + </configuration> + </execution> + </executions> </plugin> <!-- Plugin to generate the javadoc files for the project--> @@ -52,7 +84,7 @@ <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> - <version>19</version> + <version>20-ea+9</version> </dependency> <!-- Dependency to support JUnit5 library--> <dependency> @@ -63,4 +95,4 @@ </dependency> </dependencies> -</project> \ No newline at end of file +</project> diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/Game.java b/src/main/java/edu/ntnu/idatt2001/group_30/Game.java deleted file mode 100644 index abc027da7db36d19b50bd77656c8a24f45aefa62..0000000000000000000000000000000000000000 --- a/src/main/java/edu/ntnu/idatt2001/group_30/Game.java +++ /dev/null @@ -1,80 +0,0 @@ -package edu.ntnu.idatt2001.group_30; - -import edu.ntnu.idatt2001.group_30.goals.Goal; - -import java.util.List; - -/** - * This class represents the facade of a game of paths. - * This class connects a Player to a Story and has useful methods to start and maneuver through a game. - * - * @author Nicolai H. Brand, Trym Hamer Gudvangen - */ -public class Game { - private final Player player; - private final Story story; - private final List<Goal> goals; - - /** - * This method constructs a Game object with the given parameters. - * @param player The player who will be playing game. - * @param story The story for the game that the player will play through. - * @param goals A list of goals that determines the desired goals of the game: - * @throws IllegalArgumentException This exception is thrown if any argument is null. - */ - public Game(Player player, Story story, List<Goal> goals) throws IllegalArgumentException{ - if (player == null || story == null || goals == null) { - throw new IllegalArgumentException("Player, story, and goals cannot be null"); - } - this.player = player; - this.story = story; - this.goals = goals; - } - - /** - * This method starts a game. - * @return the first passage of the story. - */ - public Passage begin() { - return this.story.getOpeningPassage(); - //TODO: Should we make sure the game can not begin more than one time? - // It depends whether or not the end user itself has a way to call begin(), or if this is something - // exclusively done internally. In that case, unit-tests to ensure this method is only called once may - // suffice. - } - - /** - * This method takes in a link and returns the corresponding passage of the story. - * @param link a link to a passage in the story. - * @return the corresponding passage for the link. - */ - public Passage go(Link link) { - return this.story.getPassage(link); - } - - /** - * This method returns the player for the game. - * @return the player object for the game. - */ - public Player getPlayer() { - return player; - } - - /** - * This method returns the story for the game. - * @return the story object for the game - */ - public Story getStory() { - return story; - } - - /** - * This method returns all the goals of the game. - * @return all the goals of the game as a List{@code <Goal>} - */ - public List<Goal> getGoals() { - return goals; - } -} - - diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/Main.java b/src/main/java/edu/ntnu/idatt2001/group_30/Main.java deleted file mode 100644 index 1bfab10278744e6279f4a29e2bec1d1daf15da40..0000000000000000000000000000000000000000 --- a/src/main/java/edu/ntnu/idatt2001/group_30/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package edu.ntnu.idatt2001.group_30; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/ActionType.java b/src/main/java/edu/ntnu/idatt2001/group_30/actions/ActionType.java deleted file mode 100644 index 41f56dfa967e73e3495876c607d94c75274ffa48..0000000000000000000000000000000000000000 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/ActionType.java +++ /dev/null @@ -1,13 +0,0 @@ -package edu.ntnu.idatt2001.group_30.actions; - -/** - * This enumeration represents the different types of actions that exist: gold, health, inventory, and score. - * - * @author Trym Hamer Gudvangen - */ -public enum ActionType { - GOLD_ACTION, - HEALTH_ACTION, - INVENTORY_ACTION, - SCORE_ACTION -} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/goals/InventoryGoal.java b/src/main/java/edu/ntnu/idatt2001/group_30/goals/InventoryGoal.java deleted file mode 100644 index 2c8bbc0e9360c60dbbf5a2cccd96bb2a857faac5..0000000000000000000000000000000000000000 --- a/src/main/java/edu/ntnu/idatt2001/group_30/goals/InventoryGoal.java +++ /dev/null @@ -1,36 +0,0 @@ -package edu.ntnu.idatt2001.group_30.goals; - -import edu.ntnu.idatt2001.group_30.Player; - -import java.util.List; -import java.util.Objects; - -/** - * This class represents the items that are expected in a player's inventory. - * - * @author Trym Hamer Gudvangen - */ -public class InventoryGoal implements Goal { - - private final List<String> mandatoryItems; - - /** - * The constructor defines the items a player must have. - * @param mandatoryItems Expected items, given as a List{@code <String>}. - */ - public InventoryGoal(List<String> mandatoryItems) { - this.mandatoryItems = mandatoryItems; - //TODO: Add exception? - } - - /** - * {@inheritDoc} inventory. - * @param player The player to be checked, given as a Player object. - * @return Status of player, {@code true} if the player has the items, else {@code false}. - */ - @Override - public boolean isFulfilled(Player player) { - Objects.requireNonNull(player); - return player.getInventory().containsAll(mandatoryItems); - } -} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/Main.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..40c8401f5aada7884147ad58e9650e911474047f --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/Main.java @@ -0,0 +1,16 @@ +package edu.ntnu.idatt2001.group_30.paths; + +import edu.ntnu.idatt2001.group_30.paths.view.App; + +/** + * The Main class is the entry point for the application. + * It is responsible for launching the javafx application. + * + * @author Nicolai H. Brand, Trym H. Gudvangen + */ +public class Main { + + public static void main(String[] args) { + App.main(args); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/PathsSingleton.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/PathsSingleton.java new file mode 100644 index 0000000000000000000000000000000000000000..9946f5eb6901098847ad10f86195f520b032559c --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/PathsSingleton.java @@ -0,0 +1,198 @@ +package edu.ntnu.idatt2001.group_30.paths; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.goals.*; +import java.io.File; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javafx.scene.image.ImageView; + +/** + * This enumeration is constructed using the singleton design pattern. The implementation of this design pattern + * restricts the amount of instances of the enumeration to one. This is essential for the enumeration's purpose. + * The enum contains all the data that needs to be transmitted between controllers. + * + * @author Trym Hamer Gudvangen, Nicolai H. Brand. + */ +public enum PathsSingleton { + INSTANCE; + + private Story story; + private File storyFile; + private Player player = new Player("Default", 100, 100, 100); + private boolean passageMoving = false; + private HealthGoal healthGoal; + private ScoreGoal scoreGoal; + private InventoryGoal inventoryGoal; + private GoldGoal goldGoal; + private ImageView characterImageView; + + /** + * This method gets the current selected story. + * @return Current selected story, given as a Story object. + */ + public Story getStory() { + return story; + } + + /** + * This method sets story being played. + * @param story Story being played, given as a Story object. + */ + public void setStory(Story story) { + this.story = story; + } + + /** + * This method retrieves the file to the story. + * @return The file of the story, given as a File object. + */ + public File getStoryFile() { + return storyFile; + } + + /** + * This method changes the file of the story. + * @param storyFile New file, given as a File object. + */ + public void setStoryFile(File storyFile) { + this.storyFile = storyFile; + } + + /** + * This method retrieves the player. + * @return Current player of game, given as a Player object. + */ + public Player getPlayer() { + return player; + } + + /** + * This method sets the player to a new player. + * @param player New player, given as a Player object. + */ + public void setPlayer(Player player) { + this.player = player; + } + + /** + * This method allows a goal to be changed, given the goal type. + * @param newGoal New goal, given as a Goal implemented Object + */ + public void changeGoal(Goal<?> newGoal) { + setGoal(GoalType.getGoalType(newGoal.getClass().getSimpleName()), newGoal); + } + + /** + * This method sets a given goal to a new goal by the goal type. + * @param goalType Type of goal, given as a GoalType enum. + * @param goal New goal, given as a Goal object. + * @param <T> The type of Goal. + */ + public <T> void setGoal(GoalType goalType, Goal<?> goal) { + switch (goalType) { + case HEALTH_GOAL -> healthGoal = (HealthGoal) goal; + case SCORE_GOAL -> scoreGoal = (ScoreGoal) goal; + case INVENTORY_GOAL -> inventoryGoal = (InventoryGoal) goal; + case GOLD_GOAL -> goldGoal = (GoldGoal) goal; + default -> throw new IllegalArgumentException("Unsupported goal type: " + goalType); + } + } + + /** + * This method retrieves the health goal. + * @return Health goal, given as a HealthGoal object. + */ + public HealthGoal getHealthGoal() { + return healthGoal; + } + + /** + * This method retrieves the score goal. + * @return Score goal, given as a ScoreGoal object. + */ + public ScoreGoal getScoreGoal() { + return scoreGoal; + } + + /** + * This method retrieves the inventory goal. + * @return Inventory goal, given as a InventoryGoal object. + */ + public InventoryGoal getInventoryGoal() { + return inventoryGoal; + } + + /** + * This method retrieves the gold goal. + * @return Gold goal, given as a GoldGoal object. + */ + public GoldGoal getGoldGoal() { + return goldGoal; + } + + /** + * This method gets a goal variable given the GoalType. + * @param goalType Type of goal, given as a GoalType enum. + * @return The goal variable. + * @param <T> Type of goal. + */ + public <T> Goal<?> getGoal(GoalType goalType) { + return switch (goalType) { + case HEALTH_GOAL -> healthGoal; + case SCORE_GOAL -> scoreGoal; + case INVENTORY_GOAL -> inventoryGoal; + case GOLD_GOAL -> goldGoal; + }; + } + + /** + * This method retrieves the character load out image. + * @return Image of the character created, given as an ImageView object. + */ + public ImageView getCharacterImageView() { + return characterImageView; + } + + /** + * This method sets the character load out. + * @param characterImageView New character image, given as an ImageView object. + */ + public void setCharacterImageView(ImageView characterImageView) { + this.characterImageView = characterImageView; + } + + /** + * Returns a list of all the non-null goals. + * @return A list of all the non-null goals, given as a List of Goal objects. + */ + public List<Goal<?>> getGoals() { + List<Goal<?>> goals = Stream + .of(healthGoal, scoreGoal, goldGoal) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (inventoryGoal != null && inventoryGoal.getGoalValue().size() != 0) { + goals.add(inventoryGoal); + } + return goals; + } + + /** + * This method checks whether a passage is currently being moved. + * @return The passageMoving status, given as a boolean. {@code true} if the passage is moving, else {@code false} + */ + public boolean isPassageMoving() { + return passageMoving; + } + + /** + * This method sets the status of whether a passage is currently being moved or not. + */ + public void setPassageMoving(boolean passageMoving) { + this.passageMoving = passageMoving; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/Controller.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/Controller.java new file mode 100644 index 0000000000000000000000000000000000000000..7ea189bd035cc0f2fbf24cb3cc0f00c9598b8957 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/Controller.java @@ -0,0 +1,74 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import edu.ntnu.idatt2001.group_30.paths.view.views.HomeView; +import edu.ntnu.idatt2001.group_30.paths.view.views.View; +import edu.ntnu.idatt2001.group_30.paths.view.views.ViewFactory; +import java.util.HashMap; +import java.util.Map; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.stage.Stage; + +/** + * The class Controller is the superclass of all controllers. + * A Controller is responsible for managing one specific view. + * Every controller has a reference to the StageManager singleton. + * + * @author Nicolai H. Brand, Trym Hamer Gudvangen + */ +public class Controller { + + protected final StageManager STAGE_MANAGER = StageManager.getInstance(); + protected final Map<Class<? extends View<?>>, Runnable> availableViews = new HashMap<>(); + + /** + * Creates a new Controller with the given view classes. + * @param viewClasses The view classes that this controller is responsible for. + */ + @SafeVarargs + public Controller(Class<? extends View<?>>... viewClasses) { + for (Class<? extends View<?>> viewClass : viewClasses) { + availableViews.put(viewClass, () -> STAGE_MANAGER.setCurrentView(ViewFactory.createView(viewClass))); + } + } + + /** + * Returns an EventHandler that will set the current view to the view of the given class. + * @param viewClass The class of the view to set as the current view. + * @return An EventHandler that will set the current view to the view of the given class. + */ + public EventHandler<ActionEvent> goTo(Class<? extends View<?>> viewClass) { + return actionEvent -> availableViews.get(viewClass).run(); + } + + /** + * @return An EventHandler that will set the current view to the previous view. + */ + public EventHandler<ActionEvent> goBack() { + return actionEvent -> STAGE_MANAGER.goBack(); + } + + /** + * Go back to the previous instance of the given view class. + * @param viewClass The class of the view to go back to. + * @return An EventHandler that will set the current view to the previous instance of the given view class. + */ + public EventHandler<ActionEvent> goBackTo(Class<? extends View<?>> viewClass) { + return actionEvent -> STAGE_MANAGER.goBackTo(viewClass); + } + + /** + * This method is used to get the root stage of the application. + * @return The root stage of the application, which is the stage that is used to display the views. + */ + public Stage getRootStage() { + return STAGE_MANAGER.getStage(); + } + + /** + * This method is used to go to the home view. + */ + public void goToHome() { + STAGE_MANAGER.setCurrentView(ViewFactory.createView(HomeView.class)); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/CreatePlayerController.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/CreatePlayerController.java new file mode 100644 index 0000000000000000000000000000000000000000..e197403d78fe5133cf606840d8e82b49f0d910b2 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/CreatePlayerController.java @@ -0,0 +1,18 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import edu.ntnu.idatt2001.group_30.paths.view.views.LoadGameView; + +/** + * The class CreatePlayerController is responsible for managing the CreatePlayerView. + * + * @author Trym Hamer Gudvangen + */ +public class CreatePlayerController extends Controller { + + /** + * Creates a new CreatePlayerController. + */ + public CreatePlayerController() { + super(LoadGameView.class); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/HelpController.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/HelpController.java new file mode 100644 index 0000000000000000000000000000000000000000..e2019fdca619ac953be249ef30894d5c21a025f1 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/HelpController.java @@ -0,0 +1,18 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import edu.ntnu.idatt2001.group_30.paths.view.views.HomeView; + +/** + * This class is used to control the HelpView. + * @author Nicolai H. Brand. + * + */ +public class HelpController extends Controller { + + /** + * Creates a new HelpController. + */ + public HelpController() { + super(HomeView.class); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/HomeController.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/HomeController.java new file mode 100644 index 0000000000000000000000000000000000000000..6753105984aed797a257c8a781bf4a4921878ba7 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/HomeController.java @@ -0,0 +1,30 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import edu.ntnu.idatt2001.group_30.paths.view.views.CreatePlayerView; +import edu.ntnu.idatt2001.group_30.paths.view.views.HelpView; +import edu.ntnu.idatt2001.group_30.paths.view.views.LoadGameView; +import edu.ntnu.idatt2001.group_30.paths.view.views.PlaythroughView; + +/** + * This class is used to control the HomeView. + * @author Nicolai H. Brand, Trym H. Gudvangen. + * + */ +public class HomeController extends Controller { + + /** + * Creates a new HomeController. + */ + public HomeController() { + super(HelpView.class, LoadGameView.class, CreatePlayerView.class, PlaythroughView.class); + } + + /** + * Checks if the PlaythroughView is in the view stack. + * If it is, it means the user has started a game and can continue it. + * @return True if the user can continue a game, false otherwise. + */ + public boolean canContinueAGame() { + return STAGE_MANAGER.getViewStack().stream().anyMatch(view -> view instanceof PlaythroughView); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/NewGameController.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/NewGameController.java new file mode 100644 index 0000000000000000000000000000000000000000..7df50729abee2e0e5a6c06f3e6cb6ae0be566104 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/NewGameController.java @@ -0,0 +1,38 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.model.filehandling.StoryFileReader; +import edu.ntnu.idatt2001.group_30.paths.view.views.NewStoryView; +import edu.ntnu.idatt2001.group_30.paths.view.views.PlaythroughView; +import java.io.File; +import java.io.IOException; + +/** + * This class is used to control the NewGameView. + * + * @author Trym Hamer Gudvangen + */ +public class NewGameController extends Controller { + + /** + * Creates a new NewGameController. + */ + public NewGameController() { + super(PlaythroughView.class, NewStoryView.class); + } + + /** + * Sets the story to the storyFile. + * @param storyFile The story to set, as a File. + */ + public void setStory(File storyFile) { + StoryFileReader storyFileReader = new StoryFileReader(); + try { + INSTANCE.setStory(storyFileReader.parse(storyFile)); + INSTANCE.setStoryFile(storyFile); + } catch (IOException | InstantiationException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/NewStoryController.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/NewStoryController.java new file mode 100644 index 0000000000000000000000000000000000000000..6a646143eafc077e06fdb225f1a0047a4ac98e63 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/NewStoryController.java @@ -0,0 +1,63 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.filehandling.StoryFileWriter; +import edu.ntnu.idatt2001.group_30.paths.view.views.NewStoryView; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import javafx.stage.FileChooser; + +/** + * This class is used to control the NewStoryView. + * + * @author Trym Hamer Gudvangen + */ +public class NewStoryController extends Controller { + + /** + * Creates a new NewStoryController. + */ + public NewStoryController() { + super(NewStoryView.class); + } + + /** + * Adds a story to the PathsSingleton. + * @param title The title of the story, as a String. + * @param passages The passages of the story, as a List of Passages. + * @throws IOException If the story could not be saved to file system. + */ + public void addStory(String title, List<Passage> passages) throws IOException { + Story story = new Story(title, passages.isEmpty() ? null : passages.get(0)); + passages.forEach(story::addPassage); + INSTANCE.setStory(story); + + saveStory(story); + } + + /** + * This method saves the story to the file system. + * @param story The story to save, as a Story. + * @throws IOException If the story could not be saved to file system. + */ + public void saveStory(Story story) throws IOException { + FileChooser fileChooser = new FileChooser(); + fileChooser.setInitialDirectory(new File("./src/main/resources/story-files")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Paths files", "*.paths")); + File selectedFile = fileChooser.showSaveDialog(null); + + if (selectedFile != null) { + StoryFileWriter storyFileWriter = new StoryFileWriter(); + storyFileWriter.create(story, selectedFile); + } else { + throw new FileNotFoundException("File was not saved to file system"); + } + + INSTANCE.setStoryFile(selectedFile); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/PlaythroughController.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/PlaythroughController.java new file mode 100644 index 0000000000000000000000000000000000000000..9cf00291bb89c8bbc3ebef9ef562c91f4f8af6ec --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/PlaythroughController.java @@ -0,0 +1,268 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.model.*; +import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal; +import edu.ntnu.idatt2001.group_30.paths.view.views.HelpView; +import edu.ntnu.idatt2001.group_30.paths.view.views.HomeView; +import java.util.ArrayList; +import java.util.HashMap; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.scene.image.ImageView; + +/** + * Controller for the play-through of the game. + * It is responsible for updating the view with the current state of the game. + * + * @author Nicolai H. Brand. + */ +public class PlaythroughController extends Controller { + + private Playthrough playthrough; + + /* reactive state */ + private final StringProperty gameTitle = new SimpleStringProperty("Playing: " + INSTANCE.getStory().getTitle()); + private final StringProperty passageTitle = new SimpleStringProperty(); + private final StringProperty passageContent = new SimpleStringProperty(); + private final ObservableList<Link> links = FXCollections.observableList(new ArrayList<>()); + private final ObservableList<String> inventory = FXCollections.observableList(new ArrayList<>()); + private final ObservableMap<Goal<?>, Boolean> goals = FXCollections.observableMap(new HashMap<>()); + private final StringProperty health = new SimpleStringProperty(); + private final StringProperty score = new SimpleStringProperty(); + private final StringProperty gold = new SimpleStringProperty(); + private final IntegerProperty playthroughState = new SimpleIntegerProperty(PlaythroughState.PLAYING.ordinal()); + + /** + * Creates a new instance of the controller. + * It initializes the controller and starts a new game. + */ + public PlaythroughController() { + super(HomeView.class, HelpView.class); + startNewPlaythrough(); + } + + /** + * Starts a new Playthrough based on the data in the PathsSingleton. + * It also resets the playthroughState to PLAYING. + */ + public void startNewPlaythrough() { + assert INSTANCE.getPlayer() != null; + assert INSTANCE.getStory() != null; + assert INSTANCE.getGoals() != null; + + /* clear old state */ + playthroughState.setValue(PlaythroughState.PLAYING.ordinal()); + + /* start new game */ + Game game = new Game(new Player(INSTANCE.getPlayer()), INSTANCE.getStory(), INSTANCE.getGoals()); + playthrough = new Playthrough(game); + playthrough.beginPlaythrough(); + updateReactiveProperties(playthrough.getCurrentPassage()); + } + + /** + * Makes a turn in the game based on the given link. + * @param link the link to follow. + */ + public void makeTurn(Link link) { + PlaythroughState state = playthrough.makeTurn(link); + playthroughState.setValue(state.ordinal()); + updateReactiveProperties(playthrough.getCurrentPassage()); + } + + /** + * Updates the reactive properties based on the current state of the game. + * @param passage the current passage. + */ + private void updateReactiveProperties(Passage passage) { + updateCurrentPassage(passage); + updatePlayerData(); + updateGoals(); + updateInventory(); + updateGameTitle(); + } + + /** + * Updates the reactive properties based on the current passage. + * @param passage the current passage. + */ + private void updateCurrentPassage(Passage passage) { + passageTitle.setValue(passage.getTitle()); + passageContent.setValue(passage.getContent()); + links.clear(); + links.addAll(passage.getLinks()); + } + + /** + * Updates the reactive properties based on the current player data. + */ + private void updatePlayerData() { + health.setValue(String.valueOf(getPlayer().getHealth())); + score.setValue(String.valueOf(getPlayer().getScore())); + gold.setValue(String.valueOf(getPlayer().getGold())); + } + + /** + * Updates the reactive properties based on the current goals and their fulfillment. + */ + private void updateGoals() { + goals.clear(); + playthrough.getGame().goals().forEach(goal -> goals.put(goal, goal.isFulfilled(getPlayer()))); + } + + /** + * Updates the reactive properties based on the current inventory. + */ + private void updateInventory() { + inventory.clear(); + inventory.addAll(getPlayer().getInventory()); + } + + /** + * Updates the game title based on the game state. + */ + private void updateGameTitle() { + PlaythroughState state = PlaythroughState + .fromNumber(playthroughState.getValue()) + .orElse(PlaythroughState.PLAYING); + if (state == PlaythroughState.PLAYING) { + gameTitle.setValue("Playing: " + INSTANCE.getStory().getTitle()); + } else if (state == PlaythroughState.DEAD) { + gameTitle.setValue("You died and lost the game!"); + } else if (state == PlaythroughState.STUCK) { + gameTitle.setValue("You are stuck and lost the game!"); + } else if (state == PlaythroughState.WON) { + gameTitle.setValue("You won the game!"); + } else { + gameTitle.setValue("You won the game, but continued playing!"); + } + } + + /** + * Returns the title of the current passage as an observable StringProperty. + * @return the title of the current passage. + */ + public StringProperty getPassageTitle() { + return passageTitle; + } + + /** + * Returns the content of the current passage as an observable StringProperty. + * @return the content of the current passage. + */ + public StringProperty getPassageContent() { + return passageContent; + } + + /** + * Returns the links of the current passage as an observable list. + * @return the links of the current passage. + */ + public ObservableList<Link> getLinks() { + return links; + } + + /** + * Returns the inventory of the player as an observable list. + * @return the inventory of the player. + */ + public ObservableList<String> getInventory() { + return inventory; + } + + /** + * Returns the goals of the game as an observable map. + * @return the goals of the game. + */ + public ObservableMap<Goal<?>, Boolean> getGoals() { + return goals; + } + + /** + * Helper method that returns the current player. + * @return the current player. + */ + private Player getPlayer() { + return playthrough.getGame().player(); + } + + /** + * Returns the name of the player. + * @return the name of the player. + */ + public String getPlayerName() { + return playthrough.getGame().player().getName(); + } + + /** + * Returns the health of the player as an observable StringProperty. + * @return the health of the player. + */ + public StringProperty getHealth() { + return health; + } + + /** + * Returns the score of the player as an observable StringProperty. + * @return the score of the player. + */ + public StringProperty getScore() { + return score; + } + + /** + * Returns the gold of the player as an observable StringProperty. + * @return the gold of the player. + */ + public StringProperty getGold() { + return gold; + } + + /** + * Returns the title of the game as an observable StringProperty. + * The title of the game will be the title of the story while playing. If not playing, it will show the status of the game. + * @return the title of the game. + */ + public StringProperty getGameTitle() { + return gameTitle; + } + + /** + * Returns the image of the character as an ImageView. + * @return the image of the character. + */ + public ImageView getCharacterImageView() { + return INSTANCE.getCharacterImageView(); + } + + /** + * Gets the playthrough state as an observable IntegerProperty. + * @return the image of the character. + */ + public IntegerProperty getPlaythroughState() { + return playthroughState; + } + + /** + * Sets the playthrough state to WON_BUT_KEEP_PLAYING. + */ + public void setWonButKeepPlaying() { + playthrough.setWonButKeepPlaying(); + playthroughState.setValue(PlaythroughState.WON_BUT_KEEP_PLAYING.ordinal()); + } + + /** + * A game can only be continued if it is in the PLAYING or WON_BUT_KEEP_PLAYING state. + * @return whether the game can be kept playing. + */ + public boolean canKeepPlaying() { + return ( + playthroughState.get() == PlaythroughState.PLAYING.ordinal() || + playthroughState.get() == PlaythroughState.WON_BUT_KEEP_PLAYING.ordinal() + ); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/StageManager.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/StageManager.java new file mode 100644 index 0000000000000000000000000000000000000000..48da5eb67041adb4dcd7ccbcd136fda18931c0b4 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/controller/StageManager.java @@ -0,0 +1,145 @@ +package edu.ntnu.idatt2001.group_30.paths.controller; + +import edu.ntnu.idatt2001.group_30.paths.view.views.View; +import java.util.Stack; +import javafx.stage.Stage; + +/** + * The class StageManager is responsible for managing the JavaFX Stage of the application. + * It is implemented as a singleton class and is initialized in the start() method of the Application. + * It keeps track of the current view and previous views using a stack. + * Using a stack makes it easy to go back to the previous view if desirable. + * + * @author Nicolai H. Brand. + */ +public class StageManager { + + private final Stage stage; + private final Stack<View<?>> viewStack; + + /* static reference to the single instance of the class */ + private static StageManager instance; + + /** + * private constructor to prevent external instantiation. + * @param stage The primary stage for this application, onto which the application scene can be set. + */ + private StageManager(Stage stage) { + this.stage = stage; + this.viewStack = new Stack<>(); + stage.show(); + } + + /** + * Initializes the StageManager. + * @param stage The primary stage for this application, onto which the application scene can be set. + * @return The StageManager instance. + */ + public static synchronized StageManager init(Stage stage) { + if (instance == null) { + instance = new StageManager(stage); + } + return instance; + } + + /** + * Returns the StageManager instance if it has been initialized. + * @return The StageManager instance. + * @throws IllegalStateException if the StageManager has not been initialized. + */ + public static synchronized StageManager getInstance() throws IllegalStateException { + if (instance == null) { + throw new IllegalStateException("StageManager not initialized."); + } + return instance; + } + + /** + * Sets the current view of the stage to the given view. + * @param view The view to set as the current view. + */ + public void setCurrentView(View<?> view) { + pushAndUpdate(view); + } + + /** + * Sets the current view of the stage to the previous view in the viewStack. + */ + public void goBack() { + if (viewStack.size() > 1) { + popAndUpdate(); + } + } + + /** + * Sets the current view of the stage to the previous instance of the given view class. + * If the view is not in the viewStack, an IllegalArgumentException is thrown. + * @param viewClass The class of the view to go back to. + * @throws IllegalArgumentException if the view is not in the viewStack. + */ + public void goBackTo(Class<? extends View<?>> viewClass) throws IllegalArgumentException { + while (viewStack.size() > 1 && !viewStack.peek().getClass().equals(viewClass)) { + popAndUpdate(); + } + + if (viewStack.size() == 1 && !viewStack.peek().getClass().equals(viewClass)) { + throw new IllegalArgumentException("The view " + viewClass.getSimpleName() + " is not in the viewStack."); + } + } + + /** + * Pushes the given view onto the viewStack and updates the stage. + * @param view The view to push onto the viewStack. + */ + private void pushAndUpdate(View<?> view) { + viewStack.push(view); + updateView(); + } + + /** + * Pops the top view from the viewStack and updates the stage. + */ + private void popAndUpdate() { + viewStack.pop(); + updateView(); + } + + /** + * Updates the stage to the current scene of the current view. + */ + private void updateView() { + stage.setScene(viewStack.get(viewStack.size() - 1).asScene()); + } + + /** + * Returns the primary stage for this application. + * @return The primary stage for this application. + */ + public Stage getStage() { + return stage; + } + + /** + * Returns the name of the current view without 'View' at the end. + * @return The name of the current view. + */ + public String getCurrentViewName() { + return viewStack.peek().getClass().getSimpleName().replace("View", ""); + } + + /** + * Returns the name of the previous view without 'View' at the end. + * @return The name of the previous view. + */ + public String getPreviousViewName() { + return viewStack.peek().getClass().getSimpleName().replace("View", ""); + } + + /** + * Returns the entire stack of views. + * @return The viewStack. + */ + public Stack<View<?>> getViewStack() { + return viewStack; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/exceptions/CorruptFileException.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/CorruptFileException.java similarity index 88% rename from src/main/java/edu/ntnu/idatt2001/group_30/exceptions/CorruptFileException.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/CorruptFileException.java index f3897248fe64034ad6ce4d034f89e456a279514f..6c82fceaccd72c4e22fde008192560e58ee94982 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/exceptions/CorruptFileException.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/CorruptFileException.java @@ -1,16 +1,14 @@ -package edu.ntnu.idatt2001.group_30.exceptions; +package edu.ntnu.idatt2001.group_30.paths.exceptions; /** * An exception that is thrown when Link information a paths file has been corrupted. */ -public class CorruptFileException extends RuntimeException{ +public class CorruptFileException extends RuntimeException { /** * Constructs a new CorruptFileException with no detail message. */ - public CorruptFileException() { - - } + public CorruptFileException() {} /** * Constructs a new CorruptFileException with the specified detail message. diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/exceptions/CorruptLinkException.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/CorruptLinkException.java similarity index 88% rename from src/main/java/edu/ntnu/idatt2001/group_30/exceptions/CorruptLinkException.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/CorruptLinkException.java index 5a247bb5f01e964d97de8656f71527249cd32b67..f296464ed933258039b1d0a26417ba84d05f9462 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/exceptions/CorruptLinkException.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/CorruptLinkException.java @@ -1,15 +1,14 @@ -package edu.ntnu.idatt2001.group_30.exceptions; +package edu.ntnu.idatt2001.group_30.paths.exceptions; /** * An exception that is thrown when Link information a paths file has been corrupted. */ -public class CorruptLinkException extends RuntimeException{ +public class CorruptLinkException extends RuntimeException { /** * Constructs a new CorruptLinkException with no detail message. */ - public CorruptLinkException() { - } + public CorruptLinkException() {} /** * Constructs a new CorruptLinkException with the specified detail message. @@ -39,5 +38,4 @@ public class CorruptLinkException extends RuntimeException{ public CorruptLinkException(Throwable cause) { super(cause); } - } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/InvalidButtonException.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/InvalidButtonException.java new file mode 100644 index 0000000000000000000000000000000000000000..964f2956b6fbf3a6ec4db2fcb9f5ab8d228fee2e --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/InvalidButtonException.java @@ -0,0 +1,41 @@ +package edu.ntnu.idatt2001.group_30.paths.exceptions; + +/** + * An exception that is thrown when an invalid ButtonType is used. + */ +public class InvalidButtonException extends RuntimeException { + + /** + * Constructs a new InvalidButtonException with no detail message. + */ + public InvalidButtonException() {} + + /** + * Constructs a new InvalidButtonException with the specified detail message. + * + * @param message The detail message, given as a String. + */ + public InvalidButtonException(String message) { + super(message); + } + + /** + * Constructs a new InvalidButtonException with the specified detail message and cause. + * + * @param message The detail message, given as a String + * @param cause The cause, given as a Throwable Object. + */ + public InvalidButtonException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new InvalidButtonException with the specified cause and a detail message of + * {@code cause == null ? null : cause.toString()} (which usually contains the class and detail message of cause). + * + * @param cause The cause, given as a Throwable Object. + */ + public InvalidButtonException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/exceptions/InvalidExtensionException.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/InvalidExtensionException.java similarity index 92% rename from src/main/java/edu/ntnu/idatt2001/group_30/exceptions/InvalidExtensionException.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/InvalidExtensionException.java index 093521ec6b636c665775b23f4f6673e65af0fc49..042ec1ccb5412fe5dc4eb9a0f3001acc053ddbe4 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/exceptions/InvalidExtensionException.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/exceptions/InvalidExtensionException.java @@ -1,15 +1,14 @@ -package edu.ntnu.idatt2001.group_30.exceptions; +package edu.ntnu.idatt2001.group_30.paths.exceptions; /** * An exception that is thrown when an invalid file extension is used. */ -public class InvalidExtensionException extends IllegalArgumentException{ +public class InvalidExtensionException extends IllegalArgumentException { /** * Constructs a new InvalidExtensionException with no detail message. */ - public InvalidExtensionException() { - } + public InvalidExtensionException() {} /** * Constructs a new InvalidExtensionException with the specified detail message. @@ -40,4 +39,3 @@ public class InvalidExtensionException extends IllegalArgumentException{ super(cause); } } - diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Game.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Game.java new file mode 100644 index 0000000000000000000000000000000000000000..f1399d8c02934cadba13d12ba55b69ad7d3d60ac --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Game.java @@ -0,0 +1,72 @@ +package edu.ntnu.idatt2001.group_30.paths.model; + +import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal; +import java.util.List; + +/** + * This class represents the facade of a game of paths. + * This class connects a Player to a Story and has useful methods to start and maneuver through a game. + * + * @author Nicolai H. Brand, Trym Hamer Gudvangen + */ +public record Game(Player player, Story story, List<Goal<?>> goals) { + /** + * This method constructs a Game object with the given parameters. + * + * @param player The player who will be playing game. + * @param story The story for the game that the player will play through. + * @param goals A list of goals that determines the desired goals of the game: + * @throws IllegalArgumentException This exception is thrown if any argument is null. + */ + public Game { + if (player == null || story == null || goals == null) { + throw new IllegalArgumentException("Player, story, and goals cannot be null"); + } + } + + /** + * This method starts a game. + * + * @return the first passage of the story. + */ + public Passage begin() { + return this.story.getOpeningPassage(); + //TODO: Should we make sure the game can not begin more than one time? + // It depends whether or not the end user itself has a way to call begin(), or if this is something + // exclusively done internally. In that case, unit-tests to ensure this method is only called once may + // suffice. + } + + /** + * This method takes in a link and returns the corresponding passage of the story. + * + * @param link a link to a passage in the story. + * @return the corresponding passage for the link. + */ + public Passage go(Link link) { + return this.story.getPassage(link); + } + + /** + * If the player is out of health, the game is defined as over. + * + * @return {@code true} if the player is out of health, else {@code false}. + */ + public boolean isGameOver() { + return this.player.getHealth() <= 0; + } + + /** + * This method checks if the player has fulfilled all the goals of the game. + * If the player has fulfilled all the goals, the game is defined as won. + * + * @return {@code true} if the player has fulfilled all the goals, else {@code false}. + */ + public boolean isGameWon() { + /* + * One may think it's faster to check for one non-fulfilled goal and then exit early. + * This is actually what allMatch does internally, so this is just as fast while being more readable + */ + return this.goals.stream().allMatch(goal -> goal.isFulfilled(this.player)); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/Link.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Link.java similarity index 82% rename from src/main/java/edu/ntnu/idatt2001/group_30/Link.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Link.java index 5b62790b8dc10ee87a2b03dba6c57428d737387a..1ee84cea842810721cd34736bac990cb36f766b6 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/Link.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Link.java @@ -1,7 +1,6 @@ -package edu.ntnu.idatt2001.group_30; - -import edu.ntnu.idatt2001.group_30.actions.Action; +package edu.ntnu.idatt2001.group_30.paths.model; +import edu.ntnu.idatt2001.group_30.paths.model.actions.Action; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -13,6 +12,7 @@ import java.util.Objects; * @author Nicolai H. Brand, Trym Hamer Gudvangen */ public class Link { + private final String text; private final String reference; private final List<Action<?>> actions; @@ -23,7 +23,7 @@ public class Link { * @param reference, a string that unambiguously identify a passage * @throws IllegalArgumentException This exception is thrown if text or reference is invalid */ - public Link(String text, String reference) throws IllegalArgumentException{ + public Link(String text, String reference) throws IllegalArgumentException { if (text.isBlank()) throw new IllegalArgumentException("Text cannot be blank or empty."); this.text = text; if (reference.isBlank()) throw new IllegalArgumentException("Reference cannot be blank or empty."); @@ -31,7 +31,6 @@ public class Link { this.actions = new ArrayList<>(); } - /** * Adds an action to the list of actions * @param action, the action to be added to the list @@ -76,16 +75,4 @@ public class Link { public int hashCode() { return Objects.hash(reference); } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("[").append(text).append("](").append(reference).append(")\n"); - - for(Action<?> action : actions) { - sb.append("<").append(action.getClass().getSimpleName()).append(">\\").append(action.getActionValue()).append("/\n"); - } - - return sb.toString(); - } } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/Passage.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Passage.java similarity index 90% rename from src/main/java/edu/ntnu/idatt2001/group_30/Passage.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Passage.java index 924cb4fca502e7bdd05ee34c72d452a6bad142dc..f4a7731655eb703736d01abc9780950a7c50d7d0 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/Passage.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Passage.java @@ -1,4 +1,4 @@ -package edu.ntnu.idatt2001.group_30; +package edu.ntnu.idatt2001.group_30.paths.model; import java.util.ArrayList; import java.util.List; @@ -11,6 +11,7 @@ import java.util.Objects; * @author Trym Hamer Gudvangen */ public class Passage { + private final String title; private final String content; private final List<Link> links; @@ -21,7 +22,7 @@ public class Passage { * @param content The content of the passage, represented using a String. * @throws IllegalArgumentException This exception is thrown if title or content is invalid. */ - public Passage(String title, String content) throws IllegalArgumentException{ + public Passage(String title, String content) throws IllegalArgumentException { if (title.isBlank()) throw new IllegalArgumentException("Title cannot be blank or empty"); this.title = title; if (content.isBlank()) throw new IllegalArgumentException("Content cannot be blank or empty"); @@ -72,18 +73,6 @@ public class Passage { return this.links.size() > 0; } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - sb.append("::").append(title).append("\n") - .append(content).append("\n"); - - links.forEach(link -> sb.append(link.toString())); - - return sb.toString(); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/Player.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Player.java similarity index 51% rename from src/main/java/edu/ntnu/idatt2001/group_30/Player.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Player.java index 19d3b387df95f8d70c3bfbd9a55194e7d59644af..d07ff536e61f4e3c9d16c4254edf064c61b9fd44 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/Player.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Player.java @@ -1,4 +1,4 @@ -package edu.ntnu.idatt2001.group_30; +package edu.ntnu.idatt2001.group_30.paths.model; import java.util.ArrayList; import java.util.List; @@ -7,9 +7,10 @@ import java.util.List; * This class represents a Player who will experience the story. Therefore, it contains information surrounding their * character, such as health, score, gold, and inventory. * - * @author Trym Hamer Gudvangen + * @author Trym Hamer Gudvangen, Nicolai H. Brand */ public class Player { + private final String name; private int health; private int score; @@ -24,7 +25,7 @@ public class Player { * @param gold Amount of gold the player has, represented using an int. * @throws IllegalArgumentException This exception is thrown if the health, score, gold or name arguments are invalid. */ - public Player(String name, int health, int score, int gold) throws IllegalArgumentException{ + public Player(String name, int health, int score, int gold) throws IllegalArgumentException { if (name.isBlank()) throw new IllegalArgumentException("The name cannot be blank or empty"); this.name = name; if (health < 0) throw new IllegalArgumentException("Initial health cannot be less than 0"); @@ -36,6 +37,31 @@ public class Player { this.inventory = new ArrayList<>(); } + /** + * This constructor creates a new Player from an existing Player object. + * This creates a deep copy of the Player object. + * @param player The Player object to be copied. + */ + public Player(Player player) { + this.name = player.name; + this.health = player.health; + this.score = player.score; + this.gold = player.gold; + this.inventory = new ArrayList<>(player.inventory); + } + + /** + * The Builder class is used to create a Player object. + * @param build The Builder object that contains the information needed to create a Player object. + */ + public Player(Builder build) { + this.name = build.name; + this.health = build.health; + this.score = build.score; + this.gold = build.gold; + this.inventory = build.inventory; + } + /** * This method retrieves the name of the player. * @return Name of the player, represented using a String. @@ -93,6 +119,14 @@ public class Player { this.gold += gold; } + /** + * This method sets the gold field to the given value. + * @param gold New gold value, given as an int. + */ + public void setGold(int gold) { + this.gold = gold; + } + /** * This method retrieves the player's amount of gold. * @return Player's amount of gold, represented using an int. @@ -116,4 +150,74 @@ public class Player { public List<String> getInventory() { return this.inventory; } + + /** + * Static Builder class that enables the creation of a Player object using the builder pattern. + * @author Nicolai H. Brand + */ + public static class Builder { + + private final String name; + + private int health = 0; + private int score = 0; + private int gold = 0; + private final List<String> inventory = new ArrayList<>(); + + /** + * Constructor for the Builder class. + * @param name Name of the player, represented using a String. + */ + public Builder(String name) { + this.name = name; + } + + /** + * This method creates a Player object using the information provided by the Builder object. + * @return The Player object, created using the information provided by the Builder object. + */ + public Player build() { + return new Player(this); + } + + /** + * This method sets the health of the player and returns the Builder object to enable method chaining. + * @param health Health of the player, represented using an int. + * @return The Builder object, to enable method chaining. + */ + public Builder health(int health) { + this.health = health; + return this; + } + + /** + * This method sets the score of the player and returns the Builder object to enable method chaining. + * @param score Score of the player, represented using an int. + * @return The Builder object, to enable method chaining. + */ + public Builder score(int score) { + this.score = score; + return this; + } + + /** + * This method sets the gold of the player and returns the Builder object to enable method chaining. + * @param gold Gold of the player, represented using an int. + * @return The Builder object, to enable method chaining. + */ + public Builder gold(int gold) { + this.gold = gold; + return this; + } + + /** + * This method adds an item to the player's inventory and returns the Builder object to enable method chaining. + * @param item Item to be added to the player's inventory, represented using a String. + * @return The Builder object, to enable method chaining. + */ + public Builder addToInventory(String item) { + this.inventory.add(item); + return this; + } + } } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Playthrough.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Playthrough.java new file mode 100644 index 0000000000000000000000000000000000000000..0433bdbdf3917141aa0e72c7cb124f69b26fafe0 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Playthrough.java @@ -0,0 +1,101 @@ +package edu.ntnu.idatt2001.group_30.paths.model; + +import java.util.Objects; + +/** + * The Playthrough class represents a playthrough of a game. + * Its responsibility is to keep track of the current state of the playthrough, and to update it when the player makes a turn. + * It also keeps track of the current passage the player is in, as this needs to be known in order to figure out the game state. + * + * @author Nicolai H. Brand. + */ +public class Playthrough { + + private final Game game; + private PlaythroughState gameState; + private Passage currentPassage; + + /** + * Creates a new instance of the Playthrough class. + * @param game the game to play through. + */ + public Playthrough(Game game) { + Objects.requireNonNull(game, "Game cannot be null"); + this.game = game; + this.gameState = PlaythroughState.PLAYING; + } + + /** + * Begins the playthrough by starting the game. + * @return the current state of the playthrough. + */ + public PlaythroughState beginPlaythrough() { + currentPassage = game.begin(); + updateGameState(); + return gameState; + } + + /** + * Makes a turn in the game. + * @param link the link to follow. + * @return the current state of the playthrough. + */ + public PlaythroughState makeTurn(Link link) { + Objects.requireNonNull(link, "Link cannot be null"); + currentPassage = game.go(link); + link.getActions().forEach(action -> action.execute(game.player())); + updateGameState(); + return gameState; + } + + /** + * Updates the state of the playthrough. + * This is done by checking if the game is over, and if so, what the outcome is. + */ + private void updateGameState() { + if (gameState == PlaythroughState.WON_BUT_KEEP_PLAYING) { + return; + } + + /* + * if you win, but also die in the same turn, this is counted as a loss: you can't win if you're dead :-) + */ + if (game.isGameOver()) { + gameState = PlaythroughState.DEAD; + return; + } + + if (game.isGameWon()) { + gameState = PlaythroughState.WON; + return; + } + + if (currentPassage != null && !currentPassage.hasLinks()) { + gameState = PlaythroughState.STUCK; + } + } + + /** + * Sets the game state to WON_BUT_KEEP_PLAYING. + * This is used when the player has won the game, but still wants to keep playing. + */ + public void setWonButKeepPlaying() { + gameState = PlaythroughState.WON_BUT_KEEP_PLAYING; + } + + /** + * Returns the current state of the playthrough. + * @return the current state of the playthrough. + */ + public Passage getCurrentPassage() { + return currentPassage; + } + + /** + * Returns the game. + * @return the game. + */ + public Game getGame() { + return game; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/PlaythroughState.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/PlaythroughState.java new file mode 100644 index 0000000000000000000000000000000000000000..0785823b86eaa325789ff1695d9db343ca88dd10 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/PlaythroughState.java @@ -0,0 +1,48 @@ +package edu.ntnu.idatt2001.group_30.paths.model; + +import java.util.Arrays; +import java.util.Optional; + +/** + * The PlaythroughState enum represents the different states a playthrough can be in. + * It is used to keep track of the current state of the playthrough, and to update it when the player makes a turn. + * Note: + * A good systems language (c or c++) would have automatically converted the enum symbols to integers. + * This is not the case in Java, so we have to do it manually. Not very idiomatic, but it works. + * + * @author Nicolai H. Brand. + */ +public enum PlaythroughState { + PLAYING(0), + WON(1), + DEAD(2), + STUCK(3), + WON_BUT_KEEP_PLAYING(4); + + private final int number; + + /** + * Creates a new instance of the PlaythroughState enum. + * @param number the number of the state. + */ + PlaythroughState(int number) { + this.number = number; + } + + /** + * Gets the number of the state. + * @return the number of the state. + */ + public int getNumber() { + return number; + } + + /** + * Gets the PlaythroughState from a number. + * @param number the number of the state. + * @return the PlaythroughState. + */ + public static Optional<PlaythroughState> fromNumber(int number) { + return Arrays.stream(PlaythroughState.values()).filter(value -> value.getNumber() == number).findFirst(); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/Story.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Story.java similarity index 77% rename from src/main/java/edu/ntnu/idatt2001/group_30/Story.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Story.java index 08379f01cba59831e486dc2cdaacd0f84b79c2d8..183490579c36ad8c7277f8433eb551b23503ff76 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/Story.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/Story.java @@ -1,11 +1,10 @@ -package edu.ntnu.idatt2001.group_30; +package edu.ntnu.idatt2001.group_30.paths.model; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Stream; /** * A Story is a non-linear narrative consisting of a collection of passages @@ -13,7 +12,8 @@ import java.util.stream.Stream; * @author Nicolai H. Brand, Trym Hamer Gudvangen */ public class Story { - private final String title; + + private String title; private final Map<Link, Passage> passages; private final Passage openingPassage; @@ -24,18 +24,18 @@ public class Story { * @param openingPassage the first passage of the story. Also added to the passage. * @throws IllegalArgumentException This exception is thrown if title or openingPassage is invalid */ - public Story(String title, Passage openingPassage) throws IllegalArgumentException{ - //if (title.isBlank() || !title.matches("[a-zA-Z]")) { - // throw new IllegalArgumentException("Title cannot be blank, empty, or contain special characters."); - //} - if (title.isBlank()) throw new IllegalArgumentException("Title cannot be blank, empty, or contain special characters."); - this.title = title; + public Story(String title, Passage openingPassage) throws IllegalArgumentException { + setTitle(title); if (openingPassage == null) throw new IllegalArgumentException("Opening passage cannot be null"); this.openingPassage = openingPassage; this.passages = new HashMap<>(); addPassage(this.openingPassage); } + public boolean goalsAreAchievable() { + return true; + } + /** * Adds a passage to the passages map * The link object used as the key in the map is made from properties of the passage @@ -65,19 +65,17 @@ public class Story { public boolean removePassage(Link link) { Passage toRemove = this.passages.get(link); /* the passage was not found */ - if (toRemove == null) - return false; - + if (toRemove == null) return false; /* find out if any other passages has a link to the passage we want to remove */ - boolean anyMatch = this.passages.values() + boolean anyMatch = + this.passages.values() .stream() .flatMap(passage -> passage.getLinks().stream()) .anyMatch(l -> l.equals(link)); //TODO: should we throw an exception here instead? - if (anyMatch) - return false; + if (anyMatch) return false; this.passages.remove(link); return true; @@ -90,10 +88,10 @@ public class Story { */ public List<Link> getBrokenLinks() { return this.passages.values() - .stream() - .flatMap(passage -> passage.getLinks().stream()) - .filter(link -> this.passages.get(link) == null) - .toList(); + .stream() + .flatMap(passage -> passage.getLinks().stream()) + .filter(link -> this.passages.get(link) == null) + .toList(); } /** @@ -103,6 +101,17 @@ public class Story { return title; } + /** + * This method sets the title of the story. + * @param title The new title of the story, given as a {@code String}. + */ + public void setTitle(String title) { + if (title == null || title.isBlank()) throw new IllegalArgumentException( + "Title cannot be blank, empty, or contain special characters." + ); + this.title = title; + } + /** * This method retrieves all the passages of a story. * @return All the passages of the Story as a {@code Collection<Passages>}. @@ -118,19 +127,6 @@ public class Story { return openingPassage; } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(this.title).append("\n\n"); - sb.append(this.openingPassage.toString()).append("\n"); - - this.passages.values().forEach(passage -> { - if(!passage.equals(openingPassage)) sb.append(passage.toString()).append("\n"); - }); - - return sb.toString(); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/Action.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/Action.java similarity index 87% rename from src/main/java/edu/ntnu/idatt2001/group_30/actions/Action.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/Action.java index 8714a90830a37b3673eb9552a0c32a8f2d2e1cc9..0e53f239b817e980660e4ffb53dc457a1b1112a7 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/Action.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/Action.java @@ -1,6 +1,6 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; -import edu.ntnu.idatt2001.group_30.Player; +import edu.ntnu.idatt2001.group_30.paths.model.Player; /** * The Action interface provides the method signature for executing an attribute @@ -9,7 +9,6 @@ import edu.ntnu.idatt2001.group_30.Player; * @author Trym Hamer Gudvangen */ public interface Action<T> { - /** * This method changes a given player's attribute: * @param player The player, given as a Player object. @@ -28,5 +27,4 @@ public interface Action<T> { * @return Boolean representing {@code true} if the actions are equal, otherwise {@code false} */ boolean equals(Object o); - } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/ActionFactory.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionFactory.java similarity index 90% rename from src/main/java/edu/ntnu/idatt2001/group_30/actions/ActionFactory.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionFactory.java index cfa8198f39564195645e1e8f1416586c67799377..abc0c33a90546908765b48ec106611ace8a8cfc1 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/ActionFactory.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionFactory.java @@ -1,4 +1,4 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; /** * This class represents a factory for producing Action objects. It, therefore, has methods for instantiating a single @@ -15,7 +15,7 @@ public class ActionFactory { * @param actionValue The action value, given as a String. * @return An action object with the information specified */ - public static Action<?> getAction(ActionType actionType, String actionValue) throws IllegalArgumentException{ + public static Action<?> getAction(ActionType actionType, String actionValue) throws IllegalArgumentException { return switch (actionType) { case GOLD_ACTION -> new GoldAction(Integer.parseInt(actionValue)); case HEALTH_ACTION -> new HealthAction(Integer.parseInt(actionValue)); @@ -23,5 +23,4 @@ public class ActionFactory { case SCORE_ACTION -> new ScoreAction(Integer.parseInt(actionValue)); }; } - } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionType.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionType.java new file mode 100644 index 0000000000000000000000000000000000000000..bedbaf92da351ab21f79d3ce6534fa9f4840b6b5 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionType.java @@ -0,0 +1,23 @@ +package edu.ntnu.idatt2001.group_30.paths.model.actions; + +/** + * This enumeration represents the different types of actions that exist: gold, health, inventory, and score. + * + * @author Trym Hamer Gudvangen + */ +public enum ActionType { + GOLD_ACTION("Gold Action"), + HEALTH_ACTION("Health Action"), + INVENTORY_ACTION("Inventory Action"), + SCORE_ACTION("Score Action"); + + private final String displayName; + + ActionType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/GoldAction.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/GoldAction.java similarity index 90% rename from src/main/java/edu/ntnu/idatt2001/group_30/actions/GoldAction.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/GoldAction.java index 43e6f8b475da7ca4b0ac04bf29a104ee2bfcbb7c..f479b4826f43348845ddf5d583ff51b87cd76763 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/GoldAction.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/GoldAction.java @@ -1,6 +1,6 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; -import edu.ntnu.idatt2001.group_30.Player; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** @@ -18,10 +18,8 @@ public class GoldAction implements Action<Integer> { */ public GoldAction(int gold) { this.gold = gold; - //TODO: Add exception? } - /** * {@inheritDoc} gold. * @param player The player, given as a Player object. diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/HealthAction.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/HealthAction.java similarity index 90% rename from src/main/java/edu/ntnu/idatt2001/group_30/actions/HealthAction.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/HealthAction.java index 97dae448955ac2e3943a2ae94551c3e6c1a0652b..55a4afd0def466b0947c3ba49f16881a21dbbdfa 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/HealthAction.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/HealthAction.java @@ -1,7 +1,6 @@ -package edu.ntnu.idatt2001.group_30.actions; - -import edu.ntnu.idatt2001.group_30.Player; +package edu.ntnu.idatt2001.group_30.paths.model.actions; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** @@ -19,7 +18,6 @@ public class HealthAction implements Action<Integer> { */ public HealthAction(int health) { this.health = health; - //TODO: Add exception? } /** diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/InventoryAction.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/InventoryAction.java similarity index 91% rename from src/main/java/edu/ntnu/idatt2001/group_30/actions/InventoryAction.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/InventoryAction.java index 35da920a66ed7047bd5589e32facfef25bd72f8b..040768f0cd0e2e59ba83e4564cd45fed6c6f6901 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/InventoryAction.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/InventoryAction.java @@ -1,7 +1,6 @@ -package edu.ntnu.idatt2001.group_30.actions; - -import edu.ntnu.idatt2001.group_30.Player; +package edu.ntnu.idatt2001.group_30.paths.model.actions; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** @@ -19,7 +18,6 @@ public class InventoryAction implements Action<String> { */ public InventoryAction(String item) { this.item = item; - //TODO: Add exception? } /** diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/actions/ScoreAction.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ScoreAction.java similarity index 90% rename from src/main/java/edu/ntnu/idatt2001/group_30/actions/ScoreAction.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ScoreAction.java index 9d20b1b2044f96877b7a9d3c4a9f603bd991cbc5..9c09207642e0b830dd1a90578c38a7aff1e2bd21 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/actions/ScoreAction.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ScoreAction.java @@ -1,7 +1,6 @@ -package edu.ntnu.idatt2001.group_30.actions; - -import edu.ntnu.idatt2001.group_30.Player; +package edu.ntnu.idatt2001.group_30.paths.model.actions; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** @@ -19,7 +18,6 @@ public class ScoreAction implements Action<Integer> { */ public ScoreAction(int points) { this.points = points; - //TODO: Add exception? } /** diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/filehandling/FileHandler.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/FileHandler.java similarity index 51% rename from src/main/java/edu/ntnu/idatt2001/group_30/filehandling/FileHandler.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/FileHandler.java index fdcc1b244a01d653a0a4661479c32ac69df31116..c3ccf40a0d17d697b74906bb2051bb35b24ad391 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/filehandling/FileHandler.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/FileHandler.java @@ -1,23 +1,27 @@ -package edu.ntnu.idatt2001.group_30.filehandling; - -import edu.ntnu.idatt2001.group_30.exceptions.InvalidExtensionException; +package edu.ntnu.idatt2001.group_30.paths.model.filehandling; +import edu.ntnu.idatt2001.group_30.paths.exceptions.InvalidExtensionException; import java.io.File; import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class contains general-use methods for handling files such as file creation and name and path validation. * - * @author Trym Hamer Gudvangen + * @author Trym Hamer Gudvangen, Nicolai H. Brand. */ public class FileHandler { private static final Pattern VALID_CHAR = Pattern.compile("[^<>:\"/*|?\\\\]*"); private static final Pattern VALID_EXTENSION = Pattern.compile(".*\\.paths$"); - private static String defaultPath = "src/main/resources/story-files"; - + private static final String DEFAULT_PATH = + "src" + File.separator + "main" + File.separator + "resources" + File.separator + "story-files"; + private static Path defaultPath = Paths.get(DEFAULT_PATH); /** * This method checks whether the given file name is valid. @@ -25,22 +29,51 @@ public class FileHandler { * @return {@code true} if the file name is valid. * @throws IllegalArgumentException This exception is thrown if given file name is blank or has invalid characters. */ - public static boolean isFileNameValid(String fileName) throws IllegalArgumentException{ - if(fileName.isBlank()) throw new IllegalArgumentException("File name cannot be blank"); + public static boolean isFileNameValid(String fileName) throws IllegalArgumentException { + if (fileName.isBlank()) throw new IllegalArgumentException("File name cannot be blank"); Matcher matcher = VALID_CHAR.matcher(fileName); - if(!matcher.matches()) throw new IllegalArgumentException("File name contains invalid characters"); + if (!matcher.matches()) { + String illegalChars = findInvalidCharacters(fileName); + throw new IllegalArgumentException("File name contains invalid characters: " + illegalChars); + } return true; } + /** + * This is a helper method for finding the specific invalid characters in a file name. + * This is used to give a more specific error message to the user. + * @param fileName Name of the file, given as a String + * @return A String containing the invalid characters in the file name + */ + private static String findInvalidCharacters(String fileName) { + StringBuilder nonMatchingChars = new StringBuilder(); + Set<Character> uniqueChars = new HashSet<>(); + + Matcher matcher = VALID_CHAR.matcher(fileName); + /* finds all the non-matching segments in the file name and adds the unique characters to a set */ + matcher + .results() + .forEach(result -> { + String nonMatchingSegment = fileName.substring(result.start(), result.end()); + nonMatchingSegment + .chars() + .mapToObj(c -> (char) c) + .filter(uniqueChars::add) + .forEach(nonMatchingChars::append); + }); + + return nonMatchingChars.toString(); + } + /** * This method checks whether the given file contains .paths as the extension. * @param fileName Name of the file including extension, given as a String * @return {@code true} if file name contains .paths, else {@code false} * @throws InvalidExtensionException This exception is thrown if the file name does not contain .paths at the end */ - public static boolean isFileExtensionValid(String fileName) throws InvalidExtensionException{ + public static boolean isFileExtensionValid(String fileName) throws InvalidExtensionException { Matcher matcher = VALID_EXTENSION.matcher(fileName); - if(!matcher.matches()) throw new InvalidExtensionException("File name contains invalid characters"); + if (!matcher.matches()) throw new InvalidExtensionException("File name contains invalid characters"); return true; } @@ -50,7 +83,17 @@ public class FileHandler { * @return If the file contains no information, {@code false} is returned. Else, {@code true} is returned */ public static boolean fileExists(File file) { - return file.length() > 0; + return file.exists() && file.length() > 0; + } + + /** + * This method checks if a file exists with a given directory path and whether it contains any information. + * @param fileName The file name to be checked, given as a String + * @return If the file contains no information, {@code false} is returned. Else, {@code true} is returned + */ + public static boolean fileExists(String fileName) { + File file = createFile(fileName); + return fileExists(file); } /** @@ -59,7 +102,7 @@ public class FileHandler { * @return The file with the given name, represented using a File object. * @throws IllegalArgumentException This exception is thrown if the file name is invalid. */ - public static File createFile(String fileName) throws IllegalArgumentException{ + public static File createFile(String fileName) throws IllegalArgumentException { isFileNameValid(fileName); return new File(getFileSourcePath(fileName)); } @@ -69,16 +112,15 @@ public class FileHandler { * @param fileName Name of the desired file, represented as a String * @return The source path to the file, represented as a String */ - public static String getFileSourcePath(String fileName){ - return FileSystems.getDefault().getPath(defaultPath, fileName) + ".paths"; + public static String getFileSourcePath(String fileName) { + return defaultPath.resolve(fileName + ".paths").toString(); } - //TODO: test for different OS /** * This method changes the default path used to create files. This may, for example, be used for testing purposes. * @param newPath New default path, given as a String */ - public static void changeDefaultPath(String newPath) { + public static void changeDefaultPath(Path newPath) { defaultPath = newPath; } } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/filehandling/StoryFileHandler.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileReader.java similarity index 58% rename from src/main/java/edu/ntnu/idatt2001/group_30/filehandling/StoryFileHandler.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileReader.java index 715be06a6117936c30ecfbf2762205074de902bd..1234eb3afcb5963b2705b6aacdfe2809d8606f1f 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/filehandling/StoryFileHandler.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileReader.java @@ -1,87 +1,81 @@ -package edu.ntnu.idatt2001.group_30.filehandling; - -import edu.ntnu.idatt2001.group_30.Link; -import edu.ntnu.idatt2001.group_30.Passage; -import edu.ntnu.idatt2001.group_30.Story; -import edu.ntnu.idatt2001.group_30.actions.Action; -import edu.ntnu.idatt2001.group_30.actions.ActionFactory; -import edu.ntnu.idatt2001.group_30.actions.ActionType; -import edu.ntnu.idatt2001.group_30.exceptions.CorruptFileException; -import edu.ntnu.idatt2001.group_30.exceptions.CorruptLinkException; - -import java.io.*; -import java.util.*; +package edu.ntnu.idatt2001.group_30.paths.model.filehandling; + +import edu.ntnu.idatt2001.group_30.paths.exceptions.CorruptFileException; +import edu.ntnu.idatt2001.group_30.paths.exceptions.CorruptLinkException; +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.actions.Action; +import edu.ntnu.idatt2001.group_30.paths.model.actions.ActionFactory; +import edu.ntnu.idatt2001.group_30.paths.model.actions.ActionType; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; /** - * This class maintains the storage and retrieval of a Story file. This is done through a Buffered - * writer and reader. + * The StoryFileReader class is responsible for reading a story file and parsing it to create a story object. + * It is also responsible for validating the file. + * + * @author Trym Hamer Gudvangen, Nicolai H. Brand. */ -public class StoryFileHandler { +public class StoryFileReader { private final Pattern LINK_PATTERN = Pattern.compile("\\[.*]\\(.*\\)"); private final Pattern ACTION_PATTERN = Pattern.compile("<.*>\\\\.*/"); /** - * This method takes a story and writes its contents to a .paths file. The story information is transcribed - * in the given format: - * <pre> - * Story title - * - * ::Opening Passage Title - * Opening Passage Content - * [Link Text](Link Reference) - * - * ::Another Passage Title - * Passage Content - * [Link Text](Link Reference) - * {@code <Action Type>}\Action Value/ - * [Link Text](Link Reference) - * - * ... - * </pre> - * @param story The story to be saved, given as a Story object. - * @param fileName The name of the file the story will be saved to, given as a String. - * @throws IOException This exception is thrown if an I/O error occurs with the writer. + * This method takes a story file and parses it to create a story object. + * @param fileName The name of the story file, given as a String. + * @return The story from the file, given as a Story object. + * @throws IOException This exception is thrown if an I/O error occurs with the reader. */ - public void createStoryFile(Story story, String fileName) throws IOException { - Objects.requireNonNull(story, "Story cannot be null"); + public Story parse(String fileName) throws IOException, InstantiationException { Objects.requireNonNull(fileName, "File name cannot be null"); - File file = FileHandler.createFile(fileName); - if(FileHandler.fileExists(file)) throw new IllegalArgumentException("You cannot overwrite a pre-existing story file"); - try(BufferedWriter storyBufferedWriter = new BufferedWriter(new FileWriter(file))){ - storyBufferedWriter.write(story.toString()); - } + File file = new File(FileHandler.getFileSourcePath(fileName)); + return parse(file); } /** * This method takes a story file and parses it to create a story object. - * @param fileName The name of the story file, given as a String. + * @param file The story file, given as a File object. * @return The story from the file, given as a Story object. * @throws IOException This exception is thrown if an I/O error occurs with the reader. */ - public Story readStoryFromFile(String fileName) throws IOException, InstantiationException { - Objects.requireNonNull(fileName, "File name cannot be null"); - File file = new File(FileHandler.getFileSourcePath(fileName)); - if(!FileHandler.fileExists(file)) throw new IllegalArgumentException("There is no story file with that name!"); + public Story parse(File file) throws IOException, InstantiationException { + Objects.requireNonNull(file, "File does not exist"); + if (!FileHandler.fileExists(file)) throw new IllegalArgumentException("There is no story file with that name!"); Story story; - try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) { - + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) { String storyTitle = bufferedReader.readLine(); - List<String> passageInfo = new ArrayList<>(List.of(bufferedReader.lines() - .collect(Collectors.joining("\n")).split("\n::"))); - - if(passageInfo.size() == 1) throw new CorruptFileException(storyTitle); + List<String> passageInfo = new ArrayList<>( + List.of( + bufferedReader + .lines() + .collect(Collectors.joining(System.lineSeparator())) + .split(System.lineSeparator() + "::") + ) + ); + + if (passageInfo.size() == 1) throw new CorruptFileException( + "The title of a passage was corrupt.\nPassage title: " + + storyTitle + + "\nThe expected format is ::PASSAGE TITLE NAME" + ); passageInfo.remove(0); Passage openingPassage = parseStringToPassage(passageInfo.remove(0)); story = new Story(storyTitle, openingPassage); - for(String passage : passageInfo) { + for (String passage : passageInfo) { story.addPassage(parseStringToPassage(passage)); } } @@ -97,12 +91,12 @@ public class StoryFileHandler { * @return The passage, given as a Passage object. */ private Passage parseStringToPassage(String passageInfo) throws InstantiationException { - String[] splitPassageInfo = passageInfo.split("\n"); + String[] splitPassageInfo = passageInfo.split(System.lineSeparator()); Passage passage = new Passage(splitPassageInfo[0], splitPassageInfo[1]); - for(int i = 2; i < splitPassageInfo.length; i++) { + for (int i = 2; i < splitPassageInfo.length; i++) { Link link = parseStringToLink(splitPassageInfo[i]); passage.addLink(link); - while(i + 1 < splitPassageInfo.length && ACTION_PATTERN.matcher(splitPassageInfo[i+1]).matches()){ + while (i + 1 < splitPassageInfo.length && ACTION_PATTERN.matcher(splitPassageInfo[i + 1]).matches()) { link.addAction(parseStringToAction(splitPassageInfo[++i])); } } @@ -116,9 +110,14 @@ public class StoryFileHandler { * @param linkInfo The information of the link, given as a String. * @return The link, given as a Link object. */ - private Link parseStringToLink(String linkInfo){ - if(!LINK_PATTERN.matcher(linkInfo).matches()) throw new CorruptLinkException(linkInfo); - String text = linkInfo.substring(linkInfo.indexOf("[") + 1, linkInfo.indexOf("]") ); + private Link parseStringToLink(String linkInfo) { + if (!LINK_PATTERN.matcher(linkInfo).matches()) throw new CorruptLinkException( + "The link info is corrupt in the file.\n" + + "Link info: " + + linkInfo + + "\nThe link should be in this form [Link Name](Reference)" + ); + String text = linkInfo.substring(linkInfo.indexOf("[") + 1, linkInfo.indexOf("]")); String reference = linkInfo.substring(linkInfo.indexOf("(") + 1, linkInfo.indexOf(")")); return new Link(text, reference); @@ -145,8 +144,8 @@ public class StoryFileHandler { * @return The type of Action extracted from the String, given as a ActionType enumeration * @throws InstantiationException This exception is thrown if the action type information is corrupt */ - private ActionType extractActionTypeFromInfo(String actionTypeInfo) throws InstantiationException{ - return switch(actionTypeInfo) { + private ActionType extractActionTypeFromInfo(String actionTypeInfo) throws InstantiationException { + return switch (actionTypeInfo) { case "GoldAction" -> ActionType.GOLD_ACTION; case "HealthAction" -> ActionType.HEALTH_ACTION; case "InventoryAction" -> ActionType.INVENTORY_ACTION; @@ -154,5 +153,4 @@ public class StoryFileHandler { default -> throw new InstantiationException("The Action type information is corrupt"); }; } - -} \ No newline at end of file +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileWriter.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..3bbc14c868a9f8c3726d4dee9678b828722892f6 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileWriter.java @@ -0,0 +1,162 @@ +package edu.ntnu.idatt2001.group_30.paths.model.filehandling; + +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.actions.Action; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.util.*; + +/** + * This class is responsible for writing a story to a file. + * + * @author Trym Hamer Gudvangen, Nicolai H. Brand. + */ +public class StoryFileWriter { + + /** + * This method takes a story and writes its contents to a .paths file. The story information is transcribed + * in the given format: + * <pre> + * Story title + * + * ::Opening Passage Title + * Opening Passage Content + * [Link Text](Link Reference) + * + * ::Another Passage Title + * Passage Content + * [Link Text](Link Reference) + * {@code <Action Type>}\Action Value/ + * [Link Text](Link Reference) + * + * ... + * </pre> + * + * @param story The story to be written to the file. + * @param fileName The name of the file to be created. + * @throws IOException if an I/O error occurs with the writer, or if the file already exists. + */ + public void create(Story story, String fileName) throws IOException { + Objects.requireNonNull(story, "Story cannot be null"); + Objects.requireNonNull(fileName, "File name cannot be null"); + + File file = FileHandler.createFile(fileName); + + create(story, file); + } + + /** + * This method takes a story and writes its contents to a .paths file. The story information is transcribed + * in the given format: + * <pre> + * Story title + * + * ::Opening Passage Title + * Opening Passage Content + * [Link Text](Link Reference) + * + * ::Another Passage Title + * Passage Content + * [Link Text](Link Reference) + * {@code <Action Type>}\Action Value/ + * [Link Text](Link Reference) + * + * ... + * </pre> + * + * @param story The story to be written to the file. + * @param file The file the story is going to be written to. + * @throws IOException if an I/O error occurs with the writer, or if the file already exists. + */ + public void create(Story story, File file) throws IOException { + Objects.requireNonNull(story, "Story cannot be null"); + Objects.requireNonNull(file, "File cannot be null"); + + if (FileHandler.fileExists(file)) throw new FileAlreadyExistsException( + "You cannot overwrite a pre-existing story file" + ); + + /* propagate any errors while writing */ + writeStory(story, file); + } + + //TODO: add test for story files... + + /** + * Writes the story to the given file. + * @param story The story to be written to the file. + * @param file The file to be written to. + * @throws IOException if an I/O error occurs with the writer. + */ + private void writeStory(Story story, File file) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) { + writer.write(story.getTitle()); + writer.newLine(); + writer.newLine(); + + /* write all passages to the file */ + List<Passage> passages = new ArrayList<>(story.getPassages()); + for (Passage passage : passages) { + writePassage(passage, writer); + } + } + } + + /** + * Writes a passage to the file. + * @param passage The passage to be written to the file. + * @param writer The writer to be used. + * @throws IOException if an I/O error occurs with the writer. + */ + private void writePassage(Passage passage, BufferedWriter writer) throws IOException { + writer.write("::" + passage.getTitle()); + writer.newLine(); + writer.write(passage.getContent()); + writer.newLine(); + + for (Link link : passage.getLinks()) { + writeLink(link, writer); + } + writer.newLine(); + } + + /** + * Writes a link to the file. + * @param link The link to be written to the file. + * @param writer The writer to be used. + * @throws IOException if an I/O error occurs with the writer. + */ + private void writeLink(Link link, BufferedWriter writer) throws IOException { + writer.write("["); + writer.write(link.getText()); + writer.write("]("); + writer.write(link.getReference()); + writer.write(")"); + writer.newLine(); + + for (Action<?> action : link.getActions()) { + writeAction(action, writer); + } + } + + /** + * Writes an action to the file. + * @param action The action to be written to the file. + * @param writer The writer to be used. + * @throws IOException if an I/O error occurs with the writer. + */ + private void writeAction(Action<?> action, BufferedWriter writer) throws IOException { + writer.write("<"); + writer.write(action.getClass().getSimpleName()); + writer.write(">\\"); + writer.write(action.getActionValue().toString()); + writer.write("/"); + writer.newLine(); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/goals/Goal.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/Goal.java similarity index 63% rename from src/main/java/edu/ntnu/idatt2001/group_30/goals/Goal.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/Goal.java index 398acf28e6a2bd57a75812a5b3fc2bb2f84e7140..5ff7b63604e58b1a0322f43d48b24f494c71f404 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/goals/Goal.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/Goal.java @@ -1,6 +1,6 @@ -package edu.ntnu.idatt2001.group_30.goals; +package edu.ntnu.idatt2001.group_30.paths.model.goals; -import edu.ntnu.idatt2001.group_30.Player; +import edu.ntnu.idatt2001.group_30.paths.model.Player; /** * The functional interface Goal represents a threshold, desirable result, or condition connected to the player's @@ -8,9 +8,7 @@ import edu.ntnu.idatt2001.group_30.Player; * * @author Trym Hamer Gudvangen */ -@FunctionalInterface -public interface Goal { - +public interface Goal<T> { /** * This method checks that the player has fulfilled a certain condition of: * @param player The player to be checked, given as a Player object. @@ -18,4 +16,9 @@ public interface Goal { */ boolean isFulfilled(Player player); + /** + * This method retrieves the goal value of a given goal. + * @return Goal value, given as an Object. + */ + T getGoalValue(); } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalFactory.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..92cd32b22b19d5de62826f245e7b56bce350ef0d --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalFactory.java @@ -0,0 +1,61 @@ +package edu.ntnu.idatt2001.group_30.paths.model.goals; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * This class is a factory for creating Goal objects. + * + * @author Trym Hamer Gudvangen + */ +public class GoalFactory { + + /** + * This method creates a Goal object based on the given goal type and goal value. + * @param goalType The type of goal, given as a GoalType object. + * @param goalValue The value of the goal, given as an Object. + * @return A Goal object. + * @throws IllegalArgumentException If the goal type or value is invalid. + */ + public static Goal<?> getGoal(GoalType goalType, Object goalValue) throws IllegalArgumentException { + switch (goalType) { + case GOLD_GOAL, HEALTH_GOAL, SCORE_GOAL -> { + if (goalValue instanceof String) { + int intValue = Integer.parseInt((String) goalValue); + return switch (goalType) { + case GOLD_GOAL -> new GoldGoal(intValue); + case HEALTH_GOAL -> new HealthGoal(intValue); + case SCORE_GOAL -> new ScoreGoal(intValue); + default -> null; + }; + } + } + case INVENTORY_GOAL -> { + if (goalValue instanceof List<?> valueList) { + if (valueList.stream().allMatch(String.class::isInstance)) { + List<String> stringList = valueList + .stream() + .map(String.class::cast) + .collect(Collectors.toList()); + return new InventoryGoal(stringList); + } + } else if (goalValue instanceof String) { + return new InventoryGoal(Collections.singletonList((String) goalValue)); + } + } + } + throw new IllegalArgumentException("Invalid goal type or value"); + } + + /** + * This method creates a Goal object based on the given goal type and goal value. + * @param goalType The type of goal, given as a String. + * @param goalValue The value of the goal, given as an Object. + * @return A Goal object. + * @throws IllegalArgumentException If the goal type or value is invalid. + */ + public static Goal<?> getGoal(String goalType, Object goalValue) throws IllegalArgumentException { + return getGoal(GoalType.getGoalType(goalType), goalValue); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalType.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalType.java new file mode 100644 index 0000000000000000000000000000000000000000..0d369287e785abccae76c20a16577372f6afc9c8 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalType.java @@ -0,0 +1,50 @@ +package edu.ntnu.idatt2001.group_30.paths.model.goals; + +import java.util.HashMap; +import java.util.Map; + +/** + * This enum represents the different types of goals. + * + * @author Trym Hamer Gudvangen + */ +public enum GoalType { + GOLD_GOAL("GoldGoal"), + HEALTH_GOAL("HealthGoal"), + INVENTORY_GOAL("InventoryGoal"), + SCORE_GOAL("ScoreGoal"); + + private final String stringVal; + private static final Map<String, GoalType> stringToEnum = new HashMap<>(); + + static { + for (GoalType goalType : GoalType.values()) { + stringToEnum.put(goalType.getStringVal(), goalType); + } + } + + /** + * This constructor creates a GoalType object based on the given string value. + * @param stringVal The string value of the GoalType object. + */ + GoalType(String stringVal) { + this.stringVal = stringVal; + } + + /** + * This method retrieves the GoalType object based on the given string value. + * @param goalType The string value of the GoalType object. + * @return The GoalType object. + */ + public static GoalType getGoalType(String goalType) { + return stringToEnum.get(goalType); + } + + /** + * This method retrieves the string value of the GoalType object. + * @return The string value of the GoalType object. + */ + public String getStringVal() { + return stringVal; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/goals/GoldGoal.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoldGoal.java similarity index 53% rename from src/main/java/edu/ntnu/idatt2001/group_30/goals/GoldGoal.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoldGoal.java index 3616918658f44c03db242be9e14b42cf4ad6cd25..79ebf1121f6a17db6510b49bf46567444614298f 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/goals/GoldGoal.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoldGoal.java @@ -1,15 +1,14 @@ -package edu.ntnu.idatt2001.group_30.goals; - -import edu.ntnu.idatt2001.group_30.Player; +package edu.ntnu.idatt2001.group_30.paths.model.goals; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** * This class represents a minimum gold threshold. * - * @author Trym Hamer Gudvangen + * @author Trym Hamer Gudvangen, Nicolai H. Brand */ -public class GoldGoal implements Goal { +public class GoldGoal implements Goal<Integer> { private final int minimumGold; @@ -19,7 +18,6 @@ public class GoldGoal implements Goal { */ public GoldGoal(int minimumGold) { this.minimumGold = minimumGold; - //TODO: Add exception? } /** @@ -32,4 +30,22 @@ public class GoldGoal implements Goal { Objects.requireNonNull(player); return player.getGold() >= this.minimumGold; } + + /** + * This method retrieves the goal value. + * @return The minimum gold, given as an Integer. + */ + @Override + public Integer getGoalValue() { + return this.minimumGold; + } + + /** + * String representation of the GoldGoal object. + * @return String representation of the GoldGoal object. + */ + @Override + public String toString() { + return "Need to collect " + this.minimumGold + " gold."; + } } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/goals/HealthGoal.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/HealthGoal.java similarity index 59% rename from src/main/java/edu/ntnu/idatt2001/group_30/goals/HealthGoal.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/HealthGoal.java index d1b78d1335daa586143c321d7b0bcac0720083bb..0e35490364209f301cdf807f76f28a612202ab78 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/goals/HealthGoal.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/HealthGoal.java @@ -1,15 +1,14 @@ -package edu.ntnu.idatt2001.group_30.goals; - -import edu.ntnu.idatt2001.group_30.Player; +package edu.ntnu.idatt2001.group_30.paths.model.goals; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** * This class represents a minimum health threshold. * - * @author Trym Hamer Gudvangen + * @author Trym Hamer Gudvangen, Nicolai H. Brand. */ -public class HealthGoal implements Goal { +public class HealthGoal implements Goal<Integer> { private final int minimumHealth; @@ -18,7 +17,7 @@ public class HealthGoal implements Goal { * @param minimumHealth Minimum amount of health, given as an int. * @throws IllegalArgumentException This exception is thrown if minimum health less than 0. */ - public HealthGoal(int minimumHealth) throws IllegalArgumentException{ + public HealthGoal(int minimumHealth) throws IllegalArgumentException { if (minimumHealth < 0) throw new IllegalArgumentException("Minimum health cannot be less than 0"); this.minimumHealth = minimumHealth; } @@ -33,4 +32,22 @@ public class HealthGoal implements Goal { Objects.requireNonNull(player); return player.getHealth() >= this.minimumHealth; } + + /** + * This method retrieves the goal value. + * @return The minimum health goal, given as an Integer. + */ + @Override + public Integer getGoalValue() { + return this.minimumHealth; + } + + /** + * String representation of the HealthGoal object. + * @return String representation of the HealthGoal object. + */ + @Override + public String toString() { + return "Need to have at least " + this.minimumHealth + " health."; + } } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/InventoryGoal.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/InventoryGoal.java new file mode 100644 index 0000000000000000000000000000000000000000..1f3318b3e5845ece3357dde7f124cf7a2a413702 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/InventoryGoal.java @@ -0,0 +1,58 @@ +package edu.ntnu.idatt2001.group_30.paths.model.goals; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; + +/** + * This class represents the items that are expected in a player's inventory. + * + * @author Trym Hamer Gudvangen, Nicolai H. Brand. + */ +public class InventoryGoal implements Goal<List<String>> { + + private final List<String> mandatoryItems; + + /** + * The constructor defines the items a player must have. + * @param mandatoryItems Expected items, given as a List{@code <String>}. + */ + public InventoryGoal(List<String> mandatoryItems) throws IllegalArgumentException { + Objects.requireNonNull(mandatoryItems); + this.mandatoryItems = mandatoryItems; + } + + /** + * {@inheritDoc} inventory. + * @param player The player to be checked, given as a Player object. + * @return Status of player, {@code true} if the player has the items, else {@code false}. + */ + @Override + public boolean isFulfilled(Player player) { + Objects.requireNonNull(player); + return new HashSet<>(player.getInventory()).containsAll(mandatoryItems); + } + + public void concatGoals(InventoryGoal inventoryGoal) { + this.mandatoryItems.addAll(inventoryGoal.mandatoryItems); + } + + /** + * This method retrieves the goal value. + * @return Mandatory items, given as a List of String objects. + */ + @Override + public List<String> getGoalValue() { + return this.mandatoryItems; + } + + /** + * String representation of the InventoryGoal object. + * @return String representation of the InventoryGoal object. + */ + @Override + public String toString() { + return "Need to have the following items in your inventory: " + this.mandatoryItems; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/goals/ScoreGoal.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/ScoreGoal.java similarity index 53% rename from src/main/java/edu/ntnu/idatt2001/group_30/goals/ScoreGoal.java rename to src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/ScoreGoal.java index 09fb3ce08341472721781c8f434b3a8d21f94f8c..a0d688e1a83839e47d140a9cf9fdeba326abb315 100644 --- a/src/main/java/edu/ntnu/idatt2001/group_30/goals/ScoreGoal.java +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/goals/ScoreGoal.java @@ -1,14 +1,14 @@ -package edu.ntnu.idatt2001.group_30.goals; +package edu.ntnu.idatt2001.group_30.paths.model.goals; -import edu.ntnu.idatt2001.group_30.Player; +import edu.ntnu.idatt2001.group_30.paths.model.Player; import java.util.Objects; /** * This class represents a minimum goal threshold. * - * @author Trym Hamer Gudvangen + * @author Trym Hamer Gudvangen, Nicolai H. Brand. */ -public class ScoreGoal implements Goal { +public class ScoreGoal implements Goal<Integer> { private final int minimumPoints; @@ -18,7 +18,6 @@ public class ScoreGoal implements Goal { */ public ScoreGoal(int minimumPoints) { this.minimumPoints = minimumPoints; - //TODO: Add exception? } /** @@ -31,4 +30,22 @@ public class ScoreGoal implements Goal { Objects.requireNonNull(player); return player.getScore() >= this.minimumPoints; } + + /** + * This method retrieves the goal value. + * @return Minimum points, given as an Integer. + */ + @Override + public Integer getGoalValue() { + return this.minimumPoints; + } + + /** + * String representation of the ScoreGoal object. + * @return String representation of the ScoreGoal object. + */ + @Override + public String toString() { + return "Need to have at least " + this.minimumPoints + " points."; + } } diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/utils/TextValidation.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/utils/TextValidation.java new file mode 100644 index 0000000000000000000000000000000000000000..edfd723757903064ad2f91e31b1a8a0799b8ec70 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/model/utils/TextValidation.java @@ -0,0 +1,39 @@ +package edu.ntnu.idatt2001.group_30.paths.model.utils; + +import javafx.scene.control.TextFormatter; +import javafx.util.converter.IntegerStringConverter; + +/** + * This class represents a text validation. + * + * @author Trym Hamer Gudvangen + */ +public class TextValidation { + + /** + * This method creates a text formatter for integers. + * @return A text formatter for integers. + */ + public static TextFormatter<Integer> createIntegerTextFormatter() { + return createIntegerTextFormatter(100); + } + + /** + * This method creates a text formatter for integers. + * @param startValue The start value of the text formatter, given as an integer. + * @return A text formatter for integers. + */ + public static TextFormatter<Integer> createIntegerTextFormatter(int startValue) { + return new TextFormatter<>( + new IntegerStringConverter(), + startValue, + change -> { + String newText = change.getControlNewText(); + if (newText.matches("-?([1-9][0-9]*)?")) { + return change; + } + return null; + } + ); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/App.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/App.java new file mode 100644 index 0000000000000000000000000000000000000000..6f149869142bbc25642d016dd475b994cc9083d1 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/App.java @@ -0,0 +1,41 @@ +package edu.ntnu.idatt2001.group_30.paths.view; + +import edu.ntnu.idatt2001.group_30.paths.controller.StageManager; +import edu.ntnu.idatt2001.group_30.paths.view.views.HomeView; +import javafx.application.Application; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +/** + * The class App is the main class of the application Paths. + * It is responsible for creating the main stage and initializing the stage manager. + * + * @author Nicolai H. Brand + */ +public class App extends Application { + + private static StageManager STAGE_MANAGER; + + /** + * The entry point of the application. + * @param args The command line arguments. + */ + public static void main(String[] args) { + launch(); + } + + /** + * The start method is called by the JavaFX runtime after the init method has returned. + * Initializes the stage manager. + * @param stage The primary stage for this application, onto which the application scene can be set. + */ + @Override + public void start(Stage stage) { + stage.initStyle(StageStyle.UTILITY); + stage.setAlwaysOnTop(false); + stage.setTitle("Paths"); + /* initialize STAGE_MANAGER */ + STAGE_MANAGER = StageManager.init(stage); + STAGE_MANAGER.setCurrentView(new HomeView()); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/CreatePlayer.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/CreatePlayer.java new file mode 100644 index 0000000000000000000000000000000000000000..004702c843a858ab8e3fa8116170b0569e8ce169 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/CreatePlayer.java @@ -0,0 +1,75 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +/** + * This class represents a component for creating the player, containing the fields for the player's name, + * health, and gold, as well as a dropdown box for selecting the player's goal. + * + * @author Trym Hamer Gudvangen + */ +public class CreatePlayer extends GridPane { + + private final TextField nameField; + private final TextField healthField; + private final TextField goldField; + private final ComboBox<String> goalBox; + + /** + * Constructor for the CreatePlayer component. + */ + public CreatePlayer() { + setHgap(10); + setVgap(5); + + nameField = new TextField(); + healthField = new TextField(); + goldField = new TextField(); + goalBox = new ComboBox<>(); + goalBox.getItems().addAll("GoldGoal", "HealthGoal", "InventoryGoal", "ScoreGoal"); + + add(new Label("Name:"), 0, 0); + add(nameField, 1, 0); + add(new Label("Health:"), 0, 1); + add(healthField, 1, 1); + add(new Label("Gold:"), 0, 2); + add(goldField, 1, 2); + add(new Label("Goal:"), 0, 3); + add(goalBox, 1, 3); + } + + /** + * Method for getting the name of the player. + * @return The name of the player, as a String. + */ + public String getName() { + return nameField.getText(); + } + + /** + * Method for getting the health of the player. + * @return The health of the player, as an int. + */ + public int getHealth() { + return Integer.parseInt(healthField.getText()); + } + + /** + * Method for getting the gold of the player. + * @return The gold of the player, as an int. + */ + public int getGold() { + return Integer.parseInt(goldField.getText()); + } + + /** + * Method for getting the goal of the player. + * @return The goal of the player, as a String. + */ + public String getGoal() { + return goalBox.getValue(); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/ImageCarousel.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/ImageCarousel.java new file mode 100644 index 0000000000000000000000000000000000000000..a95573f21a490516385765e79f952d6f1154b2f9 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/ImageCarousel.java @@ -0,0 +1,98 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components; + +import java.net.URL; +import java.util.LinkedList; +import java.util.List; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * This class represents a component for displaying a carousel of images. + * + * @author Trym Hamer Gudvangen + */ +public class ImageCarousel { + + private final LinkedList<Image> images = new LinkedList<>(); + private int currentIndex; + private ImageView currentImage = new ImageView(); + private int size; + private final int WIDTH = 150; + private final int HEIGHT = 150; + + /** + * Constructor for the ImageCarousel component. + * @param imageNames A list of image names, as Strings. + */ + public ImageCarousel(List<String> imageNames) { + if (imageNames == null || imageNames.isEmpty()) { + throw new IllegalArgumentException("Image URI list must not be empty."); + } + + for (String imageURI : imageNames) { + URL imageUrl = getClass().getResource(imageURI); + if (imageUrl != null) { + images.add(new Image(imageUrl.toString())); + } else { + System.err.println("Unable to load image: " + imageURI); + } + } + + this.currentIndex = 0; + this.size = images.size(); + this.currentImage.setFitWidth(WIDTH); + this.currentImage.setFitHeight(HEIGHT); + this.currentImage.setImage(images.getFirst()); + } + + /** + * Method for getting the carousel component. + * @return The carousel component, as an HBox. + */ + public HBox getCarousel() { + Button leftButton = new Button("<"); + leftButton.setOnAction(e -> previous()); + + Button rightButton = new Button(">"); + rightButton.setOnAction(e -> next()); + + HBox carousel = new HBox(leftButton, currentImage, rightButton); + carousel.setAlignment(Pos.CENTER); + return carousel; + } + + /** + * Method for getting the next image. + */ + public void next() { + currentIndex = (currentIndex + 1) % size; + currentImage.setImage(images.get(currentIndex)); + } + + /** + * Method for getting the previous image. + */ + public void previous() { + currentIndex = (currentIndex - 1 + size) % size; + currentImage.setImage(images.get(currentIndex)); + } + + /** + * Method for getting the size of the list of images. + * @return The size of the list of images, as an int. + */ + public int size() { + return size; + } + + /** + * Method for getting the current image. + * @return The current image, as an ImageView. + */ + public ImageView getCurrentImage() { + return currentImage; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/StoryDisplay.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/StoryDisplay.java new file mode 100644 index 0000000000000000000000000000000000000000..0ef5e184defa013b60cb6b9db158008c44b60088 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/StoryDisplay.java @@ -0,0 +1,152 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components; + +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.filehandling.FileHandler; +import edu.ntnu.idatt2001.group_30.paths.model.filehandling.StoryFileReader; +import edu.ntnu.idatt2001.group_30.paths.view.components.pane.PaneSpacing; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.List; +import javafx.scene.layout.*; +import javafx.scene.text.Text; + +/** + * This class represents a depiction of a story. This story can come in both the form of a Story file as well as just a + * story object. From there, a builder, {@link Builder}, allows the user to choose what information concerning the story + * file and/or story. The file and the story's information will be instantiated as text and displayed in the GUI. + * Additionally, this class extends the {@link VBox} class, which means an object of StoryDisplay can be treated the + * same as a VBox. + * + * @author Trym Hamer Gudvangen + */ +public class StoryDisplay extends VBox { + + /** + * This constructor represents a display filled with a given story's information. It utilizes a builder object, + * {@link Builder#Builder(Story)} taken as a parameter, in order to attain all the nodes which need to be attached + * to the StoryDisplay object. + */ + public StoryDisplay(Builder builder) { + super(PaneSpacing.createVBoxWithSpacing(builder.hBoxList)); + this.autosize(); + } + + /** + * This static class employs the builder design pattern. It allows for a person to construct a display by adding + * on the desired parts of information. It, therefore, includes multiple methods for adding story information. + */ + public static class Builder { + + private String storyFileName; + + private String location; + + private Story story; + + private final List<HBox> hBoxList; + + /** + * This is the constructor for the builder class which takes in a file as input. With this constructor, + * information not only surrounding the story in the file but the file itself can be extracted. This information + * is set during the initialization phase using the method {@link #setStoryInformation()}. + * @param fileName The file name containing the story, represented as String. + * @throws IOException This exception is thrown if the file stated is invalid. + * @throws InstantiationException This exception is thrown if the file being read is + */ + public Builder(String fileName) throws IOException, InstantiationException { + this.storyFileName = fileName; + this.hBoxList = new ArrayList<>(); + setStoryInformation(); + } + + /** + * This is the constructor for the builder class which takes in an Story object as input. With this constructor, + * information only information surrounding the given story can be used. + * @param story The story whose information will be displayed, represented using an Story object. + */ + public Builder(Story story) { + this.story = story; + this.hBoxList = new ArrayList<>(); + } + + /** + * This method adds an HBox containing the story name to the hBoxList. + * @return The builder itself, represented as a Builder object. + */ + public Builder addStoryName() { + Text storyName = new Text(story.getTitle()); + storyName.setUnderline(true); + this.hBoxList.add(PaneSpacing.createHBoxWithSpacing(storyName)); + return this; + } + + public Builder addFileInfo(File file) throws IOException { + this.storyFileName = file.getName().replace(".paths", ""); + this.location = file.getAbsolutePath(); + addFileNameInfo(); + addTimeSavedInfo(); + addFileLocationInfo(); + return this; + } + + /** + * This method creates the file name information of the story file. + * @return A horizontal layout with the file name information, represented using an HBox object + */ + public Builder addFileNameInfo() { + if (storyFileName == null) return this; + Text description = new Text("File Name: "); + Text fileName = new Text(this.storyFileName); + this.hBoxList.add(PaneSpacing.createHBoxWithSpacing(description, fileName)); + return this; + } + + /** + * This method creates the time saved information of the story file. + * @return A horizontal layout with the file save information, represented using an HBox object + */ + public Builder addTimeSavedInfo() throws IOException { + if (storyFileName == null) return this; + Text savedText = new Text("Saved: "); + FileTime fileTime = Files.getLastModifiedTime(Path.of(FileHandler.getFileSourcePath(this.storyFileName))); + Text timeSaved = new Text(fileTime.toString().substring(0, 10)); + this.hBoxList.add(PaneSpacing.createHBoxWithSpacing(savedText, timeSaved)); + return this; + } + + /** + * This method creates the file location information of the story file. + * @return A horizontal layout with the file location information,represented using an HBox object + */ + public Builder addFileLocationInfo() { + if (storyFileName == null) return this; + Text locationText = new Text("Location: "); + Text pathSaved = new Text(this.location); + pathSaved.setWrappingWidth(200); + this.hBoxList.add(PaneSpacing.createHBoxWithSpacing(locationText, pathSaved)); + return this; + } + + /** + * This method retrieves the location and story information from the story file. + * @throws IOException This exception is thrown if the file read is invalid. + */ + private void setStoryInformation() throws IOException, InstantiationException { + this.location = String.valueOf(FileHandler.createFile(this.storyFileName).toPath()); + this.story = new StoryFileReader().parse(this.storyFileName); + } + + /** + * This method finalizes all the information that has been added and creates a new StoryDisplay object, + * {@link StoryDisplay#StoryDisplay(Builder)}. + * @return A story display with all the information added, represented as a VBox. + */ + public VBox build() { + return new StoryDisplay(this); + } + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultButton.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultButton.java new file mode 100644 index 0000000000000000000000000000000000000000..33b5f5e7d73b3a55b6b5ae9739ece003cafeba8d --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultButton.java @@ -0,0 +1,90 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.common; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.control.Button; + +/** + * This class is used to create buttons with default settings. + * Used for cohesion between the different buttons in the application. + * + * @author Nicolai H. Brand. + * + */ +public class DefaultButton { + + /** + * This method is used to create a big button with project-wide default settings. + * @param textString The text to be displayed on the button. + * @return A JavaFX button. + */ + public static Button big(String textString) { + Button button = new Button(textString); + button.setAlignment(Pos.CENTER); + button.setMinWidth(175); + button.setMinHeight(75); + button.setFont(DefaultFont.medium()); + return button; + } + + /** + * This method is used to create a big button with project-wide default settings. + * @param textString The text to be displayed on the button. + * @param action The action to be performed when the button is clicked. + * @return A JavaFX button. + */ + public static Button big(String textString, EventHandler<ActionEvent> action) { + Button button = big(textString); + button.setOnAction(action); + return button; + } + + /** + * This method is used to create a medium button with project-wide default settings. + * @param textString The text to be displayed on the button. + * @return A JavaFX button. + */ + public static Button medium(String textString) { + Button button = new Button(textString); + button.setAlignment(Pos.CENTER); + button.setFont(DefaultFont.small()); + return button; + } + + /** + * This method is used to create a medium button with project-wide default settings. + * @param textString The text to be displayed on the button. + * @param action The action to be performed when the button is clicked. + * @return A JavaFX button. + */ + public static Button medium(String textString, EventHandler<ActionEvent> action) { + Button button = medium(textString); + button.setOnAction(action); + return button; + } + + /** + * This method is used to create a small button with project-wide default settings. + * @param textString The text to be displayed on the button. + * @return A JavaFX button. + */ + public static Button small(String textString) { + Button button = new Button(textString); + button.setAlignment(Pos.CENTER); + button.setFont(DefaultFont.small()); + return button; + } + + /** + * This method is used to create a small button with project-wide default settings. + * @param textString The text to be displayed on the button. + * @param action The action to be performed when the button is clicked. + * @return A JavaFX button. + */ + public static Button small(String textString, EventHandler<ActionEvent> action) { + Button button = small(textString); + button.setOnAction(action); + return button; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultFont.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultFont.java new file mode 100644 index 0000000000000000000000000000000000000000..1e3e66040fde016a1beebb8c292531f91db14331 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultFont.java @@ -0,0 +1,41 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.common; + +import javafx.scene.text.Font; + +/** + * This class contains the default font used in the application. + * Used for cohesion between the different texts in the application. + * + * @author Nicolai H. Brand. + */ +public class DefaultFont { + + /* + * The default font used in the application. + */ + public static final String DEFAULT_FONT = "Arial"; + + /** + * This method is used to create a big font. + * @return A JavaFX font. + */ + public static Font big() { + return new Font(DEFAULT_FONT, 42); + } + + /** + * This method is used to create a medium font. + * @return A JavaFX font. + */ + public static Font medium() { + return new Font(DEFAULT_FONT, 26); + } + + /** + * This method is used to create a small font. + * @return A JavaFX font. + */ + public static Font small() { + return new Font(DEFAULT_FONT, 16); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultInputField.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultInputField.java new file mode 100644 index 0000000000000000000000000000000000000000..a030a4bd51bd011df7e4c4f6ed4768859013e228 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultInputField.java @@ -0,0 +1,26 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.common; + +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.text.Text; + +/** + * This class contains methods to create different input fields with default layouts. + * + * @author Trym Hamer Gudvangen + */ +public class DefaultInputField { + + /** + * This method creates a text input field with a given label and prompt. + * @param label The label of the input field, given as a String. + * @param prompt The prompt of the input field, given as a String. + * @return An HBox containing the label and the input field. + */ + public static HBox inputWithLabelAndPrompt(String label, String prompt) { + Text labelText = new Text(label); + TextField textField = new TextField(); + textField.setPromptText(prompt); + return new HBox(labelText, textField); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultText.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultText.java new file mode 100644 index 0000000000000000000000000000000000000000..34b6003b4932694765969c408cbd0c4439ae8349 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/DefaultText.java @@ -0,0 +1,45 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.common; + +import javafx.scene.text.Text; + +/** + * This class contains the default text used in the application. + * Used for cohesion between the different texts in the application. + * + * @author Nicolai H. Brand. + */ +public class DefaultText { + + /** + * This method is used to create a small text using project-wide default settings. + * @param textString The text to be displayed. + * @return A JavaFX text object. + */ + public static Text big(String textString) { + Text text = new Text(textString); + text.setFont(DefaultFont.big()); + return text; + } + + /** + * This method is used to create a medium text using project-wide default settings. + * @param textString The text to be displayed. + * @return A JavaFX text object. + */ + public static Text medium(String textString) { + Text text = new Text(textString); + text.setFont(DefaultFont.medium()); + return text; + } + + /** + * This method is used to create a small text using project-wide default settings. + * @param textString The text to be displayed. + * @return A JavaFX text object. + */ + public static Text small(String textString) { + Text text = new Text(textString); + text.setFont(DefaultFont.small()); + return text; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/Ref.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/Ref.java new file mode 100644 index 0000000000000000000000000000000000000000..5d996e2626f42af6d4bb4d1e5fb7ab48f7e3384a --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/common/Ref.java @@ -0,0 +1,63 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.common; + +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TextArea; +import javafx.scene.text.Text; + +/** + * This class is used to create Text objects that are bound to an observable property. + * This means that the text will update when the property changes. + * This is used to create a dynamic UI. + * + * @author Nicolai H. Brand. + */ +public class Ref { + + /** + * Creates a Text object that is bound to the given observable property. + * @param ref The observable property to bind the text to. + * @return A Text object that is bound to the given observable property. + */ + public static Text bigText(ObservableValue<? extends String> ref) { + Text text = new Text(); + text.textProperty().bind(ref); + text.setFont(DefaultFont.big()); + return text; + } + + /** + * Creates a Text object that is bound to the given observable property. + * @param ref The observable property to bind the text to. + * @return A Text object that is bound to the given observable property. + */ + public static Text mediumText(ObservableValue<? extends String> ref) { + Text text = new Text(); + text.textProperty().bind(ref); + text.setFont(DefaultFont.medium()); + return text; + } + + /** + * Creates a Text object that is bound to the given observable property. + * @param ref The observable property to bind the text to. + * @return A Text object that is bound to the given observable property. + */ + public static Text smallText(ObservableValue<? extends String> ref) { + Text text = new Text(); + text.textProperty().bind(ref); + text.setFont(DefaultFont.small()); + return text; + } + + /** + * Creates a TextArea object that is bound to the given observable property. + * @param ref The observable property to bind the text to. + * @return A TextArea object that is bound to the given observable property. + */ + public static TextArea smallTextArea(ObservableValue<? extends String> ref) { + TextArea text = new TextArea(); + text.textProperty().bind(ref); + text.setFont(DefaultFont.small()); + return text; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pane/PaneSpacing.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pane/PaneSpacing.java new file mode 100644 index 0000000000000000000000000000000000000000..7998ef2a993525a98177161ddcff04428bfdbeb8 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pane/PaneSpacing.java @@ -0,0 +1,104 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pane; + +import java.util.Arrays; +import java.util.List; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * This class provides utility for spacing layouts using panes. This is done by placing panes between each node + * provided. + * + * @author Trym Hamer Gudvangen + */ +public class PaneSpacing { + + /** + * This method takes in different nodes and creates an HBox with equal spaces between each of the nodes. The + * spaces are created using panes. + * @param nodes Any type of node from JavaFX such as Text + * @return An HBox with spacing between the nodes + */ + public static HBox createHBoxWithSpacing(Node... nodes) { + return (HBox) createPaneWithSpacing(new HBox(), nodes); + } + + /** + * This method takes in different nodes and creates a VBox with equal spaces between each of the nodes. The + * spaces are created using panes. + * @param nodes Any type of node from JavaFX such as Text + * @return A VBox with spacing between the nodes + */ + public static VBox createVBoxWithSpacing(Node... nodes) { + return (VBox) createPaneWithSpacing(new VBox(), nodes); + } + + /** + * This method takes in a list of nodes and creates a VBox with equal spaces between each of the nodes. The + * spaces are created using panes. + * @param nodeList A list of nodes from JavaFX such as Text + * @return A VBox with spacing between the nodes + */ + public static VBox createVBoxWithSpacing(List<HBox> nodeList) { + return (VBox) createPaneWithSpacing(new VBox(), nodeList); + } + + /** + * This method takes advantage of the fact that other layout nodes such as VBox and HBox inherit from the Pane class. + * Therefore, it creates a general pane spacing method. + * @param typeOfPane The type of pane, for example HBox, represented using a Pane object + * @param nodes Any type of node from JavaFX such as Text + * @return A pane that has spacing between the given nodes + */ + private static Pane createPaneWithSpacing(Pane typeOfPane, Node... nodes) { + Pane initialPaneSpacing = new Pane(); + typeOfPane.getChildren().add(initialPaneSpacing); + addPaneProperties(initialPaneSpacing, typeOfPane); + + Arrays + .stream(nodes) + .forEach(node -> { + Pane spacingPane = new Pane(); + addPaneProperties(spacingPane, typeOfPane); + typeOfPane.getChildren().addAll(node, spacingPane); + }); + return typeOfPane; + } + + /** + * This method takes advantage of the fact that other layout nodes such as VBox and HBox inherit from the Pane class. + * Therefore, it creates a pane spacing method. + * @param typeOfPane The type of pane, for example HBox, represented using a Pane object + * @param nodeList A list of nodes from JavaFX such as Text + * @return A pane that has spacing between the given nodes + */ + private static Pane createPaneWithSpacing(Pane typeOfPane, List<? extends Pane> nodeList) { + Pane initialPaneSpacing = new Pane(); + typeOfPane.getChildren().add(initialPaneSpacing); + addPaneProperties(initialPaneSpacing, typeOfPane); + + nodeList.forEach(node -> { + Pane spacingPane = new Pane(); + addPaneProperties(spacingPane, typeOfPane); + typeOfPane.getChildren().addAll(node, spacingPane); + }); + return typeOfPane; + } + + /** + * This method takes in a pane to receive a property and then finds out which typeOfPane it will be attached to. + * It, then, attaches desired properties to those panes. + * @param nodeToGetProperty The node that will get a property, represented as a Node object. + * @param typeOfPane The type of pane it will be attached to, represented as a Pane object. + */ + private static void addPaneProperties(Node nodeToGetProperty, Pane typeOfPane) { + if (typeOfPane instanceof HBox) HBox.setHgrow(nodeToGetProperty, Priority.ALWAYS); else if ( + typeOfPane instanceof VBox + ) VBox.setVgrow(nodeToGetProperty, Priority.ALWAYS); + typeOfPane.setPadding(new Insets(20)); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/AbstractPopUp.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/AbstractPopUp.java new file mode 100644 index 0000000000000000000000000000000000000000..35fa07f2d7b7444bbcb12bba3eb96c8004d17b1e --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/AbstractPopUp.java @@ -0,0 +1,32 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +/** + * This class provides a template for creating pop-ups. + * + * @author Trym Hamer Gudvangen + */ +public abstract class AbstractPopUp { + + /** + * This method initializes the pop-up by setting up the UI components and the behavior. + */ + protected void initialize() { + setupUiComponents(); + setupBehavior(); + } + + /** + * This method sets up the UI components of the pop-up. + */ + protected abstract void setupUiComponents(); + + /** + * This method sets up the behavior of the pop-up. + */ + protected abstract void setupBehavior(); + + /** + * This method creates the pop-up. + */ + protected abstract void createPopUp(); +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/AlertDialog.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/AlertDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..4b2cb2869d02d9131e5ea1b51f9834a69bb2858d --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/AlertDialog.java @@ -0,0 +1,103 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +import edu.ntnu.idatt2001.group_30.paths.exceptions.InvalidButtonException; +import java.util.Optional; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.TextInputDialog; + +/** + * This class contains the features essential for an alert dialog. Therefore, it provides different functions for the + * various types of alert. + * + * @author Trym Hamer Gudvangen + */ +public class AlertDialog { + + /** + * This method produces a confirmation dialog by taking in the information to be displayed and returning the status + * of whether the alert was confirmed or not. In order to create the dialog, the method + * {@link #createAlert(Alert.AlertType, String, String)} was used. + * @param message The message the user will read, represented as a String + * @param header The header, or brief information on the alert, represented as a String + * @return The status of the dialog where cancel is {@code false} and confirm/ok is {@code true}. + */ + public static boolean showConfirmation(String message, String header) { + Optional<ButtonType> buttonPressed = createAlert(Alert.AlertType.CONFIRMATION, message, header); + + return buttonPressed.orElseThrow(InvalidButtonException::new) != ButtonType.CANCEL; + } + + /** + * This method produces an error dialog with the input message displayed. In order to create the dialog, the method + * {@link #createAlert(Alert.AlertType, String, String)} was used. + * @param message The error message the user will read, represented as a String + */ + public static void showError(String message) { + createAlert(Alert.AlertType.ERROR, message, "An error has occurred!"); + } + + /** + * This method produces a warning dialog with the input message displayed. In order to create the dialog, the method + * {@link #createAlert(Alert.AlertType, String, String)} was used. + * @param message The warning message the user will read, represented as a String + */ + public static void showWarning(String message) { + createAlert(Alert.AlertType.WARNING, message, "Warning!"); + } + + /** + * This method produces an information dialog with the input message displayed. In order to create the dialog, the + * method {@link #createAlert(Alert.AlertType, String, String)} was used. + * @param message The information the user will read, represented as a String + * @param header The header, or brief information on the dialog, represented as a String. + */ + public static void showInformation(String message, String header) { + createAlert(Alert.AlertType.INFORMATION, message, header); + } + + /** + * This method produces a text input dialog, where the user has the ability to enter a String input. In order to + * create the dialog, the method {@link #setDialogInformation(Dialog, String, String, String)} was used. + * @param title The title of the dialog, represented as a String. + * @param header The header, or brief information on the dialog, represented as a String. + * @param content The prompt or message to the user, represented as a String + * @return The input from the user, represented as a String. If nothing was entered, then null is returned. + */ + public static String createTextInputDialog(String title, String header, String content) { + TextInputDialog inputDialog = new TextInputDialog(); + setDialogInformation(inputDialog, title, header, content); + Optional<String> inputText = inputDialog.showAndWait(); + return inputText.orElse(null); + } + + /** + * This method acts as a general outline for the creation of alert boxes. It takes in what type of alert is desired, + * as well as the information to appear on the alert, and then creates it. The information is attached to the + * dialog using the {@link #setDialogInformation(Dialog, String, String, String)} method. + * @param alertType The type of alert, represented using the enum Alert.AlertType. + * @param message The warning message the user will read, represented as a String + * @param headerText The header, or brief information on the dialog, represented as a String. + * @return A potential result from a button, represented using a Optional{@code <ButtonType>} object. + */ + private static Optional<ButtonType> createAlert(Alert.AlertType alertType, String message, String headerText) { + Alert alert = new Alert(alertType); + setDialogInformation(alert, alertType.name(), headerText, message); + + return alert.showAndWait(); + } + + /** + * This method attaches the information to a given dialog. + * @param dialog The dialog the information will be attached to, represented using a Dialog object. + * @param title The title of the dialog, represented as a String. + * @param header The header, or brief information on the dialog, represented as a String. + * @param content The prompt or message to the user, represented as a String. + */ + private static void setDialogInformation(Dialog<?> dialog, String title, String header, String content) { + dialog.setTitle(title); + dialog.setHeaderText(header); + dialog.setContentText(content); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/GoalsPopUp.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/GoalsPopUp.java new file mode 100644 index 0000000000000000000000000000000000000000..71e19b86a45896a37c987674488faaadb51b28ef --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/GoalsPopUp.java @@ -0,0 +1,184 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.model.goals.GoalFactory; +import edu.ntnu.idatt2001.group_30.paths.model.goals.GoalType; +import edu.ntnu.idatt2001.group_30.paths.model.utils.TextValidation; +import java.net.URL; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +/** + * This class contains a pop-up for creating and editing goals. + * + * @author Trym Hamer Gudvangen + */ +public class GoalsPopUp extends AbstractPopUp { + + private TextField healthField; + private TextField goldField; + private TextField scoreField; + private Button saveButton; + private Button addButton; + private Button deleteButton; + private TextField inventoryField; + private ObservableList<String> items; + private TableView<String> inventoryTable; + private VBox content; + private ScrollPane scrollPane; + private PopUp<ScrollPane, ?> popUp; + + /** + * This constructor creates a new GoalsPopUp. + */ + public GoalsPopUp() { + initialize(); + createPopUp(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void setupUiComponents() { + healthField = new TextField(); + healthField.setTextFormatter( + TextValidation.createIntegerTextFormatter( + INSTANCE.getHealthGoal() == null ? 100 : INSTANCE.getHealthGoal().getGoalValue() + ) + ); + healthField.setPromptText("Add health goal"); + + goldField = new TextField(); + goldField.setTextFormatter( + TextValidation.createIntegerTextFormatter( + INSTANCE.getGoldGoal() == null ? 100 : INSTANCE.getGoldGoal().getGoalValue() + ) + ); + goldField.setPromptText("Add gold goal"); + + scoreField = new TextField(); + scoreField.setTextFormatter( + TextValidation.createIntegerTextFormatter( + INSTANCE.getScoreGoal() == null ? 100 : INSTANCE.getScoreGoal().getGoalValue() + ) + ); + scoreField.setPromptText("Add score goal"); + + saveButton = new Button("Save"); + + inventoryField = new TextField(); + inventoryField.setPromptText("Add inventory item"); + + addButton = new Button(); + URL imageUrl = getClass().getResource("/images/plus.png"); + if (imageUrl != null) { + ImageView addIcon = new ImageView(new Image(imageUrl.toString())); + addIcon.setFitHeight(25); + addIcon.setFitWidth(25); + addButton.setGraphic(addIcon); + } else { + System.err.println("Something is wrong with the trash image resource link"); + } + + items = FXCollections.observableArrayList(); + if (INSTANCE.getInventoryGoal() != null) { + items.addAll(INSTANCE.getInventoryGoal().getGoalValue()); + } + inventoryTable = new TableView<>(items); + TableColumn<String, String> itemColumn = new TableColumn<>("Items"); + itemColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue())); + inventoryTable.getColumns().add(itemColumn); + itemColumn.prefWidthProperty().bind(inventoryTable.widthProperty()); + inventoryTable.setMaxHeight(200); + + deleteButton = new Button(); + imageUrl = getClass().getResource("/images/trash.png"); + if (imageUrl != null) { + ImageView trashIcon = new ImageView(new Image(imageUrl.toString())); + trashIcon.setFitHeight(25); + trashIcon.setFitWidth(25); + deleteButton.setGraphic(trashIcon); + } else { + System.err.println("Something is wrong with the trash image resource link"); + } + + content = + new VBox( + new Label("Health:"), + healthField, + new Label("Gold:"), + goldField, + new Label("Score:"), + scoreField, + new Label("Inventory"), + new HBox(inventoryField, addButton), + inventoryTable, + deleteButton, + saveButton + ); + + content.setAlignment(Pos.CENTER); + content.setSpacing(20); + + scrollPane = new ScrollPane(content); + scrollPane.setFitToWidth(true); + } + + /** + * {@inheritDoc} + */ + @Override + protected void setupBehavior() { + addButton.setOnAction(e -> { + if (!inventoryField.getText().isBlank()) { + items.add(inventoryField.getText()); + inventoryField.clear(); + } + }); + + deleteButton.setOnAction(e -> { + String selectedItem = inventoryTable.getSelectionModel().getSelectedItem(); + if (selectedItem != null) { + items.remove(selectedItem); + } + }); + + saveButton.setOnAction(e -> { + if (healthField.getText().isBlank() || goldField.getText().isBlank()) { + AlertDialog.showWarning("The different fields cannot be blank."); + } else { + INSTANCE.changeGoal(GoalFactory.getGoal(GoalType.HEALTH_GOAL, healthField.getText())); + INSTANCE.changeGoal(GoalFactory.getGoal(GoalType.GOLD_GOAL, goldField.getText())); + INSTANCE.changeGoal(GoalFactory.getGoal(GoalType.SCORE_GOAL, scoreField.getText())); + INSTANCE.changeGoal(GoalFactory.getGoal(GoalType.INVENTORY_GOAL, items)); + + popUp.close(); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + protected void createPopUp() { + popUp = + PopUp + .<ScrollPane>create() + .withTitle("Add goals to your player") + .withoutCloseButton() + .withContent(scrollPane) + .withDialogSize(400, 750); + + popUp.showAndWait(); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/LinkPopUp.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/LinkPopUp.java new file mode 100644 index 0000000000000000000000000000000000000000..54bd0ee6d0f05d650d73ff237281e3cbbda0e58d --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/LinkPopUp.java @@ -0,0 +1,213 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.actions.Action; +import edu.ntnu.idatt2001.group_30.paths.model.actions.ActionFactory; +import edu.ntnu.idatt2001.group_30.paths.model.actions.ActionType; +import edu.ntnu.idatt2001.group_30.paths.model.utils.TextValidation; +import edu.ntnu.idatt2001.group_30.paths.view.components.table.ActionTable; +import edu.ntnu.idatt2001.group_30.paths.view.components.table.TableDisplay; +import java.util.HashMap; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +/** + * This class is a pop-up for creating a new link or editing an existing one. + * + * @author Trym Hamer Gudvangen + */ +public class LinkPopUp extends AbstractPopUp { + + private TextField textField; + private TextField actionTextField; + private Button saveButton; + private Button addActionButton; + private Button removeActionButton; + private ComboBox<String> reference; + private ComboBox<ActionType> actionComboBox; + private ActionTable<Action<?>> actionTable; + private VBox content; + private final ObservableList<Passage> passages; + private ObservableList<Action<?>> actions; + private HashMap<String, Passage> passageHashMap; + private Link link; + private PopUp<VBox, ?> popUp; + + /** + * This constructor allows a new link to be created. + * @param passages Other passages in the story, given as an ObservableList of passages. + */ + public LinkPopUp(ObservableList<Passage> passages) { + this.actions = FXCollections.observableArrayList(); + this.passages = passages; + this.passageHashMap = new HashMap<>(); + passages.forEach(passage -> passageHashMap.put(passage.getTitle(), passage)); + + initialize(); + createPopUp(); + } + + /** + * This constructor allows a pre-existing link to be edited. + * @param passages Other passages in the story, given as an ObservableList of passages. + * @param link The link to be edited, given as a Link object. + */ + public LinkPopUp(ObservableList<Passage> passages, Link link) { + this.link = link; + this.actions = FXCollections.observableArrayList(link.getActions()); + this.passages = passages; + this.passageHashMap = new HashMap<>(); + passages.forEach(passage -> passageHashMap.put(passage.getTitle(), passage)); + + initialize(); + loadLink(); + createPopUp(); + } + + /** + * This method loads the link to be edited into the pop-up. + */ + @Override + protected void setupUiComponents() { + textField = new TextField(); + textField.setPromptText("Enter the text of the link"); + + reference = new ComboBox<>(FXCollections.observableArrayList(passageHashMap.keySet())); + reference.setPromptText("Select reference passage of the link"); + + saveButton = new Button("Save"); + + ObservableList<ActionType> actionTypes = FXCollections.observableArrayList(ActionType.values()); + + actionComboBox = new ComboBox<>(actionTypes); + actionComboBox.setPromptText("Select an action"); + + actionTextField = new TextField(); + + removeActionButton = new Button("Remove Action"); + removeActionButton.setDisable(true); + + addActionButton = new Button("Add Action"); + + HBox actionHbox = new HBox(actionComboBox, actionTextField, addActionButton); + actionHbox.setAlignment(Pos.CENTER); + + actionTable = + new ActionTable<>( + new TableDisplay.Builder<Action<?>>() + .addColumnWithComplexValue("Type", action -> action.getClass().getSimpleName()) + .addColumnWithComplexValue("Value", action -> action.getActionValue().toString()) + ); + + actionTable.setItems(actions); + + actionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + content = + new VBox( + new Label("Link Text:"), + textField, + new Label("Link Reference:"), + reference, + new Label("Actions:"), + actionHbox, + actionTable, + removeActionButton, + saveButton + ); + + content.setAlignment(Pos.CENTER); + content.setSpacing(20); + } + + /** + * This method sets up the behavior of the Ui components. + */ + @Override + protected void setupBehavior() { + removeActionButton.setOnAction(event -> actions.remove(actionTable.getSelectionModel().getSelectedItem())); + + actionTable + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldSelection, newSelection) -> { + removeActionButton.setDisable(newSelection == null); + }); + + addActionButton.setOnAction(e -> { + if (actionComboBox.getValue() != null) { + actions.add(ActionFactory.getAction(actionComboBox.getValue(), actionTextField.getText())); + actionComboBox.setValue(null); + } + }); + + saveButton.setOnAction(e -> { + if (textField.getText().isBlank() || reference.getValue() == null) { + AlertDialog.showWarning("The text or reference cannot be blank."); + } else { + link = new Link(textField.getText(), passageHashMap.get(reference.getValue()).getTitle()); + actions.forEach(action -> link.addAction(action)); + popUp.close(); + } + }); + + actionComboBox.setCellFactory(listView -> + new ListCell<>() { + @Override + protected void updateItem(ActionType actionType, boolean empty) { + super.updateItem(actionType, empty); + if (empty || actionType == null) { + setText(null); + } else { + setText(actionType.getDisplayName()); + switch (actionType) { + case SCORE_ACTION, GOLD_ACTION, HEALTH_ACTION -> actionTextField.setTextFormatter( + TextValidation.createIntegerTextFormatter() + ); + case INVENTORY_ACTION -> actionTextField.setTextFormatter(null); + } + } + } + } + ); + + actionComboBox.setButtonCell(actionComboBox.getCellFactory().call(null)); + } + + /** + * This method creates the pop-up. + */ + @Override + protected void createPopUp() { + popUp = + PopUp + .<VBox>create() + .withTitle("Create a Link") + .withoutCloseButton() + .withContent(content) + .withDialogSize(400, 500); + + popUp.showAndWait(); + } + + /** + * This method loads the link to be edited into the pop-up. + */ + private void loadLink() { + textField.setText(this.link.getText()); + reference.setValue(this.link.getReference()); + } + + /** + * This method retrieves the link created in the pop-up. + * @return Link created in pop-up, given as a Link object. + */ + public Link getLink() { + return link; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/PassagePopUp.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/PassagePopUp.java new file mode 100644 index 0000000000000000000000000000000000000000..3b25186dcc066391c7d7de2ac6678e971d28efbe --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/PassagePopUp.java @@ -0,0 +1,187 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.view.components.table.LinkTable; +import edu.ntnu.idatt2001.group_30.paths.view.components.table.TableDisplay; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +/** + * This class contains a pop-up for creating a new passage. + * + * @author Trym Hamer Gudvangen + */ +public class PassagePopUp extends AbstractPopUp { + + private TextField titleField; + private TextArea contentArea; + private Button saveButton; + private Button removeLinkButton; + private Button addLinkButton; + private Button editLinkButton; + private VBox content; + private LinkTable<Link> linkTable; + private final ObservableList<Passage> passages; + private final ObservableList<Link> links; + private Passage passage; + private PopUp<VBox, ?> popUp; + + /** + * This constructor allows a new passage to be created. + * @param passages Other passages in the story, given as an ObservableList of passages. + */ + public PassagePopUp(ObservableList<Passage> passages) { + this.passages = passages; + this.links = FXCollections.observableArrayList(); + initialize(); + createPopUp(); + } + + /** + * This constructor allows a pre-existing passage to be edited. + * @param passages Other passages in the story, given as an ObservableList of passages. + * @param passage Passage to edit, given as a Passage object. + */ + public PassagePopUp(ObservableList<Passage> passages, Passage passage) { + this.passages = passages; + this.passage = passage; + this.links = FXCollections.observableArrayList(passage.getLinks()); + initialize(); + loadPassage(passage); + createPopUp(); + } + + /** + * This method sets up the UI components for the pop-up. + */ + @Override + protected void setupUiComponents() { + titleField = new TextField(); + titleField.setPromptText("Enter the title of the passage"); + + contentArea = new TextArea(); + contentArea.setPromptText("Enter the content of the passage"); + contentArea.setMinHeight(150); + + saveButton = new Button("Save"); + + linkTable = + new LinkTable<>( + new TableDisplay.Builder<Link>().addColumn("Link Title", "text").addColumn("Reference", "reference") + ); + + linkTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + linkTable.setItems(links); + + editLinkButton = new Button("Edit Link"); + editLinkButton.setDisable(true); + + removeLinkButton = new Button("Remove Link"); + removeLinkButton.setDisable(true); + + addLinkButton = new Button("Add Link"); + if (passages.isEmpty()) addLinkButton.setDisable(true); + + HBox linkTableButtonHBox = new HBox(editLinkButton, addLinkButton, removeLinkButton); + linkTableButtonHBox.setAlignment(Pos.CENTER); + + content = + new VBox( + new Label("Passage Title:"), + titleField, + new Label("Passage Content:"), + contentArea, + new Label("Links:"), + linkTable, + linkTableButtonHBox, + saveButton + ); + + content.setAlignment(Pos.CENTER); + content.setSpacing(20); + } + + /** + * This method sets up the behavior for the pop-up. + */ + @Override + protected void setupBehavior() { + editLinkButton.setOnAction(e -> { + Link newLink = new LinkPopUp(this.passages, linkTable.getSelectionModel().getSelectedItem()).getLink(); + if (newLink != null) { + this.links.remove(linkTable.getSelectionModel().getSelectedItem()); + this.links.add(newLink); + } + }); + + removeLinkButton.setOnAction(e -> links.remove(linkTable.getSelectionModel().getSelectedItem())); + linkTable + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldSelection, newSelection) -> { + removeLinkButton.setDisable(newSelection == null); + editLinkButton.setDisable(newSelection == null); + }); + + addLinkButton.setOnAction(e -> { + Link newLink = new LinkPopUp(this.passages).getLink(); + if (newLink != null) { + this.links.add(newLink); + } + }); + + saveButton.setOnAction(e -> { + if (titleField.getText().isBlank() || contentArea.getText().isBlank()) { + AlertDialog.showWarning("The title or content cannot be blank."); + } else if ( + this.passages.stream() + .anyMatch(passage1 -> passage1.getTitle().equals(titleField.getText()) && passage != passage1) + ) { + AlertDialog.showWarning("A passage with the title " + titleField.getText() + " already exists."); + } else { + this.passage = new Passage(titleField.getText(), contentArea.getText()); + + this.links.forEach(link -> this.passage.addLink(link)); + popUp.close(); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + protected void createPopUp() { + popUp = + PopUp + .<VBox>create() + .withTitle("Create a Passage") + .withoutCloseButton() + .withContent(content) + .withDialogSize(400, 750); + + popUp.showAndWait(); + } + + /** + * This method loads a passage into the pop-up. + * @param passage Passage to load, given as a Passage object. + */ + private void loadPassage(Passage passage) { + titleField.setText(passage.getTitle()); + contentArea.setText(passage.getContent()); + } + + /** + * This method retrieves the passages created from the pop-up. + * @return Passages created, given as a List of Passage objects. + */ + public Passage getPassage() { + return passage; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/PopUp.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/PopUp.java new file mode 100644 index 0000000000000000000000000000000000000000..9fff0bfd999acf0d5a905339dcb4daf1a60c1473 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/PopUp.java @@ -0,0 +1,94 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +import javafx.scene.control.*; +import javafx.scene.layout.Region; + +/** + * This class provides a template for creating pop-ups. + * @param <T> The type of the content of the pop-up. + * @param <SELF> The type of the pop-up. + */ +public class PopUp<T extends Region, SELF extends PopUp<T, SELF>> extends Dialog<Void> { + + private final DialogPane dialogPane; + + /** + * This constructor sets up the dialog pane. + */ + protected PopUp() { + dialogPane = new DialogPane(); + setDialogPane(dialogPane); + } + + /** + * This method creates a pop-up. + * @return The pop-up. + * @param <T> The type of the content of the pop-up. + */ + public static <T extends Region> PopUp<T, ?> create() { + return new PopUp<>(); + } + + public SELF withTitle(String title) { + setTitle(title); + return self(); + } + + /** + * This method returns the dialog pane. + * @param content The content of the dialog pane. + * @return The dialog pane. + */ + public SELF withContent(T content) { + dialogPane.setContent(content); + return self(); + } + + /** + * This method returns the dialog pane. + * @param buttonType The button type of the dialog pane. + * @return The dialog pane. + */ + public SELF withButton(ButtonType buttonType) { + dialogPane.getButtonTypes().add(buttonType); + return self(); + } + + /** + * This method returns the dialog pane. + * @return The dialog pane. + */ + public SELF withoutCloseButton() { + // Add a close button type to the dialog + ButtonType closeButtonType = new ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE); + dialogPane.getButtonTypes().add(closeButtonType); + + // Get the actual button and make it invisible and unmanaged + Button closeButton = (Button) dialogPane.lookupButton(closeButtonType); + closeButton.setVisible(false); + closeButton.setManaged(false); + + return self(); + } + + /** + * This method returns the dialog pane. + * @param width The width of the dialog pane. + * @param height The height of the dialog pane. + * @return The dialog pane. + */ + public SELF withDialogSize(double width, double height) { + getDialogPane().setMinSize(width, height); + getDialogPane().setMaxSize(width, height); + return self(); + } + + /** + * This method returns the dialog pane. + * @return The dialog pane. + */ + @SuppressWarnings("unchecked") + protected SELF self() { + return (SELF) this; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/StatsPopUp.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/StatsPopUp.java new file mode 100644 index 0000000000000000000000000000000000000000..5e6ba2692d8d81b5937c13fac36729ab07d5ff52 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/pop_up/StatsPopUp.java @@ -0,0 +1,101 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.model.utils.TextValidation; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; + +/** + * This class contains a pop-up for adding stats to the player. + * + * @author Trym Hamer Gudvangen + */ +public class StatsPopUp extends AbstractPopUp { + + private TextField healthField; + private TextField goldField; + private Button saveButton; + private VBox content; + private PopUp<VBox, ?> popUp; + + /** + * This constructor creates a new StatsPopUp. + */ + public StatsPopUp() { + initialize(); + createPopUp(); + } + + /** + * This method retrieves the health value from the pop-up. + * @return The health value, as an int. + */ + public int getHealth() { + return Integer.parseInt(healthField.getText()); + } + + /** + * This method retrieves the gold value from the pop-up. + * @return The gold value, as an int. + */ + public int getGold() { + return Integer.parseInt(goldField.getText()); + } + + /** + * {@inheritDoc} + */ + @Override + protected void setupUiComponents() { + healthField = new TextField(); + healthField.setTextFormatter(TextValidation.createIntegerTextFormatter(INSTANCE.getPlayer().getHealth())); + + healthField.setPromptText("Add health value"); + + goldField = new TextField(); + goldField.setTextFormatter(TextValidation.createIntegerTextFormatter(INSTANCE.getPlayer().getGold())); + goldField.setPromptText("Add gold value"); + + saveButton = new Button("Save"); + + content = new VBox(new Label("Health:"), healthField, new Label("Gold:"), goldField, saveButton); + content.setAlignment(Pos.CENTER); + content.setSpacing(20); + } + + /** + * {@inheritDoc} + */ + @Override + protected void setupBehavior() { + saveButton.setOnAction(e -> { + if (healthField.getText().isBlank() || goldField.getText().isBlank()) { + AlertDialog.showWarning("The different fields cannot be blank."); + } else { + INSTANCE.getPlayer().setHealth(Integer.parseInt(healthField.getText())); + INSTANCE.getPlayer().setGold(Integer.parseInt(goldField.getText())); + popUp.close(); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + protected void createPopUp() { + popUp = + PopUp + .<VBox>create() + .withTitle("Add stats to your player") + .withoutCloseButton() + .withContent(content) + .withDialogSize(400, 300); + + popUp.showAndWait(); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/ActionTable.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/ActionTable.java new file mode 100644 index 0000000000000000000000000000000000000000..31719edef73503f62471e6fc6e08fd5d9a481baf --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/ActionTable.java @@ -0,0 +1,19 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +/** + * This class contains the table display implementation for the action object. + * @param <Action> Type of table, which is for Action. + * + * @author Trym Hamer Gudvangen + */ +public class ActionTable<Action> extends TableDisplay<Action> { + + /** + * This is a constructor which is used to construct a table for different actions. + * + * @param tableBuilder The builder used to construct a table, represented using an tableBuilder object. + */ + public ActionTable(Builder<Action> tableBuilder) { + super(tableBuilder); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/GoalTable.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/GoalTable.java new file mode 100644 index 0000000000000000000000000000000000000000..c9646a11b7d1f7fff6ec658b8d90751dc145d50a --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/GoalTable.java @@ -0,0 +1,19 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +/** + * This class contains the specific table display implementation for the Goal classes. + * @param <Goal> Type of table, which is Goal. + * + * @author Trym Hamer Gudvangen + */ +public class GoalTable<Goal> extends TableDisplay<Goal> { + + /** + * This is a constructor which is used to construct a table for different goals. + * + * @param tableBuilder The builder used to construct a table, represented using an tableBuilder object. + */ + public GoalTable(Builder<Goal> tableBuilder) { + super(tableBuilder); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/LinkTable.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/LinkTable.java new file mode 100644 index 0000000000000000000000000000000000000000..4a4dbaccc42362bc0bc7ced9c876aad4e75b3acf --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/LinkTable.java @@ -0,0 +1,17 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +/** + * This class concerns itself with the aspects intrinsic to a link table. + * @param <Link> The type of the table, represented using a Link object. + */ +public class LinkTable<Link> extends TableDisplay<Link> { + + /** + * This is a constructor which is used to construct a table for different links. + * + * @param tableBuilder The builder used to construct a table, represented using an tableBuilder object. + */ + public LinkTable(Builder<Link> tableBuilder) { + super(tableBuilder); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/PassageTable.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/PassageTable.java new file mode 100644 index 0000000000000000000000000000000000000000..50ade57fad82d379fe538d430571577c291e8947 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/PassageTable.java @@ -0,0 +1,17 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +/** + * This class concerns itself with the aspects intrinsic to a passage table. + * @param <Passage> The type of the table, represented using a Passage object. + */ +public class PassageTable<Passage> extends TableDisplay<Passage> { + + /** + * This is a constructor which is used to construct a table for different passages. + * + * @param tableBuilder The builder used to construct a table, represented using an tableBuilder object. + */ + public PassageTable(Builder<Passage> tableBuilder) { + super(tableBuilder); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/StatsTable.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/StatsTable.java new file mode 100644 index 0000000000000000000000000000000000000000..e639b45d7b44b74cc947efa52a7400eacce32c61 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/StatsTable.java @@ -0,0 +1,17 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +/** + * This class concerns itself with the aspects intrinsic to a stats table. + * @param <Player> The type of the table, represented using a Player object. + */ +public class StatsTable<Player> extends TableDisplay<Player> { + + /** + * This is a constructor which is used to construct a Player table. + * + * @param tableBuilder The builder used to construct a table, represented using an tableBuilder object. + */ + public StatsTable(Builder<Player> tableBuilder) { + super(tableBuilder); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/TableDisplay.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/TableDisplay.java new file mode 100644 index 0000000000000000000000000000000000000000..ad2f18c18b60760a46df91631850afe3113dccde --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/TableDisplay.java @@ -0,0 +1,77 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +import java.util.function.Function; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; + +/** + * This class concerns itself with building a table view filled with the desired information in columns. It does so + * by using a TableBuilder. + * + * @author Trym Hamer Gudvangen + */ +public class TableDisplay<T> extends TableView<T> { + + /** + * This is a constructor which is used to construct a table. + * @param tableBuilder The builder used to construct a table, represented using an tableBuilder object. + */ + public TableDisplay(Builder<T> tableBuilder) { + super(); + this.getColumns().addAll(tableBuilder.tableColumns); + } + + /** + * This class concerns itself with building a table view filled with the desired information of columns. In order + * to efficiently accomplish this, as well as remove the telescoping constructor anti-pattern, the builder pattern + * was utilized. + */ + public static class Builder<T> { + + private final ObservableList<TableColumn<T, ?>> tableColumns; + + public Builder() { + this.tableColumns = FXCollections.observableArrayList(); + } + + /** + * This method attaches a desired column {@link TableDisplayColumn#TableDisplayColumn(String, String)} to the + * tableview. + * @param infoHeader The name of the column, represented using a String. + * @param variableName The attribute the information will be extracted from, represented as a String. + * @return The builder itself is returned, represented as a Builder object. + */ + public Builder<T> addColumn(String infoHeader, String variableName) { + this.tableColumns.add(new TableDisplayColumn<T>(infoHeader, variableName).getColumn()); + return this; + } + + /** + * This method attaches a desired column {@link TableDisplayColumn#TableDisplayColumn(String, String)} to the + * tableview that is complex. + * @param infoHeader The name of the column, represented using a String. + * @param complexValueFunction The attribute the information will be extracted from, represented as a String. + * @return The builder itself is returned, represented as a Builder object. + */ + public Builder<T> addColumnWithComplexValue(String infoHeader, Function<T, String> complexValueFunction) { + TableColumn<T, String> column = new TableColumn<>(infoHeader); + column.setCellValueFactory(cellData -> + new SimpleStringProperty(complexValueFunction.apply(cellData.getValue())) + ); + column.setStyle("-fx-alignment: CENTER"); + this.tableColumns.add(column); + return this; + } + + /** + * This method actually constructs the table by creating an TableDisplay object. + * @return The table view, represented using an TableDisplay object. + */ + public TableDisplay<T> build() { + return new TableDisplay<T>(this); + } + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/TableDisplayColumn.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/TableDisplayColumn.java new file mode 100644 index 0000000000000000000000000000000000000000..0bdf895cbb4bd4d339aef21b2dd50be4699d9137 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/components/table/TableDisplayColumn.java @@ -0,0 +1,44 @@ +package edu.ntnu.idatt2001.group_30.paths.view.components.table; + +import javafx.scene.control.TableColumn; +import javafx.scene.control.cell.PropertyValueFactory; + +/** + * This class concerns itself with the aspects intrinsic to a column. It, therefore, contains a method for + * the creation of such a column. + * + * @author Trym Hamer Gudvangen + */ +public class TableDisplayColumn<T> { + + private TableColumn<T, ?> column; + + /** + * This constructor focuses on constructing the desired column from the information specified. + * @param infoHeader The name of the column, represented using a String. + * @param variableName The attribute the information will be extracted from, represented as a String. + */ + public TableDisplayColumn(String infoHeader, String variableName) { + createUnitColumn(infoHeader, variableName); + } + + /** + * This method creates a table column with the specified information. It, thereafter, is added + * as a column in the tableview provided. + * @param infoHeader The header of the column being added, represented as a String. + * @param variableName The name of the variable from the Unit, represented as a String. + */ + private void createUnitColumn(String infoHeader, String variableName) { + this.column = new TableColumn<>(infoHeader); + this.column.setCellValueFactory(new PropertyValueFactory<>(variableName)); + this.column.setStyle("-fx-alignment: CENTER"); + } + + /** + * This method retrieves the column. + * @return The column, represented using a TableColumn object. + */ + public TableColumn<T, ?> getColumn() { + return column; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/CreatePlayerView.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/CreatePlayerView.java new file mode 100644 index 0000000000000000000000000000000000000000..30947754877071704ec83cfa210b0d0ff006d75e --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/CreatePlayerView.java @@ -0,0 +1,192 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.controller.CreatePlayerController; +import edu.ntnu.idatt2001.group_30.paths.controller.StageManager; +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.view.components.ImageCarousel; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.AlertDialog; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.GoalsPopUp; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.StatsPopUp; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.image.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +/** + * The view for creating a player. This view therefore contains the character layout implementation and pop-ups + * for goals and stats. + * + * @author Trym Hamer Gudvangen + */ +public class CreatePlayerView extends View<BorderPane> { + + private final CreatePlayerController createPlayerController; + private TextField nameField; + private Button continueButton, returnButton; + + /** + * Creates the view for creating a player. + */ + public CreatePlayerView() { + super(BorderPane.class); + createPlayerController = new CreatePlayerController(); + + Text title = new Text("Create Your Player"); + title.setFont(Font.font("Arial", FontWeight.BOLD, 30)); + VBox topBox = new VBox(title); + topBox.setAlignment(Pos.CENTER); + topBox.setPadding(new Insets(20, 0, 20, 0)); + getParentPane().setTop(topBox); + + getParentPane().getStyleClass().add("create-player-view"); + getParentPane().getStylesheets().add(getClass().getResource("/stylesheet.css").toExternalForm()); + + Button statsButton = new Button("Stats"); + URL imageUrl = getClass().getResource("/images/stats.png"); + if (imageUrl != null) { + ImageView imageView = new ImageView(imageUrl.toString()); + imageView.setFitWidth(64); + imageView.setFitHeight(64); + statsButton.setGraphic(imageView); + } else { + System.err.println("Unable to load image: /images/stats.png"); + } + + Button goalsButton = new Button("Goals"); + imageUrl = getClass().getResource("/images/goals.png"); + if (imageUrl != null) { + ImageView imageView = new ImageView(imageUrl.toString()); + imageView.setFitWidth(90); + imageView.setFitHeight(90); + goalsButton.setGraphic(imageView); + } else { + System.err.println("Unable to load image: /images/stats.png"); + } + + statsButton.getStyleClass().add("stats-button"); + goalsButton.getStyleClass().add("goals-button"); + + VBox leftVBox = new VBox(statsButton, goalsButton); + leftVBox.setSpacing(20); + leftVBox.setPadding(new Insets(300, 20, 0, 20)); + leftVBox.setAlignment(Pos.CENTER); + getParentPane().setLeft(leftVBox); + getParentPane().getLeft().setTranslateY(-200); + + statsButton.setOnAction(e -> new StatsPopUp()); + + goalsButton.setOnAction(e -> new GoalsPopUp()); + + List<String> headURIs = new ArrayList<>(); + headURIs.add("/images/RedHair.png"); + headURIs.add("/images/BlueHair.png"); + headURIs.add("/images/GreenHair.png"); + + List<String> torsoURIs = new ArrayList<>(); + torsoURIs.add("/images/RedTorso.png"); + torsoURIs.add("/images/BlueTorso.png"); + torsoURIs.add("/images/GreenTorso.png"); + + List<String> legsURIs = new ArrayList<>(); + legsURIs.add("/images/RedLegs.png"); + legsURIs.add("/images/BlueLegs.png"); + legsURIs.add("/images/GreenLegs.png"); + + ImageCarousel headCarousel = new ImageCarousel(headURIs); + ImageCarousel torsoCarousel = new ImageCarousel(torsoURIs); + ImageCarousel legsCarousel = new ImageCarousel(legsURIs); + VBox centerBox = new VBox(headCarousel.getCarousel(), torsoCarousel.getCarousel(), legsCarousel.getCarousel()); + centerBox.setAlignment(Pos.CENTER); + leftVBox.getStyleClass().add("left-vbox"); + + nameField = new TextField(); + if (Objects.equals(INSTANCE.getPlayer().getName(), "Default")) { + nameField.setPromptText("Enter your name"); + } else { + nameField.setText(INSTANCE.getPlayer().getName()); + } + nameField.setMinWidth(200); + + continueButton = new Button("Continue"); + returnButton = new Button("Back"); + + getParentPane().setBottom(returnButton); + getParentPane().getBottom().setTranslateY(-50); + getParentPane().getBottom().setTranslateX(10); + + VBox bottomBox = new VBox(nameField, continueButton); + bottomBox.setSpacing(20); + bottomBox.setAlignment(Pos.CENTER); + bottomBox.setPadding(new Insets(0, 0, 0, 0)); + centerBox.getChildren().add(bottomBox); + getParentPane().setCenter(centerBox); + + centerBox.getStyleClass().add("center-box"); + nameField.getStyleClass().add("name-field"); + + continueButton.getStyleClass().add("continue-button"); + returnButton.getStyleClass().add("return-button"); + + continueButton.setOnAction(event -> { + try { + INSTANCE.setPlayer( + new Player( + nameField.getText(), + INSTANCE.getPlayer().getHealth(), + INSTANCE.getPlayer().getScore(), + INSTANCE.getPlayer().getGold() + ) + ); + + Image headImage = headCarousel.getCurrentImage().getImage(); + Image torsoImage = torsoCarousel.getCurrentImage().getImage(); + Image legsImage = legsCarousel.getCurrentImage().getImage(); + + WritableImage characterImage = new WritableImage( + (int) headImage.getWidth(), + (int) headImage.getHeight() * 3 + ); + PixelWriter writer = characterImage.getPixelWriter(); + + copyImageOnto(headImage, writer, 0); + copyImageOnto(torsoImage, writer, (int) headImage.getHeight()); + copyImageOnto(legsImage, writer, (int) ((int) headImage.getHeight() + torsoImage.getHeight())); + + ImageView characterImageView = new ImageView(characterImage); + INSTANCE.setCharacterImageView(characterImageView); + + createPlayerController.goTo(LoadGameView.class).handle(event); + } catch (Exception e) { + AlertDialog.showWarning(e.getMessage()); + } + }); + returnButton.setOnAction(e -> StageManager.getInstance().goBack()); + } + + /** + * Copies the image onto the writable image. + * @param image the image to copy + * @param writer the pixel writer + * @param yOffset the y offset + */ + private void copyImageOnto(Image image, PixelWriter writer, int yOffset) { + PixelReader reader = image.getPixelReader(); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + writer.setColor(x, y + yOffset, reader.getColor(x, y)); + } + } + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/HelpView.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/HelpView.java new file mode 100644 index 0000000000000000000000000000000000000000..d4df12b733ef8de4db37e94681b0556c644d8bbb --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/HelpView.java @@ -0,0 +1,78 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import edu.ntnu.idatt2001.group_30.paths.controller.HelpController; +import edu.ntnu.idatt2001.group_30.paths.controller.StageManager; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultButton; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultText; +import java.util.ArrayList; +import java.util.List; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +/** + * The Help page for the application. + * This page contains information on how to play the game. + * It also contains buttons for going back to the home page. + * @author Nicolai H. Brand. + */ +public class HelpView extends View<VBox> { + + private final HelpController controller = new HelpController(); + + /** + * Creates the help page. + */ + public HelpView() { + super(VBox.class); + VBox parent = getParentPane(); + parent.setAlignment(Pos.TOP_CENTER); + parent.setSpacing(20); + parent.setPadding(new javafx.geometry.Insets(50)); + + add(DefaultText.big("Help")); + add(helpText()); + addAll(getButtons()); + } + + /** + * Creates a scroll pane containing the help text. + * @return A scroll pane containing the help text. + */ + private Node helpText() { + ScrollPane scrollPane = new ScrollPane(); + scrollPane.prefWidth(600); + String howToPlay = + """ + New game: + + First you must create your own player. You can customize your outfit and enter your name. If you press the Stats button you can enter in your starting stats. If you press the Goals button you can set the goals for the play-trough. Once you are happy with your player, click continue. + + Then you need to load or create a new story by pressing the respective buttons. After a story has been selected or created, click Start game. + + During a play through, you will find the current passage to the left of the screen with its links to the bottom. In order to make a turn in the game you must press a link. A dialog pop-up will appear if you win or lose the game. If you want to restart a game or go back to home, you can press the restart and or home buttons in the top right corner. + """; + Text help = DefaultText.medium(howToPlay); + help.wrappingWidthProperty().bind(scrollPane.widthProperty().multiply(0.85)); + scrollPane.setContent(help); + + return scrollPane; + } + + /** + * Creates the buttons for the help page. + * The buttons are for going back to the home page and going back to the previous page. + * The previous page is the page the user was on before going to the help page. + * @return A list of buttons for the help page. + */ + private List<Node> getButtons() { + List<Node> buttons = new ArrayList<>(); + add(DefaultButton.medium("Home", controller.goTo(HomeView.class))); + add( + DefaultButton.medium("Go back to " + StageManager.getInstance().getPreviousViewName(), controller.goBack()) + ); + return buttons; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/HomeView.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/HomeView.java new file mode 100644 index 0000000000000000000000000000000000000000..55cbd5a9d909f0d08484b334dce398ba93f8a61a --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/HomeView.java @@ -0,0 +1,52 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import edu.ntnu.idatt2001.group_30.paths.controller.HomeController; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultButton; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultText; +import java.util.ArrayList; +import java.util.List; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.layout.VBox; + +/** + * The Home page for the application. + * This is the first page the user sees when starting the application. + * It contains buttons for starting a new game, showing the settings page and showing the help page. + * + * @author Nicolai H. Brand. + */ +public class HomeView extends View<VBox> { + + private final HomeController controller; + + /** + * The constructor of the class HomeView. + */ + public HomeView() { + super(VBox.class); + controller = new HomeController(); + + VBox parent = getParentPane(); + parent.setAlignment(Pos.TOP_CENTER); + parent.setSpacing(20); + parent.setPadding(new javafx.geometry.Insets(100)); + + add(DefaultText.big("Paths")); + addAll(getStartMenuButtons()); + } + + /** + * Creates the buttons for the start menu. + * @return A list of buttons for the start menu. + */ + private List<Node> getStartMenuButtons() { + List<Node> buttons = new ArrayList<>(); + if (controller.canContinueAGame()) buttons.add( + DefaultButton.big("Continue", controller.goBackTo(PlaythroughView.class)) + ); + buttons.add(DefaultButton.big("New game", controller.goTo(CreatePlayerView.class))); + buttons.add(DefaultButton.big("Help", controller.goTo(HelpView.class))); + return buttons; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/LoadGameView.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/LoadGameView.java new file mode 100644 index 0000000000000000000000000000000000000000..a4c2f3ba84ab8c86488377534a1f5b08cfdf3948 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/LoadGameView.java @@ -0,0 +1,205 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.controller.NewGameController; +import edu.ntnu.idatt2001.group_30.paths.controller.StageManager; +import edu.ntnu.idatt2001.group_30.paths.view.components.StoryDisplay; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultButton; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.AlertDialog; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; + +/** + * This class represents the view for creating/initiating a new game. It, therefore, contains the user + * interactions for creating, editing, and loading a new game, as well as for creating your player. + * + * @author Trym Hamer Gudvangen + */ +public class LoadGameView extends View<BorderPane> { + + private final NewGameController newGameController; + + private BorderPane titlePane; + private VBox buttonVBox; + private Button startButton; + private Button loadButton; + private Button newButton; + private HBox buttonBox; + + /** + * The constructor of the View class. + * It creates a new instance of the Pane that the View wraps. + */ + public LoadGameView() { + super(BorderPane.class); + newGameController = new NewGameController(); + + BorderPane titlePane = createTitlePane(); + + VBox mainContainer = createMainContainerVBox(titlePane); + + if (INSTANCE.getStory() != null) { + try { + addStoryPane(); + } catch (IOException e) { + AlertDialog.showError(e.getMessage()); + } + } + + setupParentPane(mainContainer); + } + + /** + * Adds the story pane to the view. + * @param titlePane The title pane of the view. + * @return The main container of the view. + */ + private VBox createMainContainerVBox(BorderPane titlePane) { + VBox mainContainer = new VBox(); + mainContainer.getChildren().addAll(titlePane); + mainContainer.setAlignment(Pos.TOP_CENTER); + mainContainer.setSpacing(100); + + Button backButton = new Button("Back"); + backButton.setOnAction(e -> StageManager.getInstance().goBack()); + getParentPane().setBottom(backButton); + + startButton = DefaultButton.medium("Start game", newGameController.goTo(PlaythroughView.class)); + startButton.setVisible(false); + + VBox containerWithButtons = new VBox(mainContainer, startButton); + containerWithButtons.setSpacing(20); + containerWithButtons.setAlignment(Pos.CENTER); + + return containerWithButtons; + } + + /** + * Sets up the parent pane of the view. + * @param mainContainer The main container of the view. + */ + private void setupParentPane(VBox mainContainer) { + getParentPane().setCenter(mainContainer); + getParentPane().setStyle("-fx-background-color: #f5f5f5"); + getParentPane().setMaxWidth(Double.MAX_VALUE); + getParentPane().setMaxHeight(Double.MAX_VALUE); + getParentPane().setPrefWidth(800); + getParentPane().setPrefHeight(600); + getParentPane().setPadding(new Insets(20)); + } + + /** + * Adds the story pane to the view. + * @return The story pane. + */ + private BorderPane createTitlePane() { + BorderPane titlePane = new BorderPane(); + titlePane.setPadding(new Insets(20)); + titlePane.setStyle("-fx-background-color: #f5f5f5"); + + Label title = new Label("New Game"); + title.setStyle("-fx-font-size: 20px; -fx-font-weight: bold"); + titlePane.setTop(title); + BorderPane.setAlignment(title, Pos.TOP_CENTER); + titlePane.getTop().setTranslateY(-70); + + loadButton = new Button("Load"); + newButton = new Button("New"); + + buttonBox = new HBox(10, loadButton, newButton); + buttonBox.setAlignment(Pos.CENTER); + buttonVBox = new VBox(buttonBox); + buttonVBox.setAlignment(Pos.CENTER); + titlePane.setCenter(buttonVBox); + + loadButton.setOnAction(e -> { + FileChooser fileChooser = new FileChooser(); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Paths files (*.paths)", "*.paths")); + + File resourceDirectory = new File("./src/main/resources/story-files"); + fileChooser.setInitialDirectory(resourceDirectory); + + File selectedFile = fileChooser.showOpenDialog(getParentPane().getScene().getWindow()); + if (selectedFile != null) { + try { + newGameController.setStory(selectedFile); + addStoryPane(); + } catch (RuntimeException runtimeException) { + AlertDialog.showError(runtimeException.getMessage()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + }); + + newButton.setOnAction(newGameController.goTo(NewStoryView.class)); + + this.titlePane = titlePane; + return titlePane; + } + + /** + * Creates an icon button. + * @param imagePath The path to the image. + * @param width The width of the image. + * @param height The height of the image. + * @return The button. + */ + private Button createIconButton(String imagePath, int width, int height) { + Button button = new Button(); + URL imageUrl = getClass().getResource(imagePath); + if (imageUrl != null) { + ImageView imageView = new ImageView(imageUrl.toString()); + imageView.setFitWidth(width); + imageView.setFitHeight(height); + button.setGraphic(imageView); + } else { + System.err.println("Unable to load image: " + imagePath); + } + return button; + } + + /** + * Adds the story pane to the view. + * @throws IOException If the story pane cannot be added. + */ + private void addStoryPane() throws IOException { + VBox storyVBox = new StoryDisplay.Builder(INSTANCE.getStory()) + .addStoryName() + .addFileInfo(INSTANCE.getStoryFile()) + .build(); + storyVBox.setAlignment(Pos.CENTER); + + Button pencilButton = createIconButton("/images/pencil.png", 16, 16); + Button xButton = createIconButton("/images/remove.png", 16, 16); + + HBox buttonIcons = new HBox(10, pencilButton, xButton); + buttonIcons.setAlignment(Pos.CENTER); + + VBox storyContainer = new VBox(storyVBox, buttonIcons); + storyContainer.setAlignment(Pos.CENTER); + + pencilButton.setOnAction(newGameController.goTo(NewStoryView.class)); + + xButton.setOnAction(event -> { + titlePane.getChildren().remove(storyContainer); + titlePane.setCenter(buttonBox); + startButton.setVisible(false); + + INSTANCE.setStory(null); + }); + + titlePane.setCenter(storyContainer); + startButton.setVisible(true); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/NewStoryView.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/NewStoryView.java new file mode 100644 index 0000000000000000000000000000000000000000..eedaa1d4d4c259148d1d926a775dab26a37dae03 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/NewStoryView.java @@ -0,0 +1,188 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import static edu.ntnu.idatt2001.group_30.paths.PathsSingleton.INSTANCE; + +import edu.ntnu.idatt2001.group_30.paths.controller.NewStoryController; +import edu.ntnu.idatt2001.group_30.paths.controller.StageManager; +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultText; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.AlertDialog; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.PassagePopUp; +import edu.ntnu.idatt2001.group_30.paths.view.components.table.PassageTable; +import edu.ntnu.idatt2001.group_30.paths.view.components.table.TableDisplay; +import java.net.URL; +import java.util.Objects; +import java.util.stream.Collectors; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +/** + * This class contains the view for creating/writing a new story. It, therefore, contains the title and passages. + * The passages can be changed and the corresponding object variables such as links and actions can also be changed. + * + * @author Trym Hamer Gudvangen + */ +public class NewStoryView extends View<BorderPane> { + + private final NewStoryController newStoryController; + private String title = ""; + private Story story; + private final ObservableList<Passage> passages; + private final Button removePassageButton; + private final Button editPassageButton; + + /** + * The constructor to create the NewStoryView. + */ + public NewStoryView() { + super(BorderPane.class); + newStoryController = new NewStoryController(); + + if (INSTANCE.getStory() != null) { + story = INSTANCE.getStory(); + } + + if (story != null) title = story.getTitle(); + + passages = + story == null + ? FXCollections.observableArrayList() + : FXCollections.observableArrayList(story.getPassages()); + Text titleText = DefaultText.big("Create a new/edit a Story"); + + Text labelText = new Text("Story Title: "); + TextField textField = new TextField(title); + textField.setPromptText("Enter story title"); + + HBox titleBox = new HBox(labelText, textField); + titleBox.setSpacing(20); + + textField.setOnKeyTyped(event -> title = textField.getText()); + + titleBox.setAlignment(Pos.CENTER); + + PassageTable<Passage> passageTable = new PassageTable<>( + new TableDisplay.Builder<Passage>() + .addColumn("Name of Passage", "title") + .addColumn("Passage Content", "content") + .addColumnWithComplexValue( + "Links", + passage -> + passage == null + ? null + : passage.getLinks().stream().map(Link::getText).collect(Collectors.joining(", ")) + ) + ); + + passageTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + passageTable.setItems(passages); + passageTable.setMaxWidth(1000); + + removePassageButton = new Button("Remove Passage"); + removePassageButton.setDisable(true); + removePassageButton.setOnAction(e -> { + passages.forEach(passage -> + passage + .getLinks() + .removeIf(link -> + Objects.equals( + link.getReference(), + passageTable.getSelectionModel().getSelectedItem().getTitle() + ) + ) + ); + passages.remove(passageTable.getSelectionModel().getSelectedItem()); + }); + + editPassageButton = new Button("Edit Passage"); + editPassageButton.setDisable(true); + editPassageButton.setOnAction(e -> { + Passage selectedPassage = passageTable.getSelectionModel().getSelectedItem(); + if (selectedPassage != null) { + Passage updatedPassage = new PassagePopUp(passages, selectedPassage).getPassage(); + if (updatedPassage != null && !selectedPassage.equals(updatedPassage)) { + passages.forEach(passage -> + passage + .getLinks() + .replaceAll(link -> + link.getReference().equals(selectedPassage.getTitle()) + ? new Link(link.getText(), updatedPassage.getTitle()) + : link + ) + ); + passages.remove(selectedPassage); + passages.add(updatedPassage); + } + } + }); + + passageTable + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldSelection, newSelection) -> { + removePassageButton.setDisable(newSelection == null); + editPassageButton.setDisable(newSelection == null); + }); + + Button addPassageButton = new Button(); + URL imageUrl = getClass().getResource("/images/plus.png"); + if (imageUrl != null) { + ImageView addIcon = new ImageView(new Image(imageUrl.toString())); + addIcon.setFitHeight(25); + addIcon.setFitWidth(25); + addPassageButton.setGraphic(addIcon); + } else { + System.err.println("Something is wrong with the trash image resource link"); + } + + VBox editTableButtons = new VBox(addPassageButton, removePassageButton, editPassageButton); + editTableButtons.setAlignment(Pos.CENTER); + editTableButtons.setSpacing(20); + + addPassageButton.setOnAction(event -> { + if (passages.isEmpty()) { + AlertDialog.showInformation( + "Every story needs an opening passage.", + "The opening passage" + " will by default be the first passage added." + ); + } + PassagePopUp passagePopUp = new PassagePopUp(passages); + if (passagePopUp.getPassage() != null) this.passages.addAll(passagePopUp.getPassage()); + }); + + Button saveButton = new Button("Save Story"); + saveButton.setOnAction(event -> { + try { + newStoryController.addStory(title, passages); + StageManager.getInstance().setCurrentView(new LoadGameView()); + } catch (Exception ex) { + AlertDialog.showWarning(ex.getMessage()); + } + }); + + VBox display = new VBox(titleText, titleBox, passageTable, saveButton); + display.setAlignment(Pos.CENTER); + display.setSpacing(10); + display.setPrefWidth(500); + + Button backButton = new Button("Back"); + backButton.setOnAction(newStoryController.goBack()); + + getParentPane().setCenter(display); + getParentPane().setBottom(backButton); + getParentPane().setRight(editTableButtons); + getParentPane().getRight().setTranslateX(-50); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/PlaythroughView.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/PlaythroughView.java new file mode 100644 index 0000000000000000000000000000000000000000..0f9da678288c54ba019f4258fe16e8c9203c1363 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/PlaythroughView.java @@ -0,0 +1,411 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import edu.ntnu.idatt2001.group_30.paths.controller.PlaythroughController; +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.PlaythroughState; +import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultButton; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultText; +import edu.ntnu.idatt2001.group_30.paths.view.components.common.Ref; +import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.AlertDialog; +import java.net.URL; +import java.util.Optional; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; + +/** + * The view for the play-through of the game. + * It is responsible for displaying the current passage, the current goals and the current inventory. + * It also displays the current statistics for the player, like health, score and gold. + * + * @author Nicolai H. Brand + */ +public class PlaythroughView extends View<VBox> { + + private final PlaythroughController controller; + + /** + * Creates a new instance of the view. + * It initializes the controller and sets up the view. + * It also sets up listeners for the game state. + */ + public PlaythroughView() { + super(VBox.class); + controller = new PlaythroughController(); + + /* + * The view is divided into three parts: + * +------------------------------------------------------+ + * | Header | + * +------------------------------------------------------+ + * | | player & game | + * | | info | + * | | --------------- | + * | play-through window | goals | + * | | --------------- | + * | | inventory | + * | | | + * +------------------------------------------------------+ + */ + + /* header */ + add(headerBox()); + + /* main content */ + HBox mainContent = new HBox(); + mainContent.paddingProperty().setValue(new javafx.geometry.Insets(20)); + mainContent.getChildren().add(playtroughBox(mainContent)); + mainContent.getChildren().add(infoBox(mainContent)); + add(mainContent); + + /* game state */ + listenForGameStateUpdate(); + } + + /** + * Sets up an event listener for the game state. + */ + private void listenForGameStateUpdate() { + controller + .getPlaythroughState() + .addListener((observable, oldValue, newValue) -> { + Optional<PlaythroughState> state = PlaythroughState.fromNumber((Integer) newValue); + if (state.isEmpty()) return; + + switch (state.get()) { + case WON -> { + boolean continuePlaying = AlertDialog.showConfirmation( + "Congratulations, you completed all your goals and won the game." + + "You can still continue to the story if its not finished. Do you want to continue the story?", + "You won!" + ); + if (!continuePlaying) controller.goToHome(); else controller.setWonButKeepPlaying(); + } + case DEAD -> AlertDialog.showConfirmation( + "You ran out of health and died. Press restart to try again.", + "You lost!" + ); + case STUCK -> AlertDialog.showConfirmation( + "You got stuck and cannot continue as no paths lead to you of this mess!. Press restart to try again.", + "You lost!" + ); + } + }); + } + + /** + * Creates the header box. + * It contains the title of the game and buttons for navigation. + * @return The parent node of the header box. + */ + private Node headerBox() { + /* container box configuration */ + HBox header = new HBox(); + header.setAlignment(Pos.TOP_CENTER); + header.setSpacing(20); + header.paddingProperty().setValue(new javafx.geometry.Insets(20)); + header.setBackground(new Background(new BackgroundFill(Color.LIGHTBLUE, null, null))); + + /* header content */ + header.getChildren().add(Ref.bigText(controller.getGameTitle())); + header.getChildren().add(new Separator(Orientation.VERTICAL)); + header.getChildren().add(DefaultButton.medium("Home", controller.goTo(HomeView.class))); + header + .getChildren() + .add( + DefaultButton.medium( + "Restart", + e -> { + if ( + AlertDialog.showConfirmation( + "Are you sure you want to restart the game? All progress will be lost.", + "Confirm restart" + ) + ) controller.startNewPlaythrough(); + } + ) + ); + header.getChildren().add(DefaultButton.medium("Help", controller.goTo(HelpView.class))); + return header; + } + + /** + * Creates the play-trough box. + * It contains the current passage and the links to the next passages. + * + * @return The parent node of the play-trough box. + */ + private Node playtroughBox(Pane parentPane) { + /* container box configuration */ + VBox playtrough = new VBox(); + playtrough.getStyleClass().add("box-shadow"); + playtrough.prefWidthProperty().bind(parentPane.widthProperty().multiply(0.6)); + playtrough.prefHeightProperty().bind(parentPane.heightProperty()); + playtrough.setAlignment(Pos.TOP_CENTER); + playtrough.setBackground(new Background(new BackgroundFill(Color.LIGHTBLUE, new CornerRadii(30), null))); + + /* Passage title */ + playtrough.getChildren().add(Ref.mediumText(controller.getPassageTitle())); + /* Passage content */ + ScrollPane passagePane = new ScrollPane(); + passagePane.paddingProperty().setValue(new javafx.geometry.Insets(20)); + passagePane.setMinHeight(350); + Text content = Ref.smallText(controller.getPassageContent()); + content.wrappingWidthProperty().bind(passagePane.widthProperty().multiply(0.85)); + passagePane.setContent(content); + playtrough.getChildren().add(passagePane); + + /* links */ + ScrollPane linksBoxWrapper = new ScrollPane(); + linksBoxWrapper.prefHeightProperty().bind(parentPane.heightProperty().multiply(0.5)); + linksBoxWrapper.centerShapeProperty().setValue(true); + VBox linksBox = new VBox(); + linksBox.setAlignment(Pos.TOP_CENTER); + linksBox.prefWidthProperty().bind(linksBoxWrapper.widthProperty()); + + linksBox.setSpacing(10); + ObservableList<Link> links = controller.getLinks(); + links.addListener( + (ListChangeListener<Link>) change -> { + linksBox.getChildren().clear(); + populateLinksBox(linksBox, links); + } + ); + populateLinksBox(linksBox, links); + linksBoxWrapper.setContent(linksBox); + playtrough.getChildren().add(linksBoxWrapper); + + return playtrough; + } + + /** + * Populates the links box with the given links. + * Sets the links to disabled if the player cannot keep playing. + * @param linksBox The box to populate. + * @param links The links to populate the box with. + */ + private void populateLinksBox(VBox linksBox, ObservableList<Link> links) { + linksBox.getChildren().clear(); + for (Link link : links) { + Button button = DefaultButton.medium(link.getText(), e -> controller.makeTurn(link)); + button.setWrapText(true); + button.setPrefWidth(300); + button.getStyleClass().add("link-button"); + button.setDisable(!controller.canKeepPlaying()); + button.setOpacity(controller.canKeepPlaying() ? 1 : 0.5); + linksBox.getChildren().add(button); + } + } + + /** + * Creates the info box. + * It contains the current player information, like health, score and gold. + * It also contains the list of all goals. + * + * @return The parent node of the statistics box. + */ + private Node infoBox(Pane parentPane) { + VBox infoBox = new VBox(); + infoBox.prefWidthProperty().bind(parentPane.widthProperty().multiply(0.4)); + infoBox.prefHeightProperty().bind(parentPane.heightProperty()); + infoBox.setAlignment(Pos.TOP_CENTER); + infoBox.setSpacing(20); + infoBox.setPadding(new Insets(0, 0, 0, 20)); // Set 20 pixels of left padding + + /* player information */ + infoBox.getChildren().add(playerInfo(infoBox)); + /* list of all goals */ + infoBox.getChildren().add(goalInfo(infoBox)); + /* inventory */ + infoBox.getChildren().add(inventoryInfo(infoBox)); + return infoBox; + } + + /** + * Creates the player information box. + * It contains the player name, score, health and gold. + * + * @return The parent node of the player information box. + */ + private Node playerInfo(Pane parentPane) { + /* + * Player info: + * player info | image + */ + VBox playerInfoBox = new VBox(); + playerInfoBox.getStyleClass().add("box-shadow"); + playerInfoBox.prefHeightProperty().bind(parentPane.heightProperty().multiply(0.3)); + playerInfoBox.setBackground(new Background(new BackgroundFill(Color.LIGHTBLUE, new CornerRadii(30), null))); + playerInfoBox.setAlignment(Pos.TOP_CENTER); + playerInfoBox.setSpacing(10); + + /* title */ + playerInfoBox.getChildren().add(DefaultText.medium("Player info:")); + + /* player data and image container */ + HBox playerDataAndImage = new HBox(); + playerDataAndImage.setAlignment(Pos.TOP_LEFT); + + /* player data */ + VBox playerData = new VBox(); + playerData.setAlignment(Pos.TOP_CENTER); + playerData.setSpacing(10); + playerData.prefWidthProperty().bind(playerInfoBox.widthProperty().multiply(0.5)); + playerData.getChildren().add(DefaultText.small("Name: " + controller.getPlayerName())); + + HBox score = new HBox(); + score.setAlignment(Pos.TOP_CENTER); + score.getChildren().add(DefaultText.small("Score: ")); + score.getChildren().add(Ref.smallText(controller.getScore())); + playerData.getChildren().add(score); + + HBox health = new HBox(); + health.setAlignment(Pos.TOP_CENTER); + health.getChildren().add(DefaultText.small("Health: ")); + health.getChildren().add(Ref.smallText(controller.getHealth())); + playerData.getChildren().add(health); + + HBox gold = new HBox(); + gold.setAlignment(Pos.TOP_CENTER); + gold.getChildren().add(DefaultText.small("Gold: ")); + gold.getChildren().add(Ref.smallText(controller.getGold())); + playerData.getChildren().add(gold); + + /* player image */ + ImageView characterImageView = controller.getCharacterImageView(); + characterImageView.setFitHeight(150); + characterImageView.setPreserveRatio(true); + + playerDataAndImage.getChildren().add(playerData); + playerDataAndImage.getChildren().add(characterImageView); + + playerInfoBox.getChildren().add(playerDataAndImage); + + return playerInfoBox; + } + + /** + * Creates the goal information box. + * It contains the list of all goals. + * + * @return The parent node of the goal information box. + */ + private Node goalInfo(Pane parentPane) { + Pane goalInfoBox = initInfoCard(parentPane); + + /* title */ + goalInfoBox.getChildren().add(DefaultText.medium("Goals:")); + + /* content */ + ScrollPane goalBox = new ScrollPane(); + goalBox.setMinHeight(125); + VBox content = new VBox(); + content.setAlignment(Pos.TOP_LEFT); + content.setSpacing(20); + content.setPadding(new Insets(0, 0, 0, 20)); + + ObservableMap<Goal<?>, Boolean> goals = controller.getGoals(); + goals.addListener( + (MapChangeListener<Goal<?>, Boolean>) change -> { + showGoals(goals, content); + } + ); + + showGoals(goals, content); + goalBox.setContent(content); + goalInfoBox.getChildren().add(goalBox); + return goalInfoBox; + } + + /** + * Creates the inventory information box. + * It contains the list of all items in the inventory. + * @return The parent node of the inventory information box. + */ + private Node inventoryInfo(Pane parentPane) { + Pane inventoryInfoBox = initInfoCard(parentPane); + + /* title */ + inventoryInfoBox.getChildren().add(DefaultText.medium("Inventory:")); + + /* content */ + ScrollPane inventoryBox = new ScrollPane(); + inventoryBox.setMinHeight(125); + VBox content = new VBox(); + content.setAlignment(Pos.TOP_LEFT); + content.setSpacing(20); + + ObservableList<String> inventory = controller.getInventory(); + inventory.addListener( + (ListChangeListener<String>) change -> { + showInventory(inventory, content); + } + ); + + showInventory(inventory, content); + inventoryBox.setContent(content); + inventoryInfoBox.getChildren().add(inventoryBox); + return inventoryInfoBox; + } + + private Pane initInfoCard(Pane parentPane) { + VBox box = new VBox(); + box.getStyleClass().add("box-shadow"); + box.setAlignment(Pos.TOP_CENTER); + box.prefHeightProperty().bind(parentPane.heightProperty().multiply(0.3)); + box.setBackground(new Background(new BackgroundFill(Color.LIGHTBLUE, new CornerRadii(30), null))); + return box; + } + + /** + * Shows the inventory in the given content pane. + * @param inventory The inventory to show. + * @param content The pane to show the inventory in. + */ + private void showInventory(ObservableList<String> inventory, Pane content) { + content.getChildren().clear(); + for (String item : inventory) { + content.getChildren().add(DefaultText.small(item)); + } + } + + /** + * Shows the goals in the given content pane. + * + * @param goals The goals to show. + * @param content The pane to show the goals in. + */ + private void showGoals(ObservableMap<Goal<?>, Boolean> goals, Pane content) { + content.getChildren().clear(); + goals.forEach((goal, completed) -> { + HBox goalBox = new HBox(); + goalBox.setAlignment(Pos.TOP_LEFT); + goalBox.setSpacing(10); + goalBox.getChildren().add(DefaultText.small(goal.toString())); + URL imageUrl = getClass().getResource("/images/checkbox-" + (completed ? "marked" : "blank") + ".png"); + if (imageUrl != null) { + ImageView imageView = new ImageView(imageUrl.toString()); + imageView.setFitWidth(20); + imageView.setPreserveRatio(true); + goalBox.getChildren().add(imageView); + } else { + System.err.println("Unable to load image: " + imageUrl); + } + + content.getChildren().add(goalBox); + }); + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/View.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/View.java new file mode 100644 index 0000000000000000000000000000000000000000..e549e73a5ab571aae955301d855b93469f3804ab --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/View.java @@ -0,0 +1,104 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultText; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Objects; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.*; + +/** + * A View is a wrapper for a JavaFX Pane. + * It is responsible for creating the Pane and adding content to it. + * The View class is generic, and the type parameter T is the type of Pane that the View wraps. + * The View class can be effortlessly represented into a JavaFX Scene. + * The View class is also responsible for adding global content to the Scene. + * @param <T> The type of Pane that the View wraps. + * + * @author Nicolai H. Brand. + */ +public class View<T extends Pane> { + + public static final int DEFAULT_WIDTH = 1280; + public static final int DEFAULT_HEIGHT = 720; + private final String stylesheet = Objects + .requireNonNull(getClass().getResource("/stylesheet.css")) + .toExternalForm(); + private T parentPane; + + /** + * The constructor of the View class. + * It creates a new instance of the Pane that the View wraps. + * @param paneClass The class of the Pane that the View wraps. + */ + public View(Class<T> paneClass) { + try { + parentPane = paneClass.getDeclaredConstructor().newInstance(); + parentPane.getStylesheets().add(getClass().getResource("/stylesheet.css").toExternalForm()); + } catch ( + InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e + ) { + e.printStackTrace(); + } + } + + /** + * Helper method that adds a JavaFX Node to the Pane that the View wraps. + * @param node The JavaFX Node to be added to the Pane that the View wraps. + */ + protected void add(Node node) { + this.parentPane.getChildren().add(node); + } + + /** + * Helper method that adds a JavaFX Node to the Pane that the View wraps. + * @param nodes The JavaFX Nodes to be added to the Pane that the View wraps. + */ + protected void addAll(Node... nodes) { + this.parentPane.getChildren().addAll(nodes); + } + + /** + * Helper method that adds a JavaFX Node to the Pane that the View wraps. + * @param nodes The JavaFX Nodes as a List to be added to the Pane that the View wraps. + */ + protected void addAll(List<Node> nodes) { + this.parentPane.getChildren().addAll(nodes); + } + + /** + * Method that "converts" the View into a JavaFX Scene. + * It adds global content to the Scene. + * + * @return The View as a JavaFX Scene. + */ + public Scene asScene() { + //NOTE: the wrapper could also be a HBox or really any other Pane. + // The reason for using a BorderPane is that it is easy to add content to the top, bottom, left and right. + // A view is free to override this method and use a different Pane. + BorderPane wrapper = new BorderPane(); + + wrapper.setCenter(parentPane); + wrapper.setBottom(globalFooter()); + wrapper.getStylesheets().add(stylesheet); + return new Scene(wrapper, DEFAULT_WIDTH, DEFAULT_HEIGHT); + } + + //NOTE: I am not sure if this is useful + /** + * Method that adds global content to the Scene. + * @return A JavaFX Node that is added to the Scene. + */ + private Node globalFooter() { + return DefaultText.small("Made by Trym H. Gudvangen and Nicolai H. Brand"); + } + + /** + * Getter for the Pane that the View wraps. + * @return The Pane that the View wraps. + */ + protected T getParentPane() { + return parentPane; + } +} diff --git a/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/ViewFactory.java b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/ViewFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9674ac5f1536e3f4d80b3f4cfe498d80661cb24b --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2001/group_30/paths/view/views/ViewFactory.java @@ -0,0 +1,25 @@ +package edu.ntnu.idatt2001.group_30.paths.view.views; + +import java.lang.reflect.InvocationTargetException; + +/** + * A factory for creating views. + */ +public class ViewFactory { + + /** + * Given a class of a view, this method creates an instance of that view. + * @param viewClass The class of the view to be created. + * @return An instance of the view. + * @param <T> The type of the view. + */ + public static <T extends View<?>> T createView(Class<T> viewClass) { + try { + return viewClass.getDeclaredConstructor().newInstance(); + } catch ( + InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e + ) { + throw new RuntimeException("Failed to create view", e); + } + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000000000000000000000000000000000000..566bcec21c7ba5336dc035ea8de557aadf69cf00 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,16 @@ +module edu.ntnu.idatt2001.group_30.paths { + requires javafx.controls; + requires javafx.graphics; + + exports edu.ntnu.idatt2001.group_30.paths; + exports edu.ntnu.idatt2001.group_30.paths.view; + exports edu.ntnu.idatt2001.group_30.paths.model; + exports edu.ntnu.idatt2001.group_30.paths.model.goals; + exports edu.ntnu.idatt2001.group_30.paths.model.actions; + exports edu.ntnu.idatt2001.group_30.paths.view.components; + exports edu.ntnu.idatt2001.group_30.paths.view.components.common; + exports edu.ntnu.idatt2001.group_30.paths.view.components.pane; + exports edu.ntnu.idatt2001.group_30.paths.view.components.table; + exports edu.ntnu.idatt2001.group_30.paths.view.components.pop_up; + exports edu.ntnu.idatt2001.group_30.paths.view.views; +} diff --git a/src/main/resources/images/BlueHair.png b/src/main/resources/images/BlueHair.png new file mode 100644 index 0000000000000000000000000000000000000000..53266e43eff58627c5b8e69c5189b8ef68007575 Binary files /dev/null and b/src/main/resources/images/BlueHair.png differ diff --git a/src/main/resources/images/BlueLegs.png b/src/main/resources/images/BlueLegs.png new file mode 100644 index 0000000000000000000000000000000000000000..19825d85e4f846b379e15e7ac2b93d8e40c30d41 Binary files /dev/null and b/src/main/resources/images/BlueLegs.png differ diff --git a/src/main/resources/images/BlueTorso.png b/src/main/resources/images/BlueTorso.png new file mode 100644 index 0000000000000000000000000000000000000000..2c3c42e94c1fac5209282ffaaabd1a9892a60f19 Binary files /dev/null and b/src/main/resources/images/BlueTorso.png differ diff --git a/src/main/resources/images/GreenHair.png b/src/main/resources/images/GreenHair.png new file mode 100644 index 0000000000000000000000000000000000000000..d46e977539f481065882d30f8c8ae9e8f1e09539 Binary files /dev/null and b/src/main/resources/images/GreenHair.png differ diff --git a/src/main/resources/images/GreenLegs.png b/src/main/resources/images/GreenLegs.png new file mode 100644 index 0000000000000000000000000000000000000000..330f4005a9485f2763feb718fda311d056500102 Binary files /dev/null and b/src/main/resources/images/GreenLegs.png differ diff --git a/src/main/resources/images/GreenTorso.png b/src/main/resources/images/GreenTorso.png new file mode 100644 index 0000000000000000000000000000000000000000..5d767afa278d7543c6665d5e1e8ef5785fda4c6c Binary files /dev/null and b/src/main/resources/images/GreenTorso.png differ diff --git a/src/main/resources/images/RedHair.png b/src/main/resources/images/RedHair.png new file mode 100644 index 0000000000000000000000000000000000000000..b3bad69ad8b82de1bf7cbfd8c0a3da813552a623 Binary files /dev/null and b/src/main/resources/images/RedHair.png differ diff --git a/src/main/resources/images/RedLegs.png b/src/main/resources/images/RedLegs.png new file mode 100644 index 0000000000000000000000000000000000000000..1f6362b66bd8420dcedb91013f287acf456c8ca3 Binary files /dev/null and b/src/main/resources/images/RedLegs.png differ diff --git a/src/main/resources/images/RedTorso.png b/src/main/resources/images/RedTorso.png new file mode 100644 index 0000000000000000000000000000000000000000..6641be0d86b0b162825314f21bd22db2d4b8ad37 Binary files /dev/null and b/src/main/resources/images/RedTorso.png differ diff --git a/src/main/resources/images/checkbox-blank.png b/src/main/resources/images/checkbox-blank.png new file mode 100644 index 0000000000000000000000000000000000000000..2f476be5e2850b492d5e05e50bcba763cc2b69ee Binary files /dev/null and b/src/main/resources/images/checkbox-blank.png differ diff --git a/src/main/resources/images/checkbox-marked.png b/src/main/resources/images/checkbox-marked.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1ffe6337a91cd223336fe6c6dbc5da66206288 Binary files /dev/null and b/src/main/resources/images/checkbox-marked.png differ diff --git a/src/main/resources/images/goals.png b/src/main/resources/images/goals.png new file mode 100644 index 0000000000000000000000000000000000000000..a489800498b8fc729c660682e555244c9152b169 Binary files /dev/null and b/src/main/resources/images/goals.png differ diff --git a/src/main/resources/images/pencil.png b/src/main/resources/images/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb6b3346ff57af0ec417e7883b9e47d6a0ed394 Binary files /dev/null and b/src/main/resources/images/pencil.png differ diff --git a/src/main/resources/images/plus.png b/src/main/resources/images/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..f37375ad493137bed31587b47b1096a8777d2e6c Binary files /dev/null and b/src/main/resources/images/plus.png differ diff --git a/src/main/resources/images/remove.png b/src/main/resources/images/remove.png new file mode 100644 index 0000000000000000000000000000000000000000..7332c38e55633253a05a6f4408a419fe13c4fe67 Binary files /dev/null and b/src/main/resources/images/remove.png differ diff --git a/src/main/resources/images/stats.png b/src/main/resources/images/stats.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f99039bdfce27cae5246b5cd23ba6ffeea0885 Binary files /dev/null and b/src/main/resources/images/stats.png differ diff --git a/src/main/resources/images/trash.png b/src/main/resources/images/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..ad7faf0f79e9488778dbea99146f459990bd76d6 Binary files /dev/null and b/src/main/resources/images/trash.png differ diff --git a/src/main/resources/story-files/Bones.paths b/src/main/resources/story-files/Bones.paths new file mode 100644 index 0000000000000000000000000000000000000000..59af36f46e6ff1e582aa3b0cf503a5ec7b374fe2 --- /dev/null +++ b/src/main/resources/story-files/Bones.paths @@ -0,0 +1,10 @@ +Haunted House + +::Beginnings +You are in a small, dimly lit room. There is a door in front of you. +[Try to open the door](Another room) + +::Another room +The door opens to another room. You see a desk with a large, dusty book. +[Open the book](The book of spells) +[Go back](Beginnings) \ No newline at end of file diff --git a/src/main/resources/story-files/Programmer.paths b/src/main/resources/story-files/Programmer.paths new file mode 100644 index 0000000000000000000000000000000000000000..dec5bb342c10d949811b377bd0bc0018d0b5d1e9 --- /dev/null +++ b/src/main/resources/story-files/Programmer.paths @@ -0,0 +1,40 @@ +A Programmer's Adventure at NTNU + +::Morning Routine +As the sun shines through the blinds, the programmer wakes up in his apartment, nestled in the heart of Trondheim. He looks at his NTNU mug, ready to be filled with coffee, the fuel of programmers. +[Continue to Breakfast](Breakfast) + +::Breakfast +Breakfast consists of coffee and skolebrød, a traditional Norwegian pastry. He sits at his desk, his workstation waiting for his magic touch. Looking out the window, he enjoys the calmness of Trondheim before his day begins. +[Head to the University](The Journey Begins) +<HealthAction>\-5/ +[Suddenly, the power goes out](The Journey Begins) + +::The Journey Begins +Leaving his apartment, he heads towards NTNU’s Gløshaugen campus. It’s a brisk and sunny walk, just enough to refresh his mind before the codes begin to fly. +[Arrive at University](First Class of the Day) +<ScoreAction>\12/ +[Snow starts falling](First Class of the Day) + +::First Class of the Day +His first class of the day is "Advanced Algorithms". The lecture is complex, but he manages to follow along and even answers a couple of questions, impressing his peers and professors. +[Continue to the Library](Library Visit) + +::Library Visit +With a few hours until his next class, he heads to the university library. Surrounded by books, his heart is filled with a sense of tranquillity. This is where he prepares for his next project. +[Start Working on Project](Project Time) +<GoldAction>\50/ +[Find a book on a new programming concept](Project Time) +<InventoryAction>\Book/ + +::Project Time +He's working on a project that involves machine learning algorithms. It's challenging but he loves every bit of it. This project is his chance to show his potential. +[Continue Working on the Project](Project Continuation) + +::Project Continuation +Hours fly by as he's immersed in his project. He encounters a tricky bug but after some debugging, he squashes it triumphantly. +[End of Day](The Day Ends) + +::The Day Ends +The programmer's day at NTNU comes to an end. Filled with adventures, knowledge, and triumphs, it's a day that he will always remember. As he steps out of the university, he looks forward to what the next day will bring. +[Another day](Morning Routine) \ No newline at end of file diff --git a/src/main/resources/stylesheet.css b/src/main/resources/stylesheet.css new file mode 100644 index 0000000000000000000000000000000000000000..8aeae865e557fef7f334a291f96731308bd7f609 --- /dev/null +++ b/src/main/resources/stylesheet.css @@ -0,0 +1,151 @@ +* { + /*-fx-font-family: Arial;*/ + -fx-transition: all 0.3s ease-in-out; +} + +.border-pane { + background-color: #f5f5f5; +} + +.label { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 5px; +} + +.text-field { + -fx-padding: 8px; + -fx-border-color: #ccc; + -fx-border-width: 1px; + -fx-border-radius: 5px; +} + + +.text-field:focused { + -fx-border-color: #0077ff; +} + + + +#title { + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-color: #2c3e50; +} + +#player-container, #goal-container { + -fx-background-color: white; + -fx-padding: 20px; + -fx-spacing: 10px; +} + +#player-form { + -fx-padding: 10px; + -fx-border-color: #ccc; + -fx-border-width: 1px; + -fx-border-radius: 5px; +} + +#player-form .label { + -fx-font-size: 14px; + -fx-font-weight: bold; +} + +#player-form .text-field { + -fx-padding: 5px; + -fx-border-color: #ccc; + -fx-border-width: 1px; + -fx-border-radius: 5px; +} + +#player-form .text-field:focused { + -fx-border-color: #0077ff; +} + +#goal-selector { + -fx-font-size: 14px; + -fx-padding: 5px; + -fx-border-color: #ccc; + -fx-border-width: 1px; + -fx-border-radius: 5px; +} + +#load-button, #new-button { + -fx-background-color: #0077ff; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 5px 10px; + -fx-border-radius: 5px; +} + +#load-button:hover, #new-button:hover { + -fx-background-color: #005ce6; +} + +.create-player-view { + -fx-background-color: #f0f0f0; +} + +.left-vbox { + -fx-spacing: 30; +} + +.name-field { + -fx-padding: 10; +} + +Button { + -fx-background-color: #3f51b5; + -fx-text-fill: #ffffff; + -fx-border-color: #303f9f; + -fx-border-width: 1px; + -fx-padding: 10 20; +} + +Button:hover { + -fx-background-color: #303f9f; +} + +.stats-button, +.goals-button { + -fx-background-color: #64b5f6; + -fx-text-fill: #ffffff; + -fx-border-color: #42a5f5; + -fx-border-width: 1px; + -fx-padding: 10 20; + -fx-font-weight: bold; +} + +.stats-button:hover, +.goals-button:hover { + -fx-background-color: #42a5f5; +} + +.continue-button, +.return-button { + -fx-background-color: #3fb551; + -fx-text-fill: #ffffff; + -fx-border-color: #43a047; + -fx-border-width: 1px; + -fx-padding: 10 20; + -fx-font-weight: bold; +} + +.continue-button:hover, +.return-button:hover { + -fx-background-color: #43a047; +} + +.center-box { + -fx-spacing: 20; + -fx-padding: 20; +} + +.box-shadow { + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0, 0, 0); +} + +.link-button { + -fx-background-radius: 20; +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/filehandling/StoryFileHandlerTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/filehandling/StoryFileHandlerTest.java deleted file mode 100644 index 08dc95b5455492b4c28e81ec8160aafebd625a0b..0000000000000000000000000000000000000000 --- a/src/test/java/edu/ntnu/idatt2001/group_30/filehandling/StoryFileHandlerTest.java +++ /dev/null @@ -1,289 +0,0 @@ -package edu.ntnu.idatt2001.group_30.filehandling; - -import edu.ntnu.idatt2001.group_30.Link; -import edu.ntnu.idatt2001.group_30.Passage; -import edu.ntnu.idatt2001.group_30.Story; -import edu.ntnu.idatt2001.group_30.actions.*; -import edu.ntnu.idatt2001.group_30.exceptions.CorruptFileException; -import edu.ntnu.idatt2001.group_30.exceptions.CorruptLinkException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.io.*; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class StoryFileHandlerTest { - - @BeforeAll - static void setFileHandlerPath() { - FileHandler.changeDefaultPath("src/test/resources/storytestfiles"); - } - - StoryFileHandler storyFileHandler = new StoryFileHandler(); - - public File getValidFile(String fileName) { - return FileHandler.createFile(fileName); - } - - static Story validStory(){ - Story story = new Story("The Hobbit", new Passage("Beginning", "Once upon a time...")); - Passage secondChapter = new Passage("The Great Barrier", "After having completed the arduous..."); - story.addPassage(secondChapter); - story.getOpeningPassage().addLink(new Link(secondChapter.getTitle(), secondChapter.getTitle())); - story.getOpeningPassage().getLinks().forEach(link -> link.addAction(new GoldAction(5))); - story.getOpeningPassage().getLinks().get(0).addAction(new ScoreAction(5)); - story.getOpeningPassage().getLinks().get(0).addAction(new HealthAction(6)); - story.getOpeningPassage().getLinks().get(0).addAction(new InventoryAction("Sword")); - return story; - } - - @Nested - public class A_StoryFile_is_valid_if { - - @ParameterizedTest(name = "{index}. File name: {0}") - @ValueSource(strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void a_file_has_a_valid_name(String fileName) { - Story story = validStory(); - - try { - storyFileHandler.createStoryFile(story, fileName); - } catch (Exception e) { - if(!e.getMessage().equals("You cannot overwrite a pre-existing story file")){ - System.out.println(e.getMessage()); - fail("An exception was thrown when it shouldn't have."); - } - } - - File expectedFileCreated = getValidFile(fileName); - - Assertions.assertTrue(expectedFileCreated.isFile()); - expectedFileCreated.delete(); - } - - @ParameterizedTest (name = "{index}. File name: {0}") - @ValueSource (strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void files_created_can_be_accessed_and_read(String fileName) { - Story story = validStory(); - - try{ - storyFileHandler.createStoryFile(story, fileName); - }catch (Exception e){ - fail("An exception was thrown when it shouldn't have. " + e.getMessage()); - } - - File expectedFileCreated = getValidFile(fileName); - - Assertions.assertTrue(expectedFileCreated.canRead()); - expectedFileCreated.delete(); - } - - @ParameterizedTest (name = "{index}. File name: {0}") - @ValueSource (strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void the_pathing_is_correctly_set(String fileName) { - Story story = validStory(); - boolean fileDoesNotExistAtStart = !getValidFile(fileName).exists(); - - try{ - storyFileHandler.createStoryFile(story, fileName); - }catch (Exception e){ - fail("An exception was thrown when it shouldn't have."); - } - - File expectedFileCreated = getValidFile(fileName); - boolean fileDoesExistAfterWrite = expectedFileCreated.exists(); - - //Then/Assert - Assertions.assertTrue(fileDoesNotExistAtStart); - Assertions.assertTrue(fileDoesExistAfterWrite); - expectedFileCreated.delete(); - } - - @Test - void it_cannot_create_new_file_with_preexisting_file_name() { - Story story = validStory(); - String fileName = "Bones"; - - File preexistingFile = getValidFile(fileName); - if(getValidFile(fileName).isFile()) { - Assertions.assertThrows(IllegalArgumentException.class, () -> storyFileHandler.createStoryFile(story, fileName)); - } - else fail("The file check for doesn't exist, so this test is invalid"); - } - - } - - @Nested - public class A_StoryFile_properly_writes_a_story_to_new_file_if_it { - - @ParameterizedTest (name = "{index}. File name: {0}") - @ValueSource (strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void saves_the_story_title_correctly(String fileName) throws IOException, InstantiationException { - Story story = validStory(); - String expectedTitle = story.getTitle(); - - storyFileHandler.createStoryFile(story, fileName); - Story storyReadFromFile = storyFileHandler.readStoryFromFile(fileName); - String actualTitle = storyReadFromFile.getTitle(); - - Assertions.assertEquals(expectedTitle, actualTitle); - - File file = getValidFile(fileName); - file.delete(); - } - - @ParameterizedTest (name = "{index}. File name: {0}") - @ValueSource (strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void saves_the_opening_passage_after_title(String fileName) throws IOException, InstantiationException { - Story story = validStory(); - Passage expectedOpeningPassage = story.getOpeningPassage(); - - storyFileHandler.createStoryFile(story, fileName); - Story storyReadFromFile = storyFileHandler.readStoryFromFile(fileName); - Passage actualOpeningPassage = storyReadFromFile.getOpeningPassage(); - - Assertions.assertEquals(expectedOpeningPassage, actualOpeningPassage); - - File file = getValidFile(fileName); - file.delete(); - } - - @ParameterizedTest (name = "{index}. File name: {0}") - @ValueSource (strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void saves_all_the_links_of_passage_correctly(String fileName) throws IOException, InstantiationException { - Story story = validStory(); - List<Link> expectedOpeningPassageLinks = story.getOpeningPassage().getLinks(); - - storyFileHandler.createStoryFile(story, fileName); - Story storyReadFromFile = storyFileHandler.readStoryFromFile(fileName); - List<Link> actualOpeningPassageLinks = storyReadFromFile.getOpeningPassage().getLinks(); - - Assertions.assertEquals(expectedOpeningPassageLinks, actualOpeningPassageLinks); - - File file = getValidFile(fileName); - file.delete(); - } - - @ParameterizedTest (name = "{index}. File name: {0}") - @ValueSource (strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) - void saves_all_the_actions_of_links_correctly(String fileName) throws IOException, InstantiationException { - Story story = validStory(); - List<Action<?>> expectedOpeningPassageActions = story.getOpeningPassage().getLinks().get(0).getActions(); - - storyFileHandler.createStoryFile(story, fileName); - Story storyReadFromFile = storyFileHandler.readStoryFromFile(fileName); - List<Action<?>> actualOpeningPassageActions = storyReadFromFile.getOpeningPassage().getLinks().get(0).getActions(); - - Assertions.assertEquals(expectedOpeningPassageActions, actualOpeningPassageActions); - - File file = getValidFile(fileName); - file.delete(); - } - - } - - @Nested - public class A_StoryFile_properly_reads_a_story_if_it { - @Test - void constructs_a_Story_correctly_when_read() throws IOException, InstantiationException { - Story expectedStory = validStory(); - - Story actualStory = storyFileHandler.readStoryFromFile("The Hobbit"); - - assertEquals(expectedStory, actualStory); - } - - } - - @Nested - public class A_StoryFile_with_invalid_information_such_as { - @Test - void a_null_story_when_creating_new_file_will_throw_NullPointerException(){ - Story story = null; - - Assertions.assertThrows(NullPointerException.class, () ->{ - storyFileHandler.createStoryFile(story, "Null story test"); - }); - } - - @Test - void a_null_file_name_when_creating_new_file_will_throw_NullPointerException(){ - Story story = validStory(); - - Assertions.assertThrows(NullPointerException.class, () ->{ - storyFileHandler.createStoryFile(story, null); - }); - } - - @Test - void a_null_file_name_when_reading_file_will_throw_NullPointerException(){ - Assertions.assertThrows(NullPointerException.class, () ->{ - Story story = storyFileHandler.readStoryFromFile(null); - }); - } - - //TODO: change this actually test the link information - @Test - void corrupt_link_information_throws_CorruptLinkException_when_read(){ - Story expectedStory = validStory(); - - Assertions.assertThrows(CorruptLinkException.class, () -> { - Story actualStory = storyFileHandler.readStoryFromFile("Corrupt Link File"); - assertNotEquals(expectedStory, actualStory); - }); - - } - - @Test - void file_with_improper_format_throws_CorruptFileException() { - Story expectedStory = validStory(); - - Assertions.assertThrows(CorruptFileException.class, () ->{ - Story actualStory = storyFileHandler.readStoryFromFile("Corrupt .paths Format"); - }); - } - - @Test - void not_existing_throws_IllegalArgumentException() { - Story expectedStory = validStory(); - - Assertions.assertThrows(IllegalArgumentException.class, () ->{ - Story actualStory = storyFileHandler.readStoryFromFile("File that does not exist"); - }); - } - - @Test - void action_class_throws_InstantiationException() { - Story expectedStory = validStory(); - - Assertions.assertThrows(InstantiationException.class, () -> { - Story actualStory = storyFileHandler.readStoryFromFile("Corrupt Action Class"); - }); - } - - @Test - void corrupt_action_format_throws_CorruptLinkException() { - Story expectedStory = validStory(); - - Assertions.assertThrows(CorruptLinkException.class, () -> { - Story actualStory = storyFileHandler.readStoryFromFile("Corrupt Action"); - }); - } - - @Test - void valid_action_class_but_invalid_value_throws_IllegalArgumentException() { - Story expectedStory = validStory(); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - Story actualStory = storyFileHandler.readStoryFromFile("Corrupt Action Value"); - }); - } - - } - -} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/GameTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/GameTest.java similarity index 86% rename from src/test/java/edu/ntnu/idatt2001/group_30/GameTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/GameTest.java index 803868bd70f40e034b44266d90f4849fceb71d4d..093517875e56dcadcc3b7817896dc990b3209219 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/GameTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/GameTest.java @@ -1,28 +1,28 @@ -package edu.ntnu.idatt2001.group_30; +package edu.ntnu.idatt2001.group_30.paths.model; -import edu.ntnu.idatt2001.group_30.goals.Goal; -import edu.ntnu.idatt2001.group_30.goals.GoldGoal; -import edu.ntnu.idatt2001.group_30.goals.HealthGoal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal; +import edu.ntnu.idatt2001.group_30.paths.model.goals.GoldGoal; +import edu.ntnu.idatt2001.group_30.paths.model.goals.HealthGoal; import java.util.ArrayList; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; public class GameTest { @Nested class An_instantiated_Game_object { + Game game; Player player; Passage opening; Passage attackPassage; Story story; - List<Goal> goals; + List<Goal<?>> goals; @BeforeEach void setup() { @@ -63,17 +63,17 @@ public class GameTest { @Test void can_get_player() { // The player object from the class is created in the setup method and passed to the Game's constructor - assertEquals(player, game.getPlayer()); + assertEquals(player, game.player()); } @Test void can_get_story() { - assertEquals(story, game.getStory()); + assertEquals(story, game.story()); } @Test void can_get_goals() { - assertEquals(goals, game.getGoals()); + assertEquals(goals, game.goals()); } @Test @@ -84,6 +84,5 @@ public class GameTest { assertEquals(attackPassage, game.go(link)); assertNotEquals(opening, game.go(link)); } - } } diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/LinkTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/LinkTest.java similarity index 91% rename from src/test/java/edu/ntnu/idatt2001/group_30/LinkTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/LinkTest.java index 2240c92aa0ecf96247ba9c1b789fd1c202fdadf6..5ef17fd0875fad842ce2708167bf23d5e36d756d 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/LinkTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/LinkTest.java @@ -1,13 +1,14 @@ -package edu.ntnu.idatt2001.group_30; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +package edu.ntnu.idatt2001.group_30.paths.model; import static org.junit.jupiter.api.Assertions.*; -import edu.ntnu.idatt2001.group_30.actions.GoldAction; +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.actions.GoldAction; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; class LinkTest { + @Nested class An_instantiated_Link_object { @@ -22,7 +23,7 @@ class LinkTest { void cannot_be_constructed_with_empty_text() { /* here the text is completely empty */ assertThrows(IllegalArgumentException.class, () -> new Link("", "ref")); - /* here the text contains some spaces */ + /* here the text contains some spaces */ assertThrows(IllegalArgumentException.class, () -> new Link(" ", "ref")); } @@ -60,6 +61,7 @@ class LinkTest { @Nested class Two_Link_objects { + @Test void are_equal_if_they_have_the_same_reference() { /* different text, same reference */ @@ -76,5 +78,4 @@ class LinkTest { assertNotEquals(link1, link2); } } - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/PassageTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PassageTest.java similarity index 94% rename from src/test/java/edu/ntnu/idatt2001/group_30/PassageTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PassageTest.java index 0e0896e0350c4172c4399a1079f0691ed92d4bef..6252de7ffda1f0053e1927f87f204ca4d571ce15 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/PassageTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PassageTest.java @@ -1,10 +1,12 @@ -package edu.ntnu.idatt2001.group_30; +package edu.ntnu.idatt2001.group_30.paths.model; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - public class PassageTest { @Nested diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/PlayerTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PlayerTest.java similarity index 60% rename from src/test/java/edu/ntnu/idatt2001/group_30/PlayerTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PlayerTest.java index 841511a9616440b19bf896dd8e3d41f2c67c12da..1e80da48dcc8d1721fa43546da63d87f9de9a758 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/PlayerTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PlayerTest.java @@ -1,13 +1,16 @@ -package edu.ntnu.idatt2001.group_30; +package edu.ntnu.idatt2001.group_30.paths.model; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class PlayerTest { + @Nested class An_instantiated_Player_object { + @Test void can_be_constructed() { Player player = new Player("Ola Nordmann", 1, 2, 3); @@ -17,6 +20,34 @@ class PlayerTest { assertEquals(3, player.getGold()); } + @Test + void can_be_constructed_using_builder_pattern() { + Player player = new Player.Builder("Ola Nordmann").health(10).score(2).gold(50).build(); + assertEquals("Ola Nordmann", player.getName()); + assertEquals(10, player.getHealth()); + assertEquals(2, player.getScore()); + assertEquals(50, player.getGold()); + } + + @Test + void can_be_constructed_using_builder_pattern_with_inventory() { + Player player = new Player.Builder("Ola Nordmann") + .addToInventory("Sword") + .addToInventory("Shield") + .addToInventory("Potion") + .build(); + assertEquals("Ola Nordmann", player.getName()); + /* when not specified, zero is the default value */ + assertEquals(0, player.getHealth()); + assertEquals(0, player.getScore()); + assertEquals(0, player.getGold()); + + assertEquals(3, player.getInventory().size()); + assertTrue(player.getInventory().contains("Sword")); + assertTrue(player.getInventory().contains("Shield")); + assertTrue(player.getInventory().contains("Potion")); + } + @Test void cannot_be_constructed_with_negative_health() { assertThrows(IllegalArgumentException.class, () -> new Player("Ola Nordmann", -1, 2, 3)); @@ -55,6 +86,5 @@ class PlayerTest { player.addHealth(-6); assertEquals(0, player.getHealth()); } - } } diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PlaythroughTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PlaythroughTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d90ddb67c9bdb85ec0620905778dfc576218d61b --- /dev/null +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/PlaythroughTest.java @@ -0,0 +1,46 @@ +package edu.ntnu.idatt2001.group_30.paths.model; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal; +import edu.ntnu.idatt2001.group_30.paths.model.goals.GoldGoal; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PlaythroughTest { + + private Playthrough playthrough; + private Passage openingPassage; + + @BeforeEach + void setUp() { + openingPassage = new Passage("Opening passage", "This is the opening passage"); + Player player = new Player("Player", 10, 20, 30); + Story story = new Story("My story", openingPassage); + List<Goal<?>> goals = List.of(new GoldGoal(50)); + Game game = new Game(player, story, goals); + + playthrough = new Playthrough(game); + } + + @Test + void testBeginPlaythrough() { + PlaythroughState state = playthrough.beginPlaythrough(); + assertEquals(PlaythroughState.STUCK, state); + assertEquals(openingPassage, playthrough.getCurrentPassage()); + } + + @Test + void testMakeTurn() { + playthrough.beginPlaythrough(); + Link link = new Link("Link", "Passage123"); + PlaythroughState state = playthrough.makeTurn(link); + assertEquals(PlaythroughState.STUCK, state); + } + + @Test + void testMakeTurnWithNullLink() { + assertThrows(NullPointerException.class, () -> playthrough.makeTurn(null)); + } +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/StoryTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/StoryTest.java similarity index 88% rename from src/test/java/edu/ntnu/idatt2001/group_30/StoryTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/StoryTest.java index 3889af3ddde2bdcc54bac211a2e5e67fef85d25d..592ed516ba19421c494e8a73ad7348dab3a6b85b 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/StoryTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/StoryTest.java @@ -1,11 +1,13 @@ -package edu.ntnu.idatt2001.group_30; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +package edu.ntnu.idatt2001.group_30.paths.model; import static org.junit.jupiter.api.Assertions.*; +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; public class StoryTest { @@ -14,7 +16,10 @@ public class StoryTest { @Test void can_be_constructed() { - Story story = new Story("Berachad chronicles", new Passage("Cast magic spell", "You cast a magic spell on Berachad")); + Story story = new Story( + "Berachad chronicles", + new Passage("Cast magic spell", "You cast a magic spell on Berachad") + ); assertEquals("Berachad chronicles", story.getTitle()); } @@ -35,6 +40,7 @@ public class StoryTest { @Nested class A_valid_Story_object { + @Test void can_get_all_passages() { Story story = new Story("A story of war and Eilor", new Passage("You see Eilor", "What do you do?")); @@ -68,6 +74,7 @@ public class StoryTest { @Nested class A_valid_Story_object_with_passage_data { + Story story; @BeforeEach @@ -92,7 +99,8 @@ public class StoryTest { // this means that passageOne should not be removed passageTwo.addLink(new Link("Observing the master", "You observe the master...")); story.addPassage(passageTwo); - boolean return_value = this.story.removePassage(new Link("Observing the master", "You observe the master...")); + boolean return_value = + this.story.removePassage(new Link("Observing the master", "You observe the master...")); assertFalse(return_value); } @@ -108,7 +116,5 @@ public class StoryTest { story.addPassage(passageTwo); assertEquals(1, this.story.getBrokenLinks().size()); } - } - } diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/actions/ActionFactoryTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionFactoryTest.java similarity index 74% rename from src/test/java/edu/ntnu/idatt2001/group_30/actions/ActionFactoryTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionFactoryTest.java index 5ee99bc6c32055335f078c5e4320685892abd126..639eb15dd5239cccc854b4a5ab3dd61baae815ae 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/actions/ActionFactoryTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ActionFactoryTest.java @@ -1,5 +1,6 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; +import edu.ntnu.idatt2001.group_30.paths.model.actions.*; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -10,7 +11,7 @@ class ActionFactoryTest { public class ActionFactory_with_valid_value { @Test - void can_get_GoldAction(){ + void can_get_GoldAction() { ActionType goldAction = ActionType.GOLD_ACTION; String value = "5"; Action<Integer> expectedAction = new GoldAction(5); @@ -22,7 +23,7 @@ class ActionFactoryTest { } @Test - void can_get_HealthAction(){ + void can_get_HealthAction() { ActionType healthAction = ActionType.HEALTH_ACTION; String value = "5"; Action<Integer> expectedAction = new HealthAction(5); @@ -34,7 +35,7 @@ class ActionFactoryTest { } @Test - void can_get_InventoryAction(){ + void can_get_InventoryAction() { ActionType inventoryAction = ActionType.INVENTORY_ACTION; String value = "Sword"; Action<String> expectedAction = new InventoryAction("Sword"); @@ -46,7 +47,7 @@ class ActionFactoryTest { } @Test - void can_get_ScoreAction(){ + void can_get_ScoreAction() { ActionType scoreAction = ActionType.SCORE_ACTION; String value = "5"; Action<Integer> expectedAction = new ScoreAction(5); @@ -56,23 +57,29 @@ class ActionFactoryTest { Assertions.assertTrue(actualAction instanceof ScoreAction); Assertions.assertEquals(expectedAction, actualAction); } - } @Nested public class ActionFactory_with_invalid_value_such_as { + @Test void null_action_type_throws_NullPointerException() { - Assertions.assertThrows(NullPointerException.class, () -> { - Action<?> action = ActionFactory.getAction(null, "5"); - }); + Assertions.assertThrows( + NullPointerException.class, + () -> { + Action<?> action = ActionFactory.getAction(null, "5"); + } + ); } @Test void value_throws_NumberFormatException() { - Assertions.assertThrows(IllegalArgumentException.class, () -> { - Action<?> action = ActionFactory.getAction(ActionType.GOLD_ACTION, "Invalid value"); - }); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> { + Action<?> action = ActionFactory.getAction(ActionType.GOLD_ACTION, "Invalid value"); + } + ); } } -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/actions/GoldActionTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/GoldActionTest.java similarity index 83% rename from src/test/java/edu/ntnu/idatt2001/group_30/actions/GoldActionTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/GoldActionTest.java index 5d1b75576539a1f48a06c124bea21874e7ae45d5..bc7ffd278308995fe26225ec96eafde108f5401b 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/actions/GoldActionTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/GoldActionTest.java @@ -1,33 +1,33 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; -import edu.ntnu.idatt2001.group_30.Player; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.actions.GoldAction; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class GoldActionTest { @Nested public class A_GoldAction_object { + @Test public void instantiates_properly_with_valid_argument() { GoldAction goldAction; - try{ + try { goldAction = new GoldAction(10); - } - catch (Exception e) { + } catch (Exception e) { fail(); } - } @ParameterizedTest - @ValueSource(ints = {-1, 0, 1}) + @ValueSource(ints = { -1, 0, 1 }) public void properly_adds_gold_amount_to_Player_gold(int goldAmount) { int playerStartGold = 10; GoldAction goldAction = new GoldAction(goldAmount); @@ -48,6 +48,4 @@ class GoldActionTest { assertThrows(NullPointerException.class, () -> goldAction.execute(player)); } } - - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/actions/HealthActionTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/HealthActionTest.java similarity index 83% rename from src/test/java/edu/ntnu/idatt2001/group_30/actions/HealthActionTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/HealthActionTest.java index 06f9690851ca793cacaf2cf693bfdda8727b8a85..b236d92b2d8e24e3e6f7a9d135042048107f52ec 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/actions/HealthActionTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/HealthActionTest.java @@ -1,33 +1,33 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; -import edu.ntnu.idatt2001.group_30.Player; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.actions.HealthAction; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class HealthActionTest { @Nested public class A_HealthAction_object { + @Test public void instantiates_properly_with_valid_argument() { HealthAction healthAction; - try{ + try { healthAction = new HealthAction(10); - } - catch (Exception e) { + } catch (Exception e) { fail(); } - } @ParameterizedTest - @ValueSource(ints = {-1, 0, 1}) + @ValueSource(ints = { -1, 0, 1 }) public void properly_adds_health_amount_to_Player_health(int healthAmount) { int playerStartHealth = 20; HealthAction healthAction = new HealthAction(healthAmount); @@ -48,6 +48,4 @@ class HealthActionTest { assertThrows(NullPointerException.class, () -> healthAction.execute(player)); } } - - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/actions/InventoryActionTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/InventoryActionTest.java similarity index 85% rename from src/test/java/edu/ntnu/idatt2001/group_30/actions/InventoryActionTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/InventoryActionTest.java index fb4b5c68e73ebaca7fb052022467f98d017217a7..cf67ef4b2c201d2089a58c9c9d1177a1a0cc008c 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/actions/InventoryActionTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/InventoryActionTest.java @@ -1,31 +1,29 @@ -package edu.ntnu.idatt2001.group_30.actions; - -import edu.ntnu.idatt2001.group_30.Player; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +package edu.ntnu.idatt2001.group_30.paths.model.actions; +import static org.junit.jupiter.api.Assertions.*; +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.actions.InventoryAction; import java.util.ArrayList; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; class InventoryActionTest { @Nested public class An_InventoryAction_object { + @Test public void instantiates_properly_with_valid_argument() { InventoryAction inventoryAction; String item = "Apple"; - try{ + try { inventoryAction = new InventoryAction(item); - } - catch (Exception e) { + } catch (Exception e) { fail(); } - } @Test @@ -36,7 +34,6 @@ class InventoryActionTest { InventoryAction inventoryAction = new InventoryAction(expectedItem); Player player = new Player("Trym", 10, 10, 10); - inventoryAction.execute(player); List<String> actualInventory = player.getInventory(); @@ -51,6 +48,4 @@ class InventoryActionTest { assertThrows(NullPointerException.class, () -> inventoryAction.execute(player)); } } - - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/actions/ScoreActionTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ScoreActionTest.java similarity index 83% rename from src/test/java/edu/ntnu/idatt2001/group_30/actions/ScoreActionTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ScoreActionTest.java index 8f908564e7de7113f485a7fff57974029e474c2a..de99b5469756444a5f167a26a506844bcd20f79a 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/actions/ScoreActionTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/actions/ScoreActionTest.java @@ -1,33 +1,33 @@ -package edu.ntnu.idatt2001.group_30.actions; +package edu.ntnu.idatt2001.group_30.paths.model.actions; -import edu.ntnu.idatt2001.group_30.Player; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.actions.ScoreAction; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class ScoreActionTest { @Nested public class A_ScoreAction_object { + @Test public void instantiates_properly_with_valid_argument() { ScoreAction scoreAction; - try{ + try { scoreAction = new ScoreAction(10); - } - catch (Exception e) { + } catch (Exception e) { fail(); } - } @ParameterizedTest - @ValueSource(ints = {-1, 0, 1}) + @ValueSource(ints = { -1, 0, 1 }) public void properly_adds_score_amount_to_Player_score(int scoreAmount) { int playerStartScore = 20; ScoreAction scoreAction = new ScoreAction(scoreAmount); @@ -48,5 +48,4 @@ class ScoreActionTest { assertThrows(NullPointerException.class, () -> scoreAction.execute(player)); } } - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/filehandling/FileHandlerTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/FileHandlerTest.java similarity index 58% rename from src/test/java/edu/ntnu/idatt2001/group_30/filehandling/FileHandlerTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/FileHandlerTest.java index e67958924bc2323105672d43e1e48ee73954edb9..99a97bd2a8fea3a1ee7ee786a34abea250f65f7a 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/filehandling/FileHandlerTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/FileHandlerTest.java @@ -1,6 +1,10 @@ -package edu.ntnu.idatt2001.group_30.filehandling; +package edu.ntnu.idatt2001.group_30.paths.model.filehandling; -import edu.ntnu.idatt2001.group_30.exceptions.InvalidExtensionException; +import edu.ntnu.idatt2001.group_30.paths.exceptions.InvalidExtensionException; +import edu.ntnu.idatt2001.group_30.paths.model.filehandling.FileHandler; +import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.Path; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; @@ -8,36 +12,46 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import java.io.File; -import java.nio.file.FileSystems; -import java.util.concurrent.atomic.AtomicReference; - class FileHandlerTest { @BeforeAll static void setFileHandlerPath() { - FileHandler.changeDefaultPath("src/test/resources/storytestfiles"); + Path defaultPath = FileSystems.getDefault().getPath("src", "test", "resources", "storytestfiles"); + FileHandler.changeDefaultPath(defaultPath); } @Nested public class The_FileHandler_makes_sure_a_file_name { @ParameterizedTest(name = "{index}. File name: {0}") - @ValueSource(strings = {"$123test", "50%Off", "***Story***", "LOTR?", "Winnie the Pooh!", - "LOTR > Hobbit", "The/Hobbit", "[LOTF]", "{LOTR}", "Trym's : Adventure", "Story.paths"}) + @ValueSource( + strings = { + "$123test", + "50%Off", + "***Story***", + "LOTR?", + "Winnie the Pooh!", + "LOTR > Hobbit", + "The/Hobbit", + "[LOTF]", + "{LOTR}", + "Trym's : Adventure", + "Story.paths", + } + ) void does_not_contain_special_characters(String fileName) { - String expectedExceptionMessage = "File name contains invalid characters"; + String expectedExceptionMessage = "File name contains invalid characters: "; try { FileHandler.isFileNameValid(fileName); } catch (IllegalArgumentException e) { - Assertions.assertEquals(expectedExceptionMessage, e.getMessage()); + Assertions.assertTrue(e.getMessage().contains(expectedExceptionMessage)); } } @ParameterizedTest(name = "{index}. File name: {0}") - @ValueSource(strings = {"", " "}) - void is_not_empty_or_blank(String fileName){ + @ValueSource(strings = { "", " " }) + void is_not_empty_or_blank(String fileName) { String expectedExceptionMessage = "File name cannot be blank"; try { @@ -48,7 +62,7 @@ class FileHandlerTest { } @ParameterizedTest(name = "{index}. File name: {0}") - @ValueSource(strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) void only_contains_valid_characters(String fileName) { boolean expectedStatus = true; @@ -60,11 +74,13 @@ class FileHandlerTest { @Nested public class The_FileHandler_can_check { + @Test - void if_a_file_exists(){ + void if_a_file_exists_using_File_object() { boolean expectedStatus = true; - File validFile = new File(FileSystems.getDefault() - .getPath("src", "test", "resources", "storytestfiles", "Bones") + ".paths"); + File validFile = new File( + FileSystems.getDefault().getPath("src", "test", "resources", "storytestfiles", "Bones") + ".paths" + ); boolean actualStatusOfFile = FileHandler.fileExists(validFile); @@ -72,10 +88,21 @@ class FileHandlerTest { } @Test - void if_a_file_does_not_exist(){ + void if_a_file_exists_using_file_name() { + boolean expectedStatus = true; + String fileName = "Bones"; + + boolean actualStatusOfFile = FileHandler.fileExists(fileName); + + Assertions.assertEquals(expectedStatus, actualStatusOfFile); + } + + @Test + void if_a_file_does_not_exist() { boolean expectedStatus = false; - File validFile = new File(FileSystems.getDefault() - .getPath("src", "test", "resources", "storytestfiles", "Fairy tale") + ".paths"); + File validFile = new File( + FileSystems.getDefault().getPath("src", "test", "resources", "storytestfiles", "Fairy tale") + ".paths" + ); boolean actualStatusOfFile = FileHandler.fileExists(validFile); @@ -83,7 +110,7 @@ class FileHandlerTest { } @Test - void the_file_source_path(){ + void the_file_source_path() { String expectedFileSourcePath = "src/test/resources/storytestfiles/story.paths"; String fileName = "story"; @@ -99,23 +126,25 @@ class FileHandlerTest { boolean isPathValid = FileHandler.isFileExtensionValid(validPath); Assertions.assertTrue(isPathValid); - } @ParameterizedTest(name = "{index}. File path: {0}") - @ValueSource(strings = {"Winnie the Pooh.exe", "78924378.doc", "The-Bible.txt", "Story123.csv"}) + @ValueSource(strings = { "Winnie the Pooh.exe", "78924378.doc", "The-Bible.txt", "Story123.csv" }) void if_file_extension_is_invalid(String filePath) { - - Assertions.assertThrows(InvalidExtensionException.class, () -> { - FileHandler.isFileExtensionValid(filePath); - }); + Assertions.assertThrows( + InvalidExtensionException.class, + () -> { + FileHandler.isFileExtensionValid(filePath); + } + ); } } @Nested public class The_FileHandler_can_create { + @ParameterizedTest(name = "{index}. File name: {0}") - @ValueSource(strings = {"Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123"}) + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) void new_files_with_valid_names(String fileName) { try { File file = FileHandler.createFile(fileName); @@ -129,5 +158,4 @@ class FileHandlerTest { } } } - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileReaderImplTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileReaderImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b6e2fbe034909ddc30c381bb37532a512ae8bf62 --- /dev/null +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileReaderImplTest.java @@ -0,0 +1,148 @@ +package edu.ntnu.idatt2001.group_30.paths.model.filehandling; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.exceptions.CorruptFileException; +import edu.ntnu.idatt2001.group_30.paths.exceptions.CorruptLinkException; +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.actions.GoldAction; +import edu.ntnu.idatt2001.group_30.paths.model.actions.HealthAction; +import edu.ntnu.idatt2001.group_30.paths.model.actions.InventoryAction; +import edu.ntnu.idatt2001.group_30.paths.model.actions.ScoreAction; +import java.io.*; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class StoryFileReaderImplTest { + + @BeforeAll + static void setFileHandlerPath() { + Path defaultPath = FileSystems.getDefault().getPath("src", "test", "resources", "storytestfiles"); + FileHandler.changeDefaultPath(defaultPath); + } + + StoryFileReader storyFileReader = new StoryFileReader(); + + public File getValidFile(String fileName) { + return FileHandler.createFile(fileName); + } + + static Story validStory() { + Story story = new Story("Lord of the rings", new Passage("Beginning", "Once upon a time...")); + Passage secondChapter = new Passage("The Great Barrier", "After having completed the arduous..."); + story.addPassage(secondChapter); + story.getOpeningPassage().addLink(new Link(secondChapter.getTitle(), secondChapter.getTitle())); + story.getOpeningPassage().getLinks().forEach(link -> link.addAction(new GoldAction(5))); + story.getOpeningPassage().getLinks().get(0).addAction(new ScoreAction(5)); + story.getOpeningPassage().getLinks().get(0).addAction(new HealthAction(6)); + story.getOpeningPassage().getLinks().get(0).addAction(new InventoryAction("Sword")); + return story; + } + + @Nested + public class A_StoryFile_properly_reads_a_story_if_it { + + @Test + void constructs_a_Story_correctly_when_read() throws IOException, InstantiationException { + Story expectedStory = validStory(); + + Story actualStory = storyFileReader.parse("Lord of the rings"); + + assertEquals(expectedStory, actualStory); + } + } + + @Nested + public class A_StoryFile_with_invalid_information_such_as { + + @Test + void a_null_file_name_when_reading_file_will_throw_NullPointerException() { + Assertions.assertThrows( + NullPointerException.class, + () -> { + Story story = storyFileReader.parse((String) null); + } + ); + } + + //TODO: change this actually test the link information + @Test + void corrupt_link_information_throws_CorruptLinkException_when_read() { + Story expectedStory = validStory(); + + Assertions.assertThrows( + CorruptLinkException.class, + () -> { + Story actualStory = storyFileReader.parse("Corrupt Link File"); + assertNotEquals(expectedStory, actualStory); + } + ); + } + + @Test + void file_with_improper_format_throws_CorruptFileException() { + Story expectedStory = validStory(); + + Assertions.assertThrows( + CorruptFileException.class, + () -> { + Story actualStory = storyFileReader.parse("Corrupt .paths Format"); + } + ); + } + + @Test + void not_existing_throws_IllegalArgumentException() { + Story expectedStory = validStory(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> { + Story actualStory = storyFileReader.parse("File that does not exist"); + } + ); + } + + @Test + void action_class_throws_InstantiationException() { + Story expectedStory = validStory(); + + Assertions.assertThrows( + InstantiationException.class, + () -> { + Story actualStory = storyFileReader.parse("Corrupt Action Class"); + } + ); + } + + @Test + void corrupt_action_format_throws_CorruptLinkException() { + Story expectedStory = validStory(); + + Assertions.assertThrows( + CorruptLinkException.class, + () -> { + Story actualStory = storyFileReader.parse("Corrupt Action"); + } + ); + } + + @Test + void valid_action_class_but_invalid_value_throws_IllegalArgumentException() { + Story expectedStory = validStory(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> { + Story actualStory = storyFileReader.parse("Corrupt Action Value"); + } + ); + } + } +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileWriterImplTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileWriterImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0adf5335ce2c7717b5f744bd193f3708e668d228 --- /dev/null +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/filehandling/StoryFileWriterImplTest.java @@ -0,0 +1,228 @@ +package edu.ntnu.idatt2001.group_30.paths.model.filehandling; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Link; +import edu.ntnu.idatt2001.group_30.paths.model.Passage; +import edu.ntnu.idatt2001.group_30.paths.model.Story; +import edu.ntnu.idatt2001.group_30.paths.model.actions.*; +import java.io.*; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class StoryFileWriterImplTest { + + final StoryFileWriter storyFileWriter = new StoryFileWriter(); + final StoryFileReader storyFileReader = new StoryFileReader(); + + @BeforeAll + static void setFileHandlerPath() { + Path defaultPath = FileSystems.getDefault().getPath("src", "test", "resources", "storytestfiles"); + FileHandler.changeDefaultPath(defaultPath); + } + + public File getValidFile(String fileName) { + return FileHandler.createFile(fileName); + } + + static Story validStory() { + Story story = new Story("The Hobbit", new Passage("Beginning", "Once upon a time...")); + Passage secondChapter = new Passage("The Great Barrier", "After having completed the arduous..."); + story.addPassage(secondChapter); + story.getOpeningPassage().addLink(new Link(secondChapter.getTitle(), secondChapter.getTitle())); + story.getOpeningPassage().getLinks().forEach(link -> link.addAction(new GoldAction(5))); + story.getOpeningPassage().getLinks().get(0).addAction(new ScoreAction(5)); + story.getOpeningPassage().getLinks().get(0).addAction(new HealthAction(6)); + story.getOpeningPassage().getLinks().get(0).addAction(new InventoryAction("Sword")); + return story; + } + + @Nested + public class A_StoryFile_is_valid_if { + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void a_file_has_a_valid_name(String fileName) { + Story story = validStory(); + + try { + storyFileWriter.create(story, fileName); + } catch (Exception e) { + if ( + !( + e.getMessage().equals("You cannot overwrite a pre-existing story file") && + e instanceof FileAlreadyExistsException + ) + ) { + System.out.println(e.getMessage()); + fail("An exception was thrown when it shouldn't have."); + } + } + + File expectedFileCreated = getValidFile(fileName); + Assertions.assertTrue(expectedFileCreated.isFile()); + expectedFileCreated.delete(); + } + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void files_created_can_be_accessed_and_read(String fileName) { + Story story = validStory(); + + try { + storyFileWriter.create(story, fileName); + } catch (Exception e) { + fail("An exception was thrown when it shouldn't have. " + e.getMessage()); + } + + File expectedFileCreated = getValidFile(fileName); + Assertions.assertTrue(expectedFileCreated.canRead()); + expectedFileCreated.delete(); + } + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void the_pathing_is_correctly_set(String fileName) { + Story story = validStory(); + boolean fileDoesNotExistAtStart = !getValidFile(fileName).exists(); + + try { + storyFileWriter.create(story, fileName); + } catch (Exception e) { + fail("An exception was thrown when it shouldn't have."); + } + + File expectedFileCreated = getValidFile(fileName); + boolean fileDoesExistAfterWrite = expectedFileCreated.exists(); + + //Then/Assert + Assertions.assertTrue(fileDoesNotExistAtStart); + Assertions.assertTrue(fileDoesExistAfterWrite); + expectedFileCreated.delete(); + } + + @Test + void it_cannot_create_new_file_with_preexisting_file_name() { + Story story = validStory(); + String fileName = "Bones"; + + File preexistingFile = getValidFile(fileName); + if (getValidFile(fileName).isFile()) { + Assertions.assertThrows( + FileAlreadyExistsException.class, + () -> storyFileWriter.create(story, fileName) + ); + } else fail("The file check for doesn't exist, so this test is invalid"); + } + } + + @Nested + public class A_StoryFile_properly_writes_a_story_to_new_file_if_it { + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void saves_the_story_title_correctly(String fileName) throws IOException, InstantiationException { + Story story = validStory(); + String expectedTitle = story.getTitle(); + + storyFileWriter.create(story, fileName); + Story storyReadFromFile = storyFileReader.parse(fileName); + String actualTitle = storyReadFromFile.getTitle(); + + Assertions.assertEquals(expectedTitle, actualTitle); + + File file = getValidFile(fileName); + file.delete(); + } + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void saves_the_opening_passage_after_title(String fileName) throws IOException, InstantiationException { + Story story = validStory(); + Passage expectedOpeningPassage = story.getOpeningPassage(); + + storyFileWriter.create(story, fileName); + Story storyReadFromFile = storyFileReader.parse(fileName); + Passage actualOpeningPassage = storyReadFromFile.getOpeningPassage(); + System.out.println(actualOpeningPassage.getTitle() + actualOpeningPassage.getContent()); + System.out.println(expectedOpeningPassage.getTitle() + expectedOpeningPassage.getContent()); + + Assertions.assertEquals(expectedOpeningPassage, actualOpeningPassage); + + File file = getValidFile(fileName); + file.delete(); + } + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void saves_all_the_links_of_passage_correctly(String fileName) throws IOException, InstantiationException { + Story story = validStory(); + List<Link> expectedOpeningPassageLinks = story.getOpeningPassage().getLinks(); + + storyFileWriter.create(story, fileName); + Story storyReadFromFile = storyFileReader.parse(fileName); + List<Link> actualOpeningPassageLinks = storyReadFromFile.getOpeningPassage().getLinks(); + + Assertions.assertEquals(expectedOpeningPassageLinks, actualOpeningPassageLinks); + + File file = getValidFile(fileName); + file.delete(); + } + + @ParameterizedTest(name = "{index}. File name: {0}") + @ValueSource(strings = { "Winnie the Pooh", "L.O.T.R", "The-Bible", "Story123" }) + void saves_all_the_actions_of_links_correctly(String fileName) throws IOException, InstantiationException { + Story story = validStory(); + List<Action<?>> expectedOpeningPassageActions = story.getOpeningPassage().getLinks().get(0).getActions(); + + storyFileWriter.create(story, fileName); + Story storyReadFromFile = storyFileReader.parse(fileName); + List<Action<?>> actualOpeningPassageActions = storyReadFromFile + .getOpeningPassage() + .getLinks() + .get(0) + .getActions(); + + Assertions.assertEquals(expectedOpeningPassageActions, actualOpeningPassageActions); + + File file = getValidFile(fileName); + file.delete(); + } + } + + @Nested + public class A_StoryFile_with_invalid_information_such_as { + + @Test + void a_null_story_when_creating_new_file_will_throw_NullPointerException() { + Story story = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> { + storyFileWriter.create(story, "Null story test"); + } + ); + } + + @Test + void a_null_file_name_when_creating_new_file_will_throw_NullPointerException() { + Story story = validStory(); + + Assertions.assertThrows( + NullPointerException.class, + () -> { + storyFileWriter.create(story, (String) null); + } + ); + } + } +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalFactoryTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b46ce4037943d8fce3dd402673df3c3d02934ab7 --- /dev/null +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoalFactoryTest.java @@ -0,0 +1,62 @@ +package edu.ntnu.idatt2001.group_30.paths.model.goals; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class GoalFactoryTest { + + /* + * this is a factory class, so the typical beforeEach arrange step is not needed + */ + + @ParameterizedTest + @MethodSource("validGoalTypeAndValueProvider") + void getGoal_withValidGoalTypeAndValue_returnsGoalObject() { + GoalType goalType = GoalType.GOLD_GOAL; + Object goalValue = "100"; + + Goal<?> goal = GoalFactory.getGoal(goalType, goalValue); + + assertEquals(GoldGoal.class, goal.getClass()); + } + + private static Stream<Arguments> validGoalTypeAndValueProvider() { + return Stream.of( + Arguments.of(GoalType.GOLD_GOAL, "100", GoldGoal.class), + Arguments.of(GoalType.HEALTH_GOAL, "50", HealthGoal.class), + Arguments.of(GoalType.SCORE_GOAL, "500", ScoreGoal.class) + ); + } + + @ParameterizedTest + @MethodSource("invalidGoalTypeAndValueProvider") + void getGoal_withInvalidGoalValue_throwsIllegalArgumentException() { + GoalType goalType = GoalType.GOLD_GOAL; + Object goalValue = "invalid_value"; + + assertThrows(IllegalArgumentException.class, () -> GoalFactory.getGoal(goalType, goalValue)); + } + + private static Stream<Arguments> invalidGoalTypeAndValueProvider() { + return Stream.of( + Arguments.of(GoalType.GOLD_GOAL, "invalid", GoldGoal.class), + Arguments.of(GoalType.HEALTH_GOAL, "-1", HealthGoal.class), + Arguments.of(GoalType.SCORE_GOAL, "", ScoreGoal.class) + ); + } + + @Test + void getGoal_withValidInventoryGoalValue_returnsInventoryGoalObject() { + GoalType goalType = GoalType.INVENTORY_GOAL; + Object goalValue = "item"; + + Goal<?> goal = GoalFactory.getGoal(goalType, goalValue); + + assertEquals(InventoryGoal.class, goal.getClass()); + } +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/goals/GoldGoalTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoldGoalTest.java similarity index 84% rename from src/test/java/edu/ntnu/idatt2001/group_30/goals/GoldGoalTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoldGoalTest.java index 0d43166350362167f6e03651652954fc2b3f6c9e..03b44d3d656afd9879b518b186deb75170da84dc 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/goals/GoldGoalTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/GoldGoalTest.java @@ -1,13 +1,14 @@ -package edu.ntnu.idatt2001.group_30.goals; +package edu.ntnu.idatt2001.group_30.paths.model.goals; -import edu.ntnu.idatt2001.group_30.Player; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.goals.GoldGoal; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class GoldGoalTest { @Nested @@ -17,16 +18,15 @@ class GoldGoalTest { public void can_properly_be_instantiated() { GoldGoal goldGoal; - try{ + try { goldGoal = new GoldGoal(10); - } - catch(Exception e) { + } catch (Exception e) { fail(); } } @ParameterizedTest - @ValueSource(ints = {20, 21}) + @ValueSource(ints = { 20, 21 }) public void returns_true_when_Player_has_more_than_or_equal_to_min_gold(int playerGold) { int minGoldAmount = 20; GoldGoal goldGoal = new GoldGoal(minGoldAmount); @@ -39,7 +39,7 @@ class GoldGoalTest { } @ParameterizedTest - @ValueSource(ints = {0, 9}) + @ValueSource(ints = { 0, 9 }) public void returns_false_when_Player_has_less_than_min_gold(int playerGold) { int minGoldAmount = 10; GoldGoal goldGoal = new GoldGoal(minGoldAmount); @@ -59,6 +59,4 @@ class GoldGoalTest { assertThrows(NullPointerException.class, () -> goldGoal.isFulfilled(player)); } } - - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/goals/HealthGoalTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/HealthGoalTest.java similarity index 84% rename from src/test/java/edu/ntnu/idatt2001/group_30/goals/HealthGoalTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/HealthGoalTest.java index da55bf4f2fcf31efe661fc54ae150cfae1b88496..7d2356ae89a4b0e8ae8af076b370fe92ee675c52 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/goals/HealthGoalTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/HealthGoalTest.java @@ -1,13 +1,14 @@ -package edu.ntnu.idatt2001.group_30.goals; +package edu.ntnu.idatt2001.group_30.paths.model.goals; -import edu.ntnu.idatt2001.group_30.Player; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.goals.HealthGoal; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class HealthGoalTest { @Nested @@ -17,16 +18,15 @@ class HealthGoalTest { public void can_properly_be_instantiated() { HealthGoal healthGoal; - try{ + try { healthGoal = new HealthGoal(10); - } - catch(Exception e) { + } catch (Exception e) { fail(); } } @ParameterizedTest - @ValueSource(ints = {20, 21}) + @ValueSource(ints = { 20, 21 }) public void returns_true_when_Player_has_more_than_or_equal_to_min_health(int playerHealth) { int minHealthAmount = 20; HealthGoal healthGoal = new HealthGoal(minHealthAmount); @@ -39,7 +39,7 @@ class HealthGoalTest { } @ParameterizedTest - @ValueSource(ints = {0, 19}) + @ValueSource(ints = { 0, 19 }) public void returns_false_when_Player_has_less_than_min_health(int playerHealth) { int minHealthAmount = 20; HealthGoal healthGoal = new HealthGoal(minHealthAmount); @@ -64,15 +64,11 @@ class HealthGoalTest { int health = -5; HealthGoal healthGoal; - try{ + try { healthGoal = new HealthGoal(health); - } - catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { assertTrue(true); } - - } } - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/goals/InventoryGoalTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/InventoryGoalTest.java similarity index 77% rename from src/test/java/edu/ntnu/idatt2001/group_30/goals/InventoryGoalTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/InventoryGoalTest.java index 177f4756a96b4a39e71bc264b5aac63c4b6eff4b..a97a096a9d0068297c9dbada004bea3880b0640b 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/goals/InventoryGoalTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/InventoryGoalTest.java @@ -1,13 +1,13 @@ -package edu.ntnu.idatt2001.group_30.goals; +package edu.ntnu.idatt2001.group_30.paths.model.goals; -import edu.ntnu.idatt2001.group_30.Player; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.goals.InventoryGoal; import java.util.ArrayList; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; class InventoryGoalTest { @@ -24,10 +24,9 @@ class InventoryGoalTest { @Test public void can_properly_be_instantiated() { InventoryGoal inventoryGoal; - try{ + try { inventoryGoal = new InventoryGoal(getMandatoryInventory()); - } - catch(Exception e) { + } catch (Exception e) { fail(); } } @@ -64,6 +63,13 @@ class InventoryGoalTest { assertThrows(NullPointerException.class, () -> inventoryGoal.isFulfilled(player)); } - } -} \ No newline at end of file + @Test + public void can_concatenate_two_InventoryGoals() { + InventoryGoal inventoryGoal1 = new InventoryGoal(getMandatoryInventory()); + InventoryGoal inventoryGoal2 = new InventoryGoal(getMandatoryInventory()); + inventoryGoal1.concatGoals(inventoryGoal2); + assertEquals(4, inventoryGoal1.getGoalValue().size()); + } + } +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/goals/ScoreGoalTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/ScoreGoalTest.java similarity index 87% rename from src/test/java/edu/ntnu/idatt2001/group_30/goals/ScoreGoalTest.java rename to src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/ScoreGoalTest.java index a5c5008a5152bbbbc220f82a4b13d242e00e642c..a85c45bfc2d2e212e2912df554dfd2ca62c07686 100644 --- a/src/test/java/edu/ntnu/idatt2001/group_30/goals/ScoreGoalTest.java +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/goals/ScoreGoalTest.java @@ -1,13 +1,14 @@ -package edu.ntnu.idatt2001.group_30.goals; +package edu.ntnu.idatt2001.group_30.paths.model.goals; -import edu.ntnu.idatt2001.group_30.Player; +import static org.junit.jupiter.api.Assertions.*; + +import edu.ntnu.idatt2001.group_30.paths.model.Player; +import edu.ntnu.idatt2001.group_30.paths.model.goals.ScoreGoal; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static org.junit.jupiter.api.Assertions.*; - class ScoreGoalTest { @Nested @@ -25,7 +26,7 @@ class ScoreGoalTest { } @ParameterizedTest - @ValueSource(ints = {20, 21}) + @ValueSource(ints = { 20, 21 }) public void returns_true_when_Player_has_more_than_or_equal_to_min_score(int playerScore) { int minScoreAmount = 20; ScoreGoal scoreGoal = new ScoreGoal(minScoreAmount); @@ -38,7 +39,7 @@ class ScoreGoalTest { } @ParameterizedTest - @ValueSource(ints = {0, 19}) + @ValueSource(ints = { 0, 19 }) public void returns_false_when_Player_has_less_than_min_score(int playerScore) { int minScoreAmount = 20; ScoreGoal scoreGoal = new ScoreGoal(minScoreAmount); @@ -57,7 +58,5 @@ class ScoreGoalTest { assertThrows(NullPointerException.class, () -> scoreGoal.isFulfilled(player)); } - } - -} \ No newline at end of file +} diff --git a/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/utils/TextValidationTest.java b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/utils/TextValidationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..17bc6bbd6f4ec608f511cd5f27ab7311e93ed21b --- /dev/null +++ b/src/test/java/edu/ntnu/idatt2001/group_30/paths/model/utils/TextValidationTest.java @@ -0,0 +1,26 @@ +package edu.ntnu.idatt2001.group_30.paths.model.utils; + +import javafx.scene.control.TextFormatter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TextValidationTest { + + @Test + void createIntegerTextFormatter_should_return_formatter_with_default_start_value() { + int expectedStartValue = 100; + + TextFormatter<Integer> formatter = TextValidation.createIntegerTextFormatter(); + + Assertions.assertEquals(expectedStartValue, formatter.getValue()); + } + + @Test + void createIntegerTextFormatter_should_return_formatter_with_custom_start_value() { + int startValue = 50; + + TextFormatter<Integer> formatter = TextValidation.createIntegerTextFormatter(startValue); + + Assertions.assertEquals(startValue, formatter.getValue()); + } +} diff --git a/src/test/resources/storytestfiles/The Hobbit.paths b/src/test/resources/storytestfiles/Lord of the rings.paths similarity index 92% rename from src/test/resources/storytestfiles/The Hobbit.paths rename to src/test/resources/storytestfiles/Lord of the rings.paths index 51531969e0ba640f31514f0485bf262f1d2b2502..7bf88a1183e1b3e1a51f556df176f2e523280691 100644 --- a/src/test/resources/storytestfiles/The Hobbit.paths +++ b/src/test/resources/storytestfiles/Lord of the rings.paths @@ -1,4 +1,4 @@ -The Hobbit +Lord of the rings ::Beginning Once upon a time...