From 5d17086ed49dc109d7502bbb73f55821df9c1d37 Mon Sep 17 00:00:00 2001 From: co012 Date: Sat, 14 May 2022 21:59:03 +0200 Subject: [PATCH] @co012/position and bounds (#33) * chore: deleted tag setter * chore: change checkstyle.xml to allow method chaining * chore: remove duplicate code * chore: removed assetPath from GameObject and GameObjectConfig no longer extends GameObject * feat: Made DiscretePane * chore: got rid of SCALE * chore: improve DiscretePane * chore: replace Vector with build-in Point2D * chore: preparation for event base position changing * feat: Bounds are working * chore: made position observable * chore: bound Player and PlayerView positions * chore: remove Plyer field fromm LocationModel * chore: new constructor from Point2D * chore: object collisions * chore: better LocationModel.getObject function * Update src/main/java/io/rpg/model/location/LocationModel.java Co-authored-by: Marcin Hawryluk <70582973+mhawryluk@users.noreply.github.com> * chore: better bounds Co-authored-by: Marcin Hawryluk <70582973+mhawryluk@users.noreply.github.com> --- build.gradle | 8 + config/checkstyle/checkstyle.xml | 5 - src/main/java/io/rpg/Initializer.java | 8 +- src/main/java/io/rpg/Main.java | 6 +- .../io/rpg/config/model/GameObjectConfig.java | 31 ++-- .../java/io/rpg/controller/Controller.java | 22 ++- .../io/rpg/controller/PlayerController.java | 8 +- src/main/java/io/rpg/model/data/Position.java | 8 +- src/main/java/io/rpg/model/data/Vector.java | 25 --- .../io/rpg/model/location/LocationModel.java | 146 ++++++++++++------ .../java/io/rpg/model/object/GameObject.java | 63 ++++---- src/main/java/io/rpg/model/object/Player.java | 37 +---- .../java/io/rpg/util/GameObjectFactory.java | 6 +- src/main/java/io/rpg/view/DiscretePane.java | 72 +++++++++ src/main/java/io/rpg/view/GameObjectView.java | 43 ++++-- src/main/java/io/rpg/view/LocationView.java | 15 +- src/main/java/module-info.java | 1 + .../io/rpg/viewmodel/location-view.fxml | 3 +- .../java/io/rpg/config/ConfigLoaderTest.java | 4 +- 19 files changed, 305 insertions(+), 206 deletions(-) delete mode 100644 src/main/java/io/rpg/model/data/Vector.java create mode 100644 src/main/java/io/rpg/view/DiscretePane.java diff --git a/build.gradle b/build.gradle index c4c14252..78531794 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,14 @@ test { useJUnitPlatform() } +compileJava { + options.compilerArgs += ["--add-exports=javafx.graphics/com.sun.javafx.scene=io.rpg"] +} + +run { + jvmArgs = ['--add-exports=javafx.graphics/com.sun.javafx.scene=io.rpg'] +} + jlink { imageZip = project.file("${buildDir}/distributions/app-${javafx.platform.classifier}.zip") options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages'] diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index f160a312..7d3736bb 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -149,11 +149,6 @@ - - - - - diff --git a/src/main/java/io/rpg/Initializer.java b/src/main/java/io/rpg/Initializer.java index a76d14cb..91a2c66e 100644 --- a/src/main/java/io/rpg/Initializer.java +++ b/src/main/java/io/rpg/Initializer.java @@ -1,16 +1,13 @@ package io.rpg; import io.rpg.config.ConfigLoader; -import io.rpg.config.model.PlayerConfig; import io.rpg.controller.Controller; import io.rpg.config.model.GameWorldConfig; import io.rpg.config.model.LocationConfig; import io.rpg.controller.PlayerController; import io.rpg.model.actions.LocationChangeAction; -import io.rpg.model.data.Position; import io.rpg.model.location.LocationModel; import io.rpg.model.object.GameObject; -import io.rpg.config.model.GameObjectConfig; import io.rpg.model.object.Player; import io.rpg.util.GameObjectFactory; import io.rpg.util.GameObjectViewFactory; @@ -79,7 +76,6 @@ public Result initialize() { assert view != null; gameObjectViews.forEach(view::addChild); - model.addOnLocationModelStateChangeObserver(view); controllerBuilder @@ -87,7 +83,6 @@ public Result initialize() { .addModelForTag(locationConfig.getTag(), model) .registerToViews(gameObjectViews); - view.createViewsForObjects(model); } // Player is created separately @@ -100,8 +95,6 @@ public Result initialize() { Controller controller = controllerBuilder.build(); -// // TODO: this is a temporary solution -// controller.setPlayerView(playerView); Game.Builder gameBuilder = new Game.Builder(); Game game = gameBuilder @@ -135,6 +128,7 @@ public static void registerGameObjectViewsToModel(List gameObjects, // registration gameObject.addGameObjectStateChangeObserver(gameObjectView); + gameObjectView.bindToGameObject(gameObject); } } diff --git a/src/main/java/io/rpg/Main.java b/src/main/java/io/rpg/Main.java index c497e85c..ece22952 100644 --- a/src/main/java/io/rpg/Main.java +++ b/src/main/java/io/rpg/Main.java @@ -49,11 +49,7 @@ public void handle(long now) { if (lastUpdate != -1) { float difference = (now - lastUpdate) / 1e6f; - game.getController().getCurrentModel().update(difference); - Player player = game.getController().getCurrentModel().getPlayer(); - if (player != null) { - player.render(); - } + game.getController().getPlayerController().getPlayer().update(difference); } lastUpdate = now; } diff --git a/src/main/java/io/rpg/config/model/GameObjectConfig.java b/src/main/java/io/rpg/config/model/GameObjectConfig.java index 6fb2499a..419cf392 100644 --- a/src/main/java/io/rpg/config/model/GameObjectConfig.java +++ b/src/main/java/io/rpg/config/model/GameObjectConfig.java @@ -13,12 +13,28 @@ * Represents {@link io.rpg.model.object.GameObject} configuration provided by user * in configuration files. */ -public class GameObjectConfig extends GameObject { +public class GameObjectConfig { private String type; + private String tag; + private Position position; + private String assetPath; public GameObjectConfig(@NotNull String tag, @NotNull Position position) { - super(tag, position); + this.tag = tag; + this.position = position; + } + + public String getTag() { + return tag; + } + + public Position getPosition() { + return position; + } + + public String getAssetPath() { + return assetPath; } public String getTypeString() { @@ -39,15 +55,10 @@ public Result validate() { } public void updateFrom(GameObjectConfig gameObjectConfig) { - if (gameObjectConfig.getPosition() != null) { - this.position = gameObjectConfig.getPosition(); - } + this.position = gameObjectConfig.position; if (gameObjectConfig.getTypeString() != null) { this.type = gameObjectConfig.getTypeString(); } - if (gameObjectConfig.getAssetPath() != null) { - this.assetPath = gameObjectConfig.assetPath; - } } public String getFieldDescription() { @@ -68,9 +79,7 @@ public String getFieldDescription() { @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("\n{\n").append(super.getFieldDescription()).append(getFieldDescription()); - return builder.append("}").toString(); + return "\n{\n" + getFieldDescription() + "}"; } } diff --git a/src/main/java/io/rpg/controller/Controller.java b/src/main/java/io/rpg/controller/Controller.java index c4bebae8..551c3395 100644 --- a/src/main/java/io/rpg/controller/Controller.java +++ b/src/main/java/io/rpg/controller/Controller.java @@ -5,7 +5,6 @@ import io.rpg.model.data.KeyboardEvent; import io.rpg.model.data.MouseClickedEvent; import io.rpg.model.data.Position; -import io.rpg.model.data.Vector; import io.rpg.model.location.LocationModel; import io.rpg.model.object.*; import io.rpg.util.Result; @@ -13,6 +12,8 @@ import io.rpg.view.LocationView; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.function.Supplier; +import javafx.geometry.Point2D; import javafx.scene.Scene; import javafx.scene.input.KeyEvent; import javafx.stage.Stage; @@ -135,7 +136,7 @@ else if (tagToLocationViewMap.size() != tagToLocationModelMap.size()) @Override public void onKeyboardEvent(KeyboardEvent event) { // TODO: implement event handling - logger.info("Controller notified on key pressed from " + event.source()); + logger.trace("Controller notified on key pressed from " + event.source()); //TODO: call Player::set...Pressed depending on keyCode and whether the key was pressed or released KeyEvent payload = event.payload(); @@ -163,11 +164,14 @@ private int getWindowCenterY() { @Override public void onMouseClickedEvent(MouseClickedEvent event) { - int SCALE = 64; - Vector playerPos = currentModel.getPlayer().getPixelPosition(); + Point2D playerPos = playerController.getPlayer().getExactPosition(); GameObjectView objectView = event.source(); - GameObject object = currentModel.getObject((int) objectView.getY() / SCALE, (int) objectView.getX() / SCALE); - if (Math.abs(playerPos.x - objectView.getX()) / SCALE <= 1.5 && Math.abs(playerPos.y - objectView.getY()) / SCALE <= 1.5) { + Position position = new Position(objectView.getPosition()); + GameObject object = currentModel.getObject(position) + .orElseThrow(() -> new RuntimeException("No object present at position " + position)); + + double distance = playerPos.distance(objectView.getPosition()); + if (distance < 1.5) { if (object instanceof InteractiveGameObject) { ((InteractiveGameObject) object).onAction(); } @@ -175,11 +179,17 @@ public void onMouseClickedEvent(MouseClickedEvent event) { if (object instanceof CollectibleGameObject) { popupController.openTextImagePopup("Picked up an item!", objectView.getImage(), getWindowCenterX(), getWindowCenterY()); objectView.setVisible(false); + currentModel.removeGameObject(object); } } + logger.info("Controller notified on click from " + event.source()); } + public PlayerController getPlayerController() { + return playerController; + } + public static class Builder { private final Controller controller; diff --git a/src/main/java/io/rpg/controller/PlayerController.java b/src/main/java/io/rpg/controller/PlayerController.java index cab82897..c3f5a8b2 100644 --- a/src/main/java/io/rpg/controller/PlayerController.java +++ b/src/main/java/io/rpg/controller/PlayerController.java @@ -16,6 +16,7 @@ public class PlayerController implements KeyboardEvent.Observer { public PlayerController(Player player, GameObjectView playerView) { this.player = player; this.playerView = playerView; + playerView.bindToGameObject(player); this.onChangeLocation = () -> {}; player.addGameObjectStateChangeObserver(playerView); @@ -54,7 +55,7 @@ public void teleportTo(LocationModel model, LocationView view, Position playerPo updateOnChangeLocation(model, view); player.setPosition(playerPosition); - model.setPlayer(player); + model.addGameObject(player); view.addChild(playerView); view.addKeyboardEventObserver(this); } @@ -63,6 +64,11 @@ private void updateOnChangeLocation(LocationModel model, LocationView view) { this.onChangeLocation = () -> { view.removeKeyboardEventObserver(this); view.removeChild(this.playerView); + model.removeGameObject(player); }; } + + public Player getPlayer() { + return player; + } } diff --git a/src/main/java/io/rpg/model/data/Position.java b/src/main/java/io/rpg/model/data/Position.java index 276655a3..8ca050f1 100644 --- a/src/main/java/io/rpg/model/data/Position.java +++ b/src/main/java/io/rpg/model/data/Position.java @@ -1,15 +1,15 @@ package io.rpg.model.data; import java.util.Objects; +import javafx.geometry.Point2D; /** * Represents current position by holding row / col values. * This class can NOT be record due to some issues with * Gson library. */ -public class Position { +public final class Position { public final int row; - public final int col; public Position(int row, int col) { @@ -17,6 +17,10 @@ public Position(int row, int col) { this.col = col; } + public Position(Point2D point2D) { + this((int) Math.round(point2D.getY()), (int) Math.round(point2D.getX())); + } + public int getRow() { return row; } diff --git a/src/main/java/io/rpg/model/data/Vector.java b/src/main/java/io/rpg/model/data/Vector.java deleted file mode 100644 index 4bfdc61a..00000000 --- a/src/main/java/io/rpg/model/data/Vector.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.rpg.model.data; - -/** - * TODO. - */ -public class Vector { - @SuppressWarnings("checkstyle:MemberName") - public final float x; - - @SuppressWarnings("checkstyle:MemberName") - public final float y; - - public Vector(float x, float y) { - this.x = x; - this.y = y; - } - - public Vector(Position position) { - // TODO THIS MOTHT - int SCALE = 64; - this.x = position.col * SCALE; - this.y = position.row * SCALE; - } - -} diff --git a/src/main/java/io/rpg/model/location/LocationModel.java b/src/main/java/io/rpg/model/location/LocationModel.java index 07da8020..73237b17 100644 --- a/src/main/java/io/rpg/model/location/LocationModel.java +++ b/src/main/java/io/rpg/model/location/LocationModel.java @@ -2,15 +2,18 @@ import io.rpg.model.data.LocationModelStateChange; import io.rpg.model.data.Position; -import io.rpg.model.object.Player; import io.rpg.model.object.GameObject; import io.rpg.util.Result; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javafx.beans.value.ChangeListener; +import javafx.geometry.Point2D; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.*; /** * Represents single location in our game. @@ -18,7 +21,9 @@ public class LocationModel implements LocationModelStateChange.Emitter { private String tag; private List gameObjects; - private Player player; + private final HashMap> positionListeners; + private final HashMap positionGameObjectMap; + public final Point2D bounds; private final Set locationModelStateChangeObservers; @@ -30,45 +35,19 @@ public LocationModel(@NotNull String tag, @NotNull List gameObjects) private LocationModel() { this.locationModelStateChangeObservers = new LinkedHashSet<>(); - } - - public void setPlayer(@NotNull Player player) { - this.player = player; + this.positionListeners = new HashMap<>(); + this.positionGameObjectMap = new HashMap<>(); + bounds = new Point2D(10, 10); // TODO: 09.05.2022 Implement loading from config } public String getTag() { return tag; } - public Player getPlayer() { - return player; + public Optional getObject(Position position) { + return Optional.ofNullable(positionGameObjectMap.get(position)); } - - public GameObject getObject(int row, int column) { - GameObject object = gameObjects.stream().filter(gameObject -> gameObject.getPosition() - .equals(new Position(row, column))) - .findFirst().orElse(null); - if (object == null) { - throw new NullPointerException("No object found on (" + row + ", " + column + ")"); - } - return object; - } - - /** - * Private setter for Builder usage only. - * - * @param tag tag for the location - */ - private void setTag(String tag) { - this.tag = tag; - } - -// @UnmodifiableView -// public List getGameObjects() { -// return Collections.unmodifiableList(gameObjects); -// } - /** * Private setter for Builder usage only. Notice that ownership of {@link GameObject}s is not * transferred to LocationModel. @@ -78,6 +57,88 @@ private void setTag(String tag) { */ private void setGameObjects(List gameObjects) { this.gameObjects = gameObjects; + gameObjects.forEach(this::registerGameObject); + gameObjects.forEach(g -> checkAndCorrectBoundsCrossing(g, g.getExactPosition())); + gameObjects.forEach(g -> positionGameObjectMap.put(g.getPosition(), g)); + } + + public void addGameObject(GameObject gameObject) { + gameObjects.add(gameObject); + positionGameObjectMap.put(gameObject.getPosition(), gameObject); + registerGameObject(gameObject); + checkAndCorrectBoundsCrossing(gameObject, gameObject.getExactPosition()); + } + + private void registerGameObject(GameObject gameObject) { + ChangeListener positionListener = + (observable, oldValue, newValue) -> onGameObjectPositionChange(gameObject, oldValue, newValue); + gameObject.getExactPositionProperty() + .addListener(positionListener); + positionListeners.put(gameObject, positionListener); + } + + public void removeGameObject(GameObject gameObject) { + gameObjects.remove(gameObject); + positionGameObjectMap.remove(gameObject.getPosition()); + unRegisterGameObject(gameObject); + } + + private void unRegisterGameObject(GameObject gameObject) { + ChangeListener positionListener = positionListeners.remove(gameObject); + gameObject.getExactPositionProperty() + .removeListener(positionListener); + } + + private void onGameObjectPositionChange(GameObject gameObject, Point2D oldPosition, Point2D newPosition) { + checkAndCorrectBoundsCrossing(gameObject, newPosition); + + Position newPos = new Position(newPosition); + Position oldPos = new Position(oldPosition); + if (newPos.equals(oldPos)) { + return; + } + + // Collision check + if (positionGameObjectMap.containsKey(newPos) && !positionGameObjectMap.get(newPos) + .equals(gameObject)) { + gameObject.setExactPosition(oldPosition); + return; + } + + if (gameObject.equals(positionGameObjectMap.get(oldPos))) { + changeField(gameObject, oldPos, newPos); + } + } + + private void changeField(GameObject gameObject, Position oldPos, Position newPos) { + positionGameObjectMap.remove(oldPos); + positionGameObjectMap.put(newPos, gameObject); + } + + + private void checkAndCorrectBoundsCrossing(GameObject gameObject, Point2D newPosition) { + Point2D boundPosition = getBoundPosition(newPosition); + if (boundPosition.equals(newPosition)) { + return; + } + + gameObject.setExactPosition(boundPosition); + Point2D boundsCrossedDirection = newPosition.subtract(boundPosition) + .normalize(); + + emitBoundCrossedEvent(boundsCrossedDirection); + } + + private void emitBoundCrossedEvent(Point2D boundsCrossedDirection) { + // TODO: 10.05.2022 Bound crossed action + System.out.println(boundsCrossedDirection); + } + + private Point2D getBoundPosition(Point2D pos) { + double offset = 0.3; // it should be less than 0.5 + double x = Math.max(-offset, Math.min(bounds.getX() - 1 + offset, pos.getX())); + double y = Math.max(-offset, Math.min(bounds.getY() - 1 + offset, pos.getY())); + return new Point2D(x, y); } @Override @@ -97,6 +158,7 @@ public void emitLocationModelStateChange(LocationModelStateChange event) { }); } + public Result validate() { if (tag == null || gameObjects == null) { return Result.error(null); @@ -120,8 +182,7 @@ public Builder setGameObjects(@NotNull List gameObjects) { } public Builder setTag(@NotNull String tag) { - assert tag != null : "Location tag must not be null!"; - locationModel.setTag(tag); + locationModel.tag = tag; return this; } @@ -136,14 +197,5 @@ public LocationModel build() { return locationModel; } } - public List getGameObjects() { - return gameObjects; - } - - public void update(float elapsed){ - if(player!=null){ - player.update(elapsed); - } - } } diff --git a/src/main/java/io/rpg/model/object/GameObject.java b/src/main/java/io/rpg/model/object/GameObject.java index 315f58d9..6aff9210 100644 --- a/src/main/java/io/rpg/model/object/GameObject.java +++ b/src/main/java/io/rpg/model/object/GameObject.java @@ -1,18 +1,17 @@ package io.rpg.model.object; -import io.rpg.config.model.GameObjectConfig; import io.rpg.model.data.GameObjectStateChange; import io.rpg.model.data.Position; -import io.rpg.model.data.Vector; -import io.rpg.view.GameObjectView; -import javafx.scene.image.ImageView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.lang.reflect.Field; import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Point2D; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Class representing common state properties for all @@ -20,15 +19,10 @@ */ public class GameObject implements GameObjectStateChange.Emitter { -// protected Vector currentPosition; - -// public GameObjectView view; - /** * Position of game object in model's representation of location. */ - @Nullable - protected Position position; + private final SimpleObjectProperty exactPositionProperty; /** * Unique identifier of this game object. @@ -37,20 +31,9 @@ public class GameObject implements GameObjectStateChange.Emitter { @NotNull private final String tag; - /** - * - */ - @Nullable - protected String assetPath; - @NotNull private final Set stateChangeObservers; - @Nullable - public String getAssetPath() { - return assetPath; - } - /** * Unique identifier of this game object. */ @@ -59,6 +42,13 @@ public String getTag() { return tag; } + + public GameObject(@NotNull String tag, @NotNull Position position) { + this.tag = tag; + this.stateChangeObservers = new LinkedHashSet<>(); + this.exactPositionProperty = new SimpleObjectProperty<>(new Point2D(position.col, position.row)); + } + /** * Position of game object in model's representation of location. * @@ -66,18 +56,20 @@ public String getTag() { */ @Nullable public Position getPosition() { - return position; + Point2D exactPosition = getExactPosition(); + return new Position(exactPosition); } - public GameObject(@NotNull String tag, @NotNull Position position) { - this(tag, position, ""); + public void setExactPosition(Point2D position) { + exactPositionProperty.setValue(position); } - public GameObject(@NotNull String tag, @NotNull Position position, @NotNull String assetPath) { - this.tag = tag; - this.position = position; - this.assetPath = assetPath; - this.stateChangeObservers = new LinkedHashSet<>(); + public Point2D getExactPosition() { + return exactPositionProperty.getValue(); + } + + public ObservableValue getExactPositionProperty() { + return exactPositionProperty; } @Override @@ -96,6 +88,7 @@ public void removeGameObjectStateChangeObserver(GameObjectStateChange.Observer o this.stateChangeObservers.remove(observer); } + public String getFieldDescription() { StringBuilder builder = new StringBuilder(); for (Field field : GameObject.class.getDeclaredFields()) { @@ -120,6 +113,12 @@ public String toString() { return builder.append("}").toString(); } + public void setPosition(Position playerPosition) { + setExactPosition(new Point2D(playerPosition.col, playerPosition.row)); + } + + + public enum Type { NAVIGABLE("navigable"), DIALOG("dialog"), diff --git a/src/main/java/io/rpg/model/object/Player.java b/src/main/java/io/rpg/model/object/Player.java index 4f5b8e6e..9a0a9771 100644 --- a/src/main/java/io/rpg/model/object/Player.java +++ b/src/main/java/io/rpg/model/object/Player.java @@ -1,50 +1,35 @@ package io.rpg.model.object; import io.rpg.model.data.Position; -import io.rpg.model.data.Vector; import io.rpg.view.GameObjectView; -import io.rpg.model.object.GameObject; -import javafx.scene.image.Image; +import javafx.geometry.Point2D; import org.jetbrains.annotations.NotNull; public class Player extends GameObject { - private Vector currentPosition; private int strength; private float speed; - private Vector direction; private boolean rightPressed; private boolean leftPressed; private boolean upPressed; private boolean downPressed; private GameObjectView gameObjectView; - private Vector pixelPosition; public Player(@NotNull String tag, @NotNull Position position, @NotNull String assetPath) { - super(tag, position, assetPath); - this.currentPosition = new Vector(position.col, position.row); - this.speed = 100f; - this.direction = new Vector(0, 0); + super(tag, position); + this.speed = 6f; this.rightPressed = false; this.leftPressed = false; this.upPressed = false; this.downPressed = false; this.strength = 0; - this.pixelPosition = new Vector(position); } public void updateStrength(int value) { strength += value; } - public void setDirection(Vector direction) { - this.direction = direction; - } - - public Vector getPixelPosition() { - return pixelPosition; - } public void update(float elapsed) { float y = 0; @@ -66,7 +51,11 @@ public void update(float elapsed) { x += 1; } - this.pixelPosition = new Vector(this.pixelPosition.x + speed * x * elapsed / 1000, this.pixelPosition.y + speed * y * elapsed / 1000); + Point2D nextPosition = new Point2D(x, y) + .multiply(speed * elapsed / 1000) + .add(this.getExactPosition()); + + this.setExactPosition(nextPosition); } public void setRightPressed(boolean rightPressed) { @@ -93,14 +82,4 @@ public void setGameObjectView(GameObjectView gameObjectView) { this.gameObjectView = gameObjectView; } - public void render() { - if (gameObjectView != null) { - gameObjectView.setX(this.pixelPosition.x); - gameObjectView.setY(this.pixelPosition.y); - } - } - - public void setPosition(Position position) { - pixelPosition = new Vector(position); - } } diff --git a/src/main/java/io/rpg/util/GameObjectFactory.java b/src/main/java/io/rpg/util/GameObjectFactory.java index 2fd84ead..c9b38b82 100644 --- a/src/main/java/io/rpg/util/GameObjectFactory.java +++ b/src/main/java/io/rpg/util/GameObjectFactory.java @@ -1,13 +1,10 @@ package io.rpg.util; import io.rpg.config.model.GameObjectConfig; -import io.rpg.model.data.Vector; import io.rpg.model.object.*; -import javafx.scene.image.Image; import java.util.LinkedList; import java.util.List; -import java.util.Locale; /** * Exposes collection of methods to create {@link io.rpg.model.object.GameObject} class instances. @@ -20,11 +17,10 @@ public class GameObjectFactory { * @return game object created based on information located in config */ public static GameObject fromConfig(GameObjectConfig config) { - // TODO: add case for Player type here! switch (GameObject.Type.valueOf(config.getTypeString().toUpperCase())) { case COLLECTIBLE -> { return new CollectibleGameObject(config.getTag(), config.getPosition()); } case DIALOG -> { return new DialogGameObject(config.getTag(), config.getPosition()); } - case PLAYER -> { return new Player(config.getTag(), config.getPosition(),config.getAssetPath()); } + case PLAYER -> { return new Player(config.getTag(), config.getPosition(), config.getAssetPath()); } case NAVIGABLE -> { return new NavigationalGameObject(config.getTag(), config.getPosition()); } default -> throw new RuntimeException("Unknown GameObject type. This should not happen!"); } diff --git a/src/main/java/io/rpg/view/DiscretePane.java b/src/main/java/io/rpg/view/DiscretePane.java new file mode 100644 index 00000000..37e368c8 --- /dev/null +++ b/src/main/java/io/rpg/view/DiscretePane.java @@ -0,0 +1,72 @@ +package io.rpg.view; + +import javafx.collections.ListChangeListener; +import javafx.geometry.HPos; +import javafx.geometry.Point2D; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; + +/** + * Custom pane. It scales children to correct size and lays them in correct spot. + */ +public class DiscretePane extends Pane { + private static final double FIELD_SIZE = 64; + + /** + * Creates a DiscretePane. + */ + public DiscretePane() { + super(); + getChildren().addListener((ListChangeListener) c -> { + while (c.next()) { + if (c.wasAdded()) { + c.getAddedSubList().forEach(this::modifyNewChild); + } + } + }); + } + + private void modifyNewChild(Node node) { + node.maxWidth(FIELD_SIZE); + node.maxHeight(FIELD_SIZE); + if (node instanceof ImageView imageView) { + imageView.setFitWidth(FIELD_SIZE); + imageView.setFitHeight(FIELD_SIZE); + } + } + + + @Override + protected void layoutChildren() { + double baselineOffset = 0; + for (Node child : getManagedChildren()) { + + Point2D position = calcLayoutPosition(child); + + layoutInArea(child, + position.getX(), + position.getY(), + FIELD_SIZE, + FIELD_SIZE, + baselineOffset, + HPos.LEFT, + VPos.TOP); + } + } + + private Point2D calcLayoutPosition(Node child) { + double x; + double y; + + if (child instanceof GameObjectView gameObjectView) { + return gameObjectView.getPosition().multiply(FIELD_SIZE); + } else { + x = child.getLayoutX(); + y = child.getLayoutY(); + } + + return new Point2D(x, y); + } +} diff --git a/src/main/java/io/rpg/view/GameObjectView.java b/src/main/java/io/rpg/view/GameObjectView.java index 2da8b341..5aad20e4 100644 --- a/src/main/java/io/rpg/view/GameObjectView.java +++ b/src/main/java/io/rpg/view/GameObjectView.java @@ -1,39 +1,46 @@ package io.rpg.view; +import com.sun.javafx.scene.ImageViewHelper; import io.rpg.model.data.GameObjectStateChange; import io.rpg.model.data.MouseClickedEvent; import io.rpg.model.data.Position; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import org.jetbrains.annotations.NotNull; - +import io.rpg.model.object.GameObject; import java.nio.file.Path; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Point2D; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.jetbrains.annotations.NotNull; public class GameObjectView extends ImageView implements MouseClickedEvent.Emitter, GameObjectStateChange.Observer { private Path path; private final Set onClickedObservers; + private final SimpleObjectProperty position; - private final int SCALE = 64; public GameObjectView(@NotNull Path assetPath, @NotNull Position position) { this.path = assetPath; -// String xdpath = this.setImage(new Image(resolvePathToJFXFormat(path.toString()))); - // todo: better position class - this.setX(position.col * SCALE); - this.setY(position.row * SCALE); - - this.setPreserveRatio(true); - this.setSmooth(false); - this.setFitHeight(SCALE); - + this.position = new SimpleObjectProperty<>(new Point2D(position.col, position.row)); + setLayoutUpdateOnPositionChange(); this.onClickedObservers = new HashSet<>(); this.setOnMouseClicked(event -> emitOnMouseClickedEvent(new MouseClickedEvent(this, event))); } + private void setLayoutUpdateOnPositionChange() { + this.position.addListener((observable, oldValue, newValue) -> { + if (Objects.equals(oldValue, newValue)) { + return; + } + + ImageViewHelper.geomChanged(this); + }); + } + public static String resolvePathToJFXFormat(String path) { return "file:" + path; } @@ -45,6 +52,14 @@ public void emitOnMouseClickedEvent(MouseClickedEvent event) { onClickedObservers.forEach(listener -> listener.onMouseClickedEvent(event)); } + public void bindToGameObject(GameObject gameObject) { + this.position.bind(gameObject.getExactPositionProperty()); + } + + public Point2D getPosition() { + return position.get(); + } + @Override public void addOnClickedObserver(MouseClickedEvent.Observer observer) { onClickedObservers.add(observer); diff --git a/src/main/java/io/rpg/view/LocationView.java b/src/main/java/io/rpg/view/LocationView.java index 11889df2..7a343efe 100644 --- a/src/main/java/io/rpg/view/LocationView.java +++ b/src/main/java/io/rpg/view/LocationView.java @@ -1,16 +1,11 @@ package io.rpg.view; -import io.rpg.Game; -import io.rpg.Initializer; -import io.rpg.config.model.GameObjectConfig; import io.rpg.model.data.KeyboardEvent; import io.rpg.model.data.LocationModelStateChange; import io.rpg.model.location.LocationModel; import io.rpg.model.object.GameObject; import io.rpg.viewmodel.LocationViewModel; import io.rpg.config.model.LocationConfig; -import java.util.Collection; -import java.util.Collections; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -31,7 +26,6 @@ public class LocationView extends Scene implements KeyboardEvent.Emitter, LocationModelStateChange.Observer { - static final int SCALE = 32; // TODO: REMOVE THIS private final static URL FXML_URL = LocationViewModel.class.getResource("location-view.fxml"); private final Logger logger; @@ -100,20 +94,13 @@ public void emitKeyboardEvent(KeyboardEvent event) { @Override public void onLocationModelStateChange(LocationModelStateChange event) { // TODO: implement state change & appropriate events - // most likely here we watn to pass this event to LocationViewModel or even + // most likely here we want to pass this event to LocationViewModel or even // make LocationViewModel implement LocationModelStateChange.Observer } List gameObjectViews = new ArrayList<>(); - public void createViewsForObjects(LocationModel locationModel) { - for (GameObject g : locationModel.getGameObjects()) { - GameObjectView gameObjectView = new GameObjectView(Path.of(g.getAssetPath()), g.getPosition()); - gameObjectViews.add(gameObjectView); - viewModel.getForegroundPane().getChildren().add(gameObjectView); - } - } public void removeChild(GameObjectView view) { viewModel.getForegroundPane().getChildren().remove(view); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index b6cc828d..c4140771 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -18,6 +18,7 @@ opens io.rpg.model.data to com.google.gson; opens io.rpg.viewmodel to javafx.fxml; + opens io.rpg.view to javafx.fxml; opens io.rpg.config.model to com.google.gson; exports io.rpg; diff --git a/src/main/resources/io/rpg/viewmodel/location-view.fxml b/src/main/resources/io/rpg/viewmodel/location-view.fxml index c567fec8..fecd5b24 100644 --- a/src/main/resources/io/rpg/viewmodel/location-view.fxml +++ b/src/main/resources/io/rpg/viewmodel/location-view.fxml @@ -5,6 +5,7 @@ + @@ -14,7 +15,7 @@ - + diff --git a/src/test/java/io/rpg/config/ConfigLoaderTest.java b/src/test/java/io/rpg/config/ConfigLoaderTest.java index a82168c2..c73c3163 100644 --- a/src/test/java/io/rpg/config/ConfigLoaderTest.java +++ b/src/test/java/io/rpg/config/ConfigLoaderTest.java @@ -79,7 +79,7 @@ public void gameWorldConfigIsLoadedProperly() { // "tag": "player", // "position": { "row": 4, "col": 5 }, // "type": "player", - // "assetPath": "assets/stone.png", + // "assetPath": "assets/stone.png", (Removed) // "location": "location-1" // } PlayerConfig actualPlayerConfig = config.getPlayerConfig(); @@ -87,6 +87,6 @@ public void gameWorldConfigIsLoadedProperly() { Assertions.assertEquals("player", actualPlayerConfig.getTag()); Assertions.assertEquals("player", actualPlayerConfig.getTypeString()); Assertions.assertEquals(new Position(4, 5), actualPlayerConfig.getPosition()); - Assertions.assertEquals("assets/stone.png", actualPlayerConfig.getAssetPath()); + // Assertions.assertEquals("assets/stone.png", actualPlayerConfig.getAssetPath()); } }