Explorar el Código

Merge branch 'dev' into 'main'

1.0.0 release?

See merge request FIS-VSE/4IT115/2025ZS/ct1800/scho07/gui-textova-adventura!1
Ondřej Schlaichert hace 8 meses
padre
commit
4671225b14
Se han modificado 28 ficheros con 1168 adiciones y 64 borrados
  1. 10 0
      .gitlab-ci.yml
  2. 4 4
      pom.xml
  3. 61 0
      src/main/java/eu/oschl/gui/ActionProcessor.java
  4. 7 0
      src/main/java/eu/oschl/gui/ButtonData.java
  5. 187 0
      src/main/java/eu/oschl/gui/GameController.java
  6. 0 19
      src/main/java/eu/oschl/gui/HelloApplication.java
  7. 0 14
      src/main/java/eu/oschl/gui/HelloController.java
  8. 0 9
      src/main/java/eu/oschl/gui/Launcher.java
  9. 18 0
      src/main/java/eu/oschl/gui/Observable.java
  10. 5 0
      src/main/java/eu/oschl/gui/Observer.java
  11. 48 0
      src/main/java/eu/oschl/gui/Output.java
  12. 44 0
      src/main/java/eu/oschl/gui/Session.java
  13. 36 0
      src/main/java/eu/oschl/gui/actions/Action.java
  14. 96 0
      src/main/java/eu/oschl/gui/actions/Enter.java
  15. 106 0
      src/main/java/eu/oschl/gui/actions/Explore.java
  16. 56 0
      src/main/java/eu/oschl/gui/actions/PressButton.java
  17. 73 0
      src/main/java/eu/oschl/gui/actions/Slay.java
  18. 72 0
      src/main/java/eu/oschl/gui/actions/TakeItem.java
  19. 56 0
      src/main/java/eu/oschl/gui/actions/UseItem.java
  20. 24 0
      src/main/java/eu/oschl/gui/exceptions/InvalidActionId.java
  21. 31 0
      src/main/java/eu/oschl/schmorn/Main.java
  22. 1 1
      src/main/java/eu/oschl/schmorn/Setup.java
  23. 0 1
      src/main/java/module-info.java
  24. 154 0
      src/main/resources/eu/oschl/gui/css/style.css
  25. BIN
      src/main/resources/eu/oschl/gui/fonts/SGr-IosevkaSlab-Regular.ttc
  26. 79 0
      src/main/resources/eu/oschl/gui/game.fxml
  27. 0 16
      src/main/resources/eu/oschl/gui/hello-view.fxml
  28. BIN
      src/main/resources/eu/oschl/gui/images/rooms/well.png

+ 10 - 0
.gitlab-ci.yml

@@ -0,0 +1,10 @@
+image: maven:3.9.11-eclipse-temurin-25
+
+build:
+  script:
+    - "mvn javafx:jlink -B"
+  artifacts:
+    paths:
+      - "target/*.zip"
+  only:
+    - main

+ 4 - 4
pom.xml

@@ -60,10 +60,10 @@
                         <!-- Default configuration for running with: mvn clean javafx:run -->
                         <id>default-cli</id>
                         <configuration>
-                            <mainClass>eu.oschl.schmorngui/eu.oschl.gui.HelloApplication</mainClass>
-                            <launcher>app</launcher>
-                            <jlinkZipName>app</jlinkZipName>
-                            <jlinkImageName>app</jlinkImageName>
+                            <mainClass>eu.oschl.schmorn.Main</mainClass>
+                            <launcher>schmorn</launcher>
+                            <jlinkZipName>schmorn</jlinkZipName>
+                            <jlinkImageName>schmorn</jlinkImageName>
                             <noManPages>true</noManPages>
                             <stripDebug>true</stripDebug>
                             <noHeaderFiles>true</noHeaderFiles>

+ 61 - 0
src/main/java/eu/oschl/gui/ActionProcessor.java

@@ -0,0 +1,61 @@
+package eu.oschl.gui;
+
+import eu.oschl.gui.actions.*;
+import eu.oschl.gui.exceptions.InvalidActionId;
+import eu.oschl.textadventure.Game;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ActionProcessor extends Observable {
+    private final ArrayList<Action> actions;
+
+    public ActionProcessor(ArrayList<Action> actions) {
+        this.actions = actions;
+    }
+
+    public ArrayList<Action> getActions() {
+        return actions;
+    }
+
+    /**
+     * Factory method to create a ActionProcessor with a predefined set of Actions.
+     *
+     * @param game the game instance to be used by the actions
+     * @return a new ActionProcessor instance with initialized actions
+     */
+    public static ActionProcessor create(Game game) {
+        ArrayList<Action> actions = new ArrayList<>(List.of(
+            new Explore(game),
+            new Slay(game),
+            new PressButton(game),
+            new TakeItem(game),
+            new Enter(game),
+            new UseItem(game)
+        ));
+
+        return new ActionProcessor(actions);
+    }
+
+    /**
+     * Executes the action of the provided ID and arguments.
+     *
+     * @param id the action ID
+     * @throws InvalidActionId if the action ID is not found or is invalid
+     */
+    public void executeAction(String id, String[] args) throws InvalidActionId {
+        Output.clear();
+
+        for (Action action : actions) {
+            if (action.getId().equals(id)) {
+                action.execute(args);
+
+                sendEvent();
+
+                return;
+            }
+        }
+
+        throw new InvalidActionId("Invalid action ID");
+    }
+}

+ 7 - 0
src/main/java/eu/oschl/gui/ButtonData.java

@@ -0,0 +1,7 @@
+package eu.oschl.gui;
+
+public record ButtonData(
+        String actionId,
+        String... arguments
+) {
+}

+ 187 - 0
src/main/java/eu/oschl/gui/GameController.java

@@ -0,0 +1,187 @@
+package eu.oschl.gui;
+
+import eu.oschl.gui.actions.*;
+import eu.oschl.gui.exceptions.InvalidActionId;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.TextFlow;
+
+import java.util.ArrayList;
+
+public class GameController implements Observer {
+    @FXML
+    private TextFlow output;
+
+    @FXML
+    private VBox inputButtonContainer;
+
+    @FXML
+    private VBox inventoryContainer;
+
+    @FXML
+    public void initialize() {
+        Output.setOutputElement(output);
+        Session.getActionProcessor().register(this);
+        update();
+
+        Output.print(Session.getGame().getPrologue());
+    }
+
+    @FXML
+    private void exectuteAction(ActionEvent event) throws InvalidActionId {
+        Button clickedButton = (Button) event.getSource();
+        ButtonData buttonData = (ButtonData) clickedButton.getUserData();
+
+        var actionProcessor = Session.getActionProcessor();
+
+        actionProcessor.executeAction(buttonData.actionId(), buttonData.arguments());
+    }
+
+    @Override
+    public void update() {
+        if (!Session.getGame().isRunning()) {
+            finishGame();
+            return;
+        }
+
+        renderActionButtons();
+        renderInventoryItems();
+    }
+
+    private void renderActionButtons() {
+        inputButtonContainer.getChildren().clear();
+
+        var newButtons = new ArrayList<Button>();
+
+        for (Action action : Session.getActionProcessor().getActions()) {
+            switch (action) {
+                case Enter enter -> {
+                    var room = Session.getGame().getCurrentRoom();
+                    var passages = room.getPassages();
+
+                    for (var passage : passages) {
+                        var label = passage.isSeeThrough() || passage.getOtherRoom(room).wasEntered()
+                                ? enter.getName() + " " + passage.getName() + " (>> " + passage.getOtherRoom(Session.getGame().getCurrentRoom()).getName() + ")"
+                                : enter.getName() + " " + passage.getName() + " (>> ???)";
+                        var buttonData = new ButtonData(enter.getId(), passage.getName());
+
+                        var button = createActionButton(label, buttonData);
+
+                        button.getStyleClass().add("enter-button");
+
+                        newButtons.add(button);
+                    }
+                }
+                case PressButton pressButton -> {
+                    if (Session.getGame().getCurrentRoom().isBlockedByEnemy()) {
+                        continue;
+                    }
+
+                    var objects = Session.getGame().getCurrentRoom().getObjects();
+
+                    for (var object : objects) {
+                        if (object instanceof eu.oschl.textadventure.objects.Button) {
+                            var label = pressButton.getName() + " " + object.getName();
+                            var buttonData = new ButtonData(pressButton.getId(), object.getName());
+
+                            var button = createActionButton(label, buttonData);
+
+                            button.getStyleClass().add("press-button");
+
+                            newButtons.add(button);
+                        }
+                    }
+                }
+                case Slay slay -> {
+                    var enemy = Session.getGame().getCurrentRoom().getEnemy();
+
+                    if (enemy.isPresent() && enemy.get().isAlive()) {
+                        var label = slay.getName() + " " + enemy.get().getName();
+                        var buttonData = new ButtonData(slay.getId(), enemy.get().getName());
+
+                        var button = createActionButton(label, buttonData);
+
+                        button.getStyleClass().add("slay-button");
+
+                        newButtons.add(button);
+                    }
+                }
+                case TakeItem takeItem -> {
+                    if (Session.getGame().getCurrentRoom().isBlockedByEnemy()) {
+                        continue;
+                    }
+
+                    var objects = Session.getGame().getCurrentRoom().getObjects();
+
+                    for (var object : objects) {
+                        if (object instanceof eu.oschl.textadventure.objects.PickableObject) {
+                            var label = takeItem.getName() + " " + object.getName();
+                            var buttonData = new ButtonData(takeItem.getId(), object.getName());
+
+                            var button = createActionButton(label, buttonData);
+
+                            button.getStyleClass().add("take-button");
+
+                            newButtons.add(button);
+                        }
+                    }
+                }
+                case UseItem _ -> {
+                }
+                default -> newButtons.add(createActionButton(action.getName(), new ButtonData(action.getId())));
+            }
+        }
+
+        inputButtonContainer.getChildren().addAll(newButtons);
+    }
+
+    private void renderInventoryItems() {
+        inventoryContainer.getChildren().clear();
+
+        var newItems = new ArrayList<Button>();
+
+        for (var item : Session.getGame().getInventory().getItems()) {
+            var label = item.getName();
+            var buttonData = new ButtonData("useitem", item.getName());
+
+            var button = createActionButton(label, buttonData);
+            button.getStyleClass().add("inventory-item");
+
+            newItems.add(button);
+        }
+
+        if (Session.getGame().getInventory().getWeapon().isPresent()) {
+            var weapon = Session.getGame().getInventory().getWeapon().get();
+
+            var button = createActionButton(weapon.getName(), new ButtonData(""));
+            button.getStyleClass().add("inventory-item");
+            button.getStyleClass().add("weapon");
+            button.setDisable(true);
+
+            newItems.add(button);
+        }
+
+        inventoryContainer.getChildren().addAll(newItems);
+    }
+
+    private Button createActionButton(String label, ButtonData buttonData) {
+        Button actionButton = new Button(label);
+
+        actionButton.setUserData(buttonData);
+        actionButton.setOnAction(this::exectuteAction);
+        actionButton.setMaxWidth(Double.MAX_VALUE);
+
+        return actionButton;
+    }
+
+    private void finishGame() {
+        inputButtonContainer.getChildren().clear();
+        inventoryContainer.getChildren().clear();
+
+        Output.printLine();
+        Output.printLine();
+        Output.print(Session.getGame().getEpilogue());
+    }
+}

+ 0 - 19
src/main/java/eu/oschl/gui/HelloApplication.java

@@ -1,19 +0,0 @@
-package eu.oschl.gui;
-
-import javafx.application.Application;
-import javafx.fxml.FXMLLoader;
-import javafx.scene.Scene;
-import javafx.stage.Stage;
-
-import java.io.IOException;
-
-public class HelloApplication extends Application {
-    @Override
-    public void start(Stage stage) throws IOException {
-        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
-        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
-        stage.setTitle("Hello!");
-        stage.setScene(scene);
-        stage.show();
-    }
-}

+ 0 - 14
src/main/java/eu/oschl/gui/HelloController.java

@@ -1,14 +0,0 @@
-package eu.oschl.gui;
-
-import javafx.fxml.FXML;
-import javafx.scene.control.Label;
-
-public class HelloController {
-    @FXML
-    private Label welcomeText;
-
-    @FXML
-    protected void onHelloButtonClick() {
-        welcomeText.setText("Welcome to JavaFX Application!");
-    }
-}

+ 0 - 9
src/main/java/eu/oschl/gui/Launcher.java

@@ -1,9 +0,0 @@
-package eu.oschl.gui;
-
-import javafx.application.Application;
-
-public class Launcher {
-    public static void main(String[] args) {
-        Application.launch(HelloApplication.class, args);
-    }
-}

+ 18 - 0
src/main/java/eu/oschl/gui/Observable.java

@@ -0,0 +1,18 @@
+package eu.oschl.gui;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public abstract class Observable {
+    protected Set<Observer> observers = new HashSet<>();
+
+    public void register(Observer observer) {
+        observers.add(observer);
+    }
+
+    public void sendEvent() {
+        for (Observer observer : observers) {
+            observer.update();
+        }
+    }
+}

+ 5 - 0
src/main/java/eu/oschl/gui/Observer.java

@@ -0,0 +1,5 @@
+package eu.oschl.gui;
+
+public interface Observer {
+    void update();
+}

+ 48 - 0
src/main/java/eu/oschl/gui/Output.java

@@ -0,0 +1,48 @@
+package eu.oschl.gui;
+
+import javafx.scene.paint.Color;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+
+public class Output {
+    private static TextFlow outputElement;
+
+    public static void setOutputElement(TextFlow outputElement) {
+        Output.outputElement = outputElement;
+    }
+
+    /**
+     * Prints a message to the output with the specified JavaFX color.
+     *
+     * @param message the message to print
+     * @param color the color in which to print the message
+     */
+    public static void print(String message, Color color) {
+        var text = new Text(message);
+        text.setFill(color);
+        outputElement.getChildren().add(text);
+    }
+
+    /**
+     * Prints a message to the output in the default color.
+     *
+     * @param message the message to print
+     */
+    public static void print(String message) {
+        print(message, Color.WHITE);
+    }
+
+    /**
+     * Prints a line break to the console.
+     */
+    public static void printLine() {
+        print("\n");
+    }
+
+    /**
+     * Clears the output element.
+     */
+    public static void clear() {
+        outputElement.getChildren().clear();
+    }
+}

+ 44 - 0
src/main/java/eu/oschl/gui/Session.java

@@ -0,0 +1,44 @@
+package eu.oschl.gui;
+
+import eu.oschl.textadventure.Game;
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class Session extends Application {
+    private static Game game;
+    private static ActionProcessor actionProcessor;
+
+    public static Game getGame() {
+        return game;
+    }
+
+    public static ActionProcessor getActionProcessor() {
+        return actionProcessor;
+    }
+
+    public static void launchGui(Game game, String[] args)
+    {
+        Session.game = game;
+        Session.actionProcessor = ActionProcessor.create(game);
+
+        Application.launch(Session.class, args);
+    }
+
+    @Override
+    public void start(Stage stage) throws IOException {
+        FXMLLoader fxmlLoader = new FXMLLoader(Session.class.getResource("game.fxml"));
+        Scene scene = new Scene(fxmlLoader.load(), 1280, 720);
+
+        String cssPath = Objects.requireNonNull(Session.class.getResource("css/style.css")).toExternalForm();
+        scene.getStylesheets().add(cssPath);
+
+        stage.setTitle("Schmorn");
+        stage.setScene(scene);
+        stage.show();
+    }
+}

+ 36 - 0
src/main/java/eu/oschl/gui/actions/Action.java

@@ -0,0 +1,36 @@
+package eu.oschl.gui.actions;
+
+/***
+ * This interface defines the structure for commands in the console application.
+ *
+ * @author Ondřej Schlaichert
+ */
+public interface Action {
+    /**
+     * Returns ID of the action.
+     *
+     * @return a string ID
+     */
+    String getId();
+
+    /**
+     * Returns name of the action.
+     *
+     * @return a string name
+     */
+    String getName();
+
+    /**
+     * Returns a description of what the action does.
+     *
+     * @return a string description of the action
+     */
+    String getDescription();
+
+    /**
+     * Executes the action with the provided arguments.
+     *
+     * @param args an array of strings representing the action arguments
+     */
+    void execute(String[] args);
+}

+ 96 - 0
src/main/java/eu/oschl/gui/actions/Enter.java

@@ -0,0 +1,96 @@
+package eu.oschl.gui.actions;
+
+import eu.oschl.gui.Output;
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.exceptions.InvalidGameState;
+import eu.oschl.textadventure.map.Passage;
+import javafx.scene.paint.Color;
+
+public class Enter implements Action {
+    private final Game game;
+
+    public Enter(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String getId() {
+        return "enter";
+    }
+
+    @Override
+    public String getName() {
+        return "Enter";
+    }
+
+    @Override
+    public String getDescription() {
+        return "walk through a passage";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        String input = String.join(" ", args);
+
+        var passages = this.game.getCurrentRoom().getPassages();
+        Passage passage = null;
+
+        for (Passage p : passages) {
+            if (p.getName().equalsIgnoreCase(input)) {
+                passage = p;
+                break;
+            }
+        }
+
+        var printEnterText = !passage.getOtherRoom(game.getCurrentRoom()).wasEntered();
+
+        var result = passage.passThrough(false);
+
+        if (!result) {
+            if (game.getCurrentRoom().getEnemy().isPresent()) {
+                Output.print(
+                        game.getCurrentRoom().getEnemy().get().getName() + " blocks the way. It is only possible to go back.",
+                        Color.RED
+                );
+            } else if (passage.getBlockage().isPresent()) {
+                Output.print(passage.getBlockage().get().getDescription(), Color.RED);
+            } else {
+                Output.print("The passage is blocked.", Color.RED);
+            }
+            return;
+        }
+
+        printSuccessfulPassage(printEnterText);
+    }
+
+    /**
+     * Prints a message indicating that the player has successfully passed through a passage.
+     *
+     * @param printEnterText whether to print the enter text of the current room
+     */
+    private void printSuccessfulPassage(boolean printEnterText) {
+        if (game.getLastPassage().isEmpty()) {
+            throw new InvalidGameState("Last passages stack is empty even though a passage was passed");
+        }
+
+        Output.print("Passed through the ");
+        Output.print(game.getLastPassage().get().getName(), Color.YELLOW);
+        Output.print(" and entered ");
+        Output.print(game.getCurrentRoom().getName(), Color.LIGHTBLUE);
+        Output.print(".");
+
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            Output.printLine();
+            Output.print("...", Color.WHITE);
+            Output.printLine();
+            Output.print("There is somebody in here.", Color.WHITE);
+        }
+
+        if (printEnterText && game.getCurrentRoom().getEnterText().isPresent()) {
+            Output.printLine();
+            Output.printLine();
+            Output.print(game.getCurrentRoom().getEnterText().get());
+            Output.printLine();
+        }
+    }
+}

+ 106 - 0
src/main/java/eu/oschl/gui/actions/Explore.java

@@ -0,0 +1,106 @@
+package eu.oschl.gui.actions;
+
+import eu.oschl.gui.Output;
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.map.Passage;
+import javafx.scene.paint.Color;
+
+public class Explore implements Action {
+    private final Game game;
+
+    public Explore(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String getId() {
+        return "explore";
+    }
+
+    @Override
+    public String getName() {
+        return "Look around";
+    }
+
+    @Override
+    public String getDescription() {
+        return "find out information about the current area";
+    }
+
+    public void execute(String[] args) {
+        var room = this.game.getCurrentRoom();
+        var objects = room.getObjects();
+        var passages = room.getPassages();
+
+        Output.print(room.getName(), Color.ALICEBLUE);
+        Output.printLine();
+        Output.print(room.getDescription(), Color.LIGHTBLUE);
+
+        if (room.getEnemy().isPresent()) {
+            if (room.getEnemy().get().isAlive()) {
+                Output.printLine();
+                Output.printLine();
+
+                Output.print(room.getEnemy().get().getName(), Color.ORANGERED);
+                Output.print(" blocks the way.", Color.RED);
+                Output.printLine();
+                Output.print(room.getEnemy().get().getDescription(), Color.RED);
+                Output.printLine();
+                Output.printLine();
+                Output.print(room.getEnemy().get().getName(), Color.RED);
+                Output.print(" must be defeated to continue further.");
+
+                return;
+            } else {
+                Output.printLine();
+                Output.printLine();
+                Output.print("The corpse of ");
+                Output.print(room.getEnemy().get().getName(), Color.ORANGERED);
+                Output.print(" lies on the ground.");
+            }
+        }
+
+        if (!objects.isEmpty()) {
+            Output.printLine();
+            Output.printLine();
+
+            if (objects.size() == 1) {
+                Output.print("there is something in this room: ", Color.MAGENTA);
+            } else {
+                Output.print("there are " + objects.size() + " things in this room: ", Color.MAGENTA);
+            }
+            for (var object : objects) {
+                Output.printLine();
+                Output.print(" * " + object.getName(), Color.MAGENTA);
+                Output.print(", " + object.getDescription(), Color.WHITE);
+            }
+        }
+
+        if (!passages.isEmpty()) {
+            Output.printLine();
+            Output.printLine();
+
+            if (passages.size() == 1) {
+                Output.print("there is a passage leading out of here: ", Color.YELLOW);
+            } else {
+                Output.print("there are " + passages.size() + " passages leading out of here: ", Color.YELLOW);
+            }
+
+            for (Passage passage : passages) {
+                Output.printLine();
+                Output.print(" * " + passage.getName(), Color.YELLOW);
+
+                if (passage.getDescription().isPresent()) {
+                    Output.print(", " + passage.getDescription().get(), Color.WHITE);
+                }
+
+                if (passage.isSeeThrough() || passage.getOtherRoom(room).wasEntered()) {
+                    Output.print(" - leading to ");
+                    Output.print(passage.getOtherRoom(room).getName(), Color.LIGHTBLUE);
+                } else {
+                    Output.print(" - it's impossible to see what's on the other side", Color.RED);
+                }
+            }
+        }
+    }
+}

+ 56 - 0
src/main/java/eu/oschl/gui/actions/PressButton.java

@@ -0,0 +1,56 @@
+package eu.oschl.gui.actions;
+
+import eu.oschl.gui.Output;
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.objects.Button;
+import javafx.scene.paint.Color;
+
+public class PressButton implements Action {
+    private final Game game;
+
+    public PressButton(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String getId() {
+        return "press";
+    }
+
+    @Override
+    public String getName() {
+        return "Press";
+    }
+
+    @Override
+    public String getDescription() {
+        return "press a button";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        for (var object : this.game.getCurrentRoom().getObjects()) {
+            if (object.getName().equalsIgnoreCase(String.join(" ", args))) {
+                if (object instanceof Button button) {
+                    var result = button.press();
+
+                    if (result) {
+                        Output.print(button.getPressText(), Color.MAGENTA);
+                    } else {
+                        Output.print("Button ", Color.RED);
+                        Output.print(button.getName(), Color.MAGENTA);
+                        Output.print(" has already been pressed.", Color.RED);
+                    }
+
+                } else {
+                    Output.print("It's impossible to press ", Color.RED);
+                    Output.print(object.getName(), Color.MAGENTA);
+                    Output.print(". It's not a button.", Color.RED);
+                }
+                return;
+            }
+        }
+
+        Output.print("That item is not here.", Color.RED);
+    }
+}

+ 73 - 0
src/main/java/eu/oschl/gui/actions/Slay.java

@@ -0,0 +1,73 @@
+package eu.oschl.gui.actions;
+
+import eu.oschl.gui.Output;
+import eu.oschl.textadventure.Game;
+import javafx.scene.paint.Color;
+
+public class Slay implements Action {
+    private final Game game;
+
+    public Slay(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String getId() {
+        return "slay";
+    }
+
+    @Override
+    public String getName() {
+        return "Slay";
+    }
+
+    @Override
+    public String getDescription() {
+        return "fight an enemy";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (
+                this.game.getCurrentRoom().getEnemy().isEmpty() ||
+                !this.game.getCurrentRoom().getEnemy().get().getName().equalsIgnoreCase(String.join(" ", args))
+        ) {
+            Output.print("This enemy is not here.", Color.RED);
+            return;
+        }
+
+        var enemy = this.game.getCurrentRoom().getEnemy().get();
+
+        if (enemy.strength > 0 && game.getInventory().getWeapon().isEmpty()) {
+            Output.print(enemy.getName() + " is too strong. A weapon is required.", Color.RED);
+            return;
+        }
+
+        if (enemy.strength > 0 && enemy.strength > game.getInventory().getWeapon().get().getDamage()) {
+            Output.print(enemy.getName() + " is too strong. A stronger weapon is required.", Color.RED);
+            return;
+        }
+
+        var result = enemy.kill();
+
+        if (result) {
+            if (game.getInventory().getWeapon().isPresent()) {
+                Output.print(game.getInventory().getWeapon().get().getAttackText());
+            }
+
+            if (enemy.getKillText().isPresent()) {
+                Output.printLine();
+                Output.print(enemy.getKillText().get());
+                Output.printLine();
+            }
+
+            Output.printLine();
+            Output.print(enemy.getName(), Color.RED);
+            Output.print(" lies murdered on the floor.");
+        } else {
+            Output.print("It's impossible to kill ", Color.RED);
+            Output.print(enemy.getName(), Color.RED);
+            Output.print(".", Color.RED);
+        }
+    }
+}

+ 72 - 0
src/main/java/eu/oschl/gui/actions/TakeItem.java

@@ -0,0 +1,72 @@
+package eu.oschl.gui.actions;
+
+import eu.oschl.gui.Output;
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.objects.PickableObject;
+import eu.oschl.textadventure.objects.Weapon;
+import javafx.scene.paint.Color;
+
+public class TakeItem implements Action {
+    private final Game game;
+
+    public TakeItem(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String getId() {
+        return "takeitem";
+    }
+
+    @Override
+    public String getName() {
+        return "Take";
+    }
+
+    @Override
+    public String getDescription() {
+        return "take an item";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            Output.print("The way is blocked. It's impossible to pick up items.", Color.RED);
+            return;
+        }
+
+        for (var object : this.game.getCurrentRoom().getObjects()) {
+            if (object.getName().equalsIgnoreCase(String.join(" ", args))) {
+                if (object instanceof PickableObject item) {
+                    var result = item.pickUp();
+
+                    if (item instanceof Weapon) {
+                        if (result) {
+                            Output.print("Weapon ", Color.MAGENTA);
+                            Output.print(item.getName(), Color.MAGENTA);
+                            Output.print(" obtained.", Color.MAGENTA);
+                        } else {
+                            Output.print("This weapon is weaker than the current one.", Color.RED);
+                        }
+                    } else {
+                        if (result) {
+                            Output.print("Item ", Color.MAGENTA);
+                            Output.print(item.getName(), Color.MAGENTA);
+                            Output.print(" added to inventory.", Color.MAGENTA);
+                        } else {
+                            Output.print("Carrying too many items.", Color.MAGENTA);
+                        }
+                    }
+
+                } else {
+                    Output.print("Can't pick up ", Color.RED);
+                    Output.print(object.getName(), Color.MAGENTA);
+                    Output.print(".", Color.RED);
+                }
+                return;
+            }
+        }
+
+        Output.print("That item is not here.", Color.RED);
+    }
+}

+ 56 - 0
src/main/java/eu/oschl/gui/actions/UseItem.java

@@ -0,0 +1,56 @@
+package eu.oschl.gui.actions;
+
+import eu.oschl.gui.Output;
+import eu.oschl.textadventure.Game;
+import javafx.scene.paint.Color;
+
+public class UseItem implements Action {
+    private final Game game;
+
+    public UseItem(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String getId() {
+        return "useitem";
+    }
+
+    @Override
+    public String getName() {
+        return "Use item";
+    }
+
+    @Override
+    public String getDescription() {
+        return "use an item";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length == 0) {
+            Output.print("What item?", Color.RED);
+            return;
+        }
+
+        var item = this.game.getInventory().getItems().stream()
+                .filter(inventoryItem -> inventoryItem.getName().equalsIgnoreCase(String.join(" ", args)))
+                .findFirst()
+                .orElse(null);
+
+        if (item == null) {
+            Output.print("That item does not exist.", Color.RED);
+            return;
+        }
+
+        var result = item.use();
+
+        if (result) {
+            Output.print(item.getUseText(), Color.MAGENTA);
+        } else {
+            Output.print("It's impossible to use ", Color.RED);
+            Output.print(item.getName(), Color.MAGENTA);
+            Output.print(" here.", Color.RED);
+        }
+    }
+}

+ 24 - 0
src/main/java/eu/oschl/gui/exceptions/InvalidActionId.java

@@ -0,0 +1,24 @@
+package eu.oschl.gui.exceptions;
+
+/**
+ * This exception is thrown when the action ID is invalid.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class InvalidActionId extends RuntimeException {
+    public InvalidActionId() {
+        super();
+    }
+
+    public InvalidActionId(String message) {
+        super(message);
+    }
+
+    public InvalidActionId(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidActionId(Throwable cause) {
+        super(cause);
+    }
+}

+ 31 - 0
src/main/java/eu/oschl/schmorn/Main.java

@@ -0,0 +1,31 @@
+package eu.oschl.schmorn;
+
+import eu.oschl.textadventure.Game;
+
+/**
+ * Serves as an entry point, handles starting either CLI or GUI version of the game.
+ *
+ * @author Ondřej Schlaichert
+ */
+class Main {
+    static void main(String[] args) {
+        var useCli = args.length > 0 && args[0].equalsIgnoreCase("--cli");
+
+        var game = Setup.createGame();
+
+        if (useCli) {
+            startCli(game);
+        } else {
+            startGui(game, args);
+        }
+    }
+
+    private static void startCli(Game game) {
+        var session = new eu.oschl.cli.Session(game);
+        session.start();
+    }
+
+    private static void startGui(Game game, String[] args) {
+        eu.oschl.gui.Session.launchGui(game, args);
+    }
+}

+ 1 - 1
src/main/java/eu/oschl/schmorn/Setup.java

@@ -155,7 +155,7 @@ class Setup {
                     
                         "And so, here you are, you determined failure of a man. You have made it this far, but you will not leave this place. Not as a worm, not as a prince, not as anything. I stand in your path, and I will not let you pass. You have made your choice, and now you must live with the consequences."
                     
-                        Schmorn stares at her, fearful yet defiant. In his worm mouth, he holds the might Unbreakable Ladle, the most powerful weapon in his kingdom. He is ready to fight. Oglunda does not know what's coming.
+                        Schmorn stares at her, fearful yet defiant. In his worm mouth, he holds the mighty Unbreakable Ladle, the most powerful weapon in his kingdom. He is ready to fight. Oglunda does not know what's coming.
                         """
         );
 

+ 0 - 1
src/main/java/module-info.java

@@ -2,7 +2,6 @@ module eu.oschl.gui {
     requires javafx.controls;
     requires javafx.fxml;
 
-
     opens eu.oschl.gui to javafx.fxml;
     exports eu.oschl.gui;
 }

+ 154 - 0
src/main/resources/eu/oschl/gui/css/style.css

@@ -0,0 +1,154 @@
+@font-face {
+	font-family: "Iosevka Slab";
+	src: url("fonts/SGr-IosevkaSlab-Regular.ttc");
+}
+
+.root {
+	-fx-background-color: #000000;
+	-fx-font-family: "Iosevka Slab", serif;
+	-fx-font-size: 1.7em;
+	-fx-focus-color: #aaaaaa;
+}
+
+.vbox,
+.grid-pane,
+.text-flow {
+	-fx-background-color: transparent;
+}
+
+.text,
+.label {
+	-fx-fill: #e0e0e0;
+	-fx-text-fill: #e0e0e0;
+}
+
+.scroll-pane {
+	-fx-background-color: #000000;
+	-fx-background-radius: 0;
+	-fx-border-width: 0;
+}
+
+.scroll-pane > .viewport {
+	-fx-background-color: #000000;
+}
+
+.button {
+	-fx-background-color: #222222;
+	-fx-text-fill: #e0e0e0;
+	-fx-border-color: #555555;
+	-fx-border-width: 1px;
+	-fx-background-radius: 0;
+	-fx-border-radius: 0;
+	-fx-padding: 6px 12px;
+}
+
+.enter-button {
+	-fx-background-color: #14262b !important;
+}
+
+.press-button {
+	-fx-background-color: #14182b !important;
+}
+
+.slay-button {
+	-fx-background-color: #2b1416 !important;
+}
+
+.take-button {
+	-fx-background-color: #1e142b !important;
+}
+
+.button:hover {
+	-fx-background-color: #333333;
+	-fx-border-color: #777777;
+	-fx-text-fill: #ffffff;
+}
+
+.button:focused {
+	-fx-border-color: #e0e0e0;
+	-fx-border-width: 1px;
+}
+
+.button:pressed {
+	-fx-background-color: #111111;
+}
+
+.scroll-bar {
+	-fx-background-color: #000000;
+}
+
+.scroll-bar .increment-button,
+.scroll-bar .decrement-button {
+	-fx-background-color: #000000;
+	-fx-background-radius: 0;
+	-fx-padding: 0;
+}
+
+.scroll-bar .increment-arrow,
+.scroll-bar .decrement-arrow {
+	-fx-shape: "";
+	-fx-padding: 0;
+}
+
+.scroll-bar:vertical .track,
+.scroll-bar:horizontal .track {
+	-fx-background-color: #000000;
+	-fx-border-color: #000000;
+	-fx-background-radius: 0;
+	-fx-border-radius: 0;
+}
+
+.scroll-bar .thumb {
+	-fx-background-color: #333333;
+	-fx-background-radius: 0;
+}
+
+.scroll-bar .thumb:hover {
+	-fx-background-color: #555555;
+}
+
+.inventory-item {
+	/* Base style: No background, no border, transparent */
+	-fx-background-color: transparent;
+	-fx-border-color: transparent;
+	-fx-border-width: 1px; /* Keep border-width for smooth hover transition */
+	-fx-background-radius: 0;
+	-fx-border-radius: 0;
+	-fx-padding: 6px 12px; /* Inherit or define padding */
+}
+
+.inventory-item:hover {
+	/* Hover state: Opaque border (select box) */
+	-fx-background-color: transparent; /* No background change */
+	-fx-border-color: #e0e0e0; /* Opaque white/light-gray border */
+	-fx-text-fill: #ffffff; /* Slightly brighter text */
+}
+
+.inventory-item:focused {
+	/* Focused state: Retain a visible focus indicator */
+	-fx-border-color: #aaaaaa; /* A clear, visible focus color */
+	-fx-border-width: 1px;
+	-fx-background-color: transparent;
+}
+
+.inventory-item:pressed {
+	/* Pressed state: Slight visual feedback */
+	-fx-background-color: rgba(224, 224, 224, 0.1); /* Very subtle dark overlay */
+	-fx-border-color: #e0e0e0;
+}
+
+.weapon,
+.button.weapon:disabled,
+.weapon:disabled,
+.button.weapon:disabled .text {
+	-fx-text-fill: #ff5555 !important;
+	-fx-fill: #ff5555 !important;
+	-fx-opacity: 1 !important;
+}
+
+.button:hover { -fx-cursor: hand; }
+
+.button:disabled,
+.button:disabled:hover {
+	-fx-cursor: default;
+}

BIN
src/main/resources/eu/oschl/gui/fonts/SGr-IosevkaSlab-Regular.ttc


+ 79 - 0
src/main/resources/eu/oschl/gui/game.fxml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.ScrollPane?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.ColumnConstraints?>
+<?import javafx.scene.layout.GridPane?>
+<?import javafx.scene.layout.RowConstraints?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.TextFlow?>
+
+<?import javafx.scene.layout.StackPane?>
+<?import javafx.scene.shape.Rectangle?>
+<?import javafx.scene.control.Button?>
+<GridPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
+          fx:controller="eu.oschl.gui.GameController">
+
+    <columnConstraints>
+        <ColumnConstraints hgrow="ALWAYS"/>
+    </columnConstraints>
+
+    <rowConstraints>
+        <RowConstraints minHeight="10.0" percentHeight="75.0" vgrow="ALWAYS"/>
+        <RowConstraints minHeight="10.0" percentHeight="25.0" vgrow="ALWAYS"/>
+    </rowConstraints>
+    <GridPane>
+        <columnConstraints>
+            <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="75.0"/>
+            <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="25.0"/>
+        </columnConstraints>
+        <rowConstraints>
+            <RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
+        </rowConstraints>
+        <ScrollPane fitToWidth="true" hbarPolicy="NEVER">
+            <TextFlow fx:id="output">
+                <padding>
+                    <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
+                </padding>
+            </TextFlow>
+        </ScrollPane>
+        <GridPane GridPane.columnIndex="1">
+            <columnConstraints>
+                <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/>
+            </columnConstraints>
+            <rowConstraints>
+                <RowConstraints minHeight="10.0" percentHeight="50.0" prefHeight="30.0" vgrow="SOMETIMES"/>
+                <RowConstraints minHeight="10.0" percentHeight="50.0" prefHeight="30.0" vgrow="SOMETIMES"/>
+            </rowConstraints>
+            <StackPane GridPane.columnIndex="0" GridPane.rowIndex="0"
+                       GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS"
+                       GridPane.fillWidth="true" GridPane.fillHeight="true"
+                       fx:id="cell"
+                       minWidth="0" minHeight="0"
+                       maxWidth="Infinity" maxHeight="Infinity">
+                <clip>
+                    <Rectangle width="${cell.width}" height="${cell.height}"/>
+                </clip>
+                <ImageView preserveRatio="true" fitWidth="${cell.width}">
+                    <Image url="@images/rooms/well.png"/>
+                </ImageView>
+            </StackPane>
+            <ScrollPane fitToWidth="true" fitToHeight="true" GridPane.rowIndex="1">
+                <VBox fx:id="inventoryContainer" spacing="5.0" alignment="BOTTOM_CENTER">
+                    <padding>
+                        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
+                    </padding>
+                </VBox>
+            </ScrollPane>
+        </GridPane>
+    </GridPane>
+    <ScrollPane fitToWidth="true" GridPane.rowIndex="1">
+        <VBox fx:id="inputButtonContainer" spacing="5.0">
+            <padding>
+                <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
+            </padding>
+        </VBox>
+    </ScrollPane>
+</GridPane>

+ 0 - 16
src/main/resources/eu/oschl/gui/hello-view.fxml

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<?import javafx.geometry.Insets?>
-<?import javafx.scene.control.Label?>
-<?import javafx.scene.layout.VBox?>
-
-<?import javafx.scene.control.Button?>
-<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
-      fx:controller="eu.oschl.gui.HelloController">
-    <padding>
-        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
-    </padding>
-
-    <Label fx:id="welcomeText"/>
-    <Button text="Hello!" onAction="#onHelloButtonClick"/>
-</VBox>

BIN
src/main/resources/eu/oschl/gui/images/rooms/well.png