diff --git a/.forgejo/workflows/qs.yml b/.forgejo/workflows/qs.yml index 6d48d25..de9bbf4 100644 --- a/.forgejo/workflows/qs.yml +++ b/.forgejo/workflows/qs.yml @@ -28,6 +28,17 @@ jobs: - name: "Stop Gradle" run: gradle --stop + async: + name: "Validate Async API Specification" + runs-on: stable + container: + image: asyncapi/cli:latest + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - name: run + run: asyncapi validate ws/ws.yml + linting: name: "Linting" runs-on: stable diff --git a/build.gradle.kts b/build.gradle.kts index ca54b5c..6e67ef7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,6 +61,9 @@ dependencies { // Postgres runtimeOnly("org.postgresql:postgresql") + // JOML + implementation("org.joml:joml:1.10.7") + // Lombok compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") diff --git a/src/main/java/de/towerdefence/server/game/Enemy.java b/src/main/java/de/towerdefence/server/game/Enemy.java index 3a7828f..dc21088 100644 --- a/src/main/java/de/towerdefence/server/game/Enemy.java +++ b/src/main/java/de/towerdefence/server/game/Enemy.java @@ -2,12 +2,11 @@ package de.towerdefence.server.game; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.Setter; @Getter -@Setter @AllArgsConstructor public class Enemy { - private int poxX; - private int posY; + protected int id; + protected int x; + protected int y; } diff --git a/src/main/java/de/towerdefence/server/game/GameSession.java b/src/main/java/de/towerdefence/server/game/GameSession.java index 5e04282..fcaee35 100644 --- a/src/main/java/de/towerdefence/server/game/GameSession.java +++ b/src/main/java/de/towerdefence/server/game/GameSession.java @@ -1,18 +1,27 @@ package de.towerdefence.server.game; +import java.lang.reflect.Constructor; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import de.towerdefence.server.game.callbacks.EnemiesChangedCallback; import de.towerdefence.server.game.callbacks.PlayerMoneyCallback; +import de.towerdefence.server.game.callbacks.PlayerHitpointsCallback; import de.towerdefence.server.game.exeptions.InvalidPlacementException; import de.towerdefence.server.game.exeptions.InvalidPlacementReason; +import de.towerdefence.server.game.exeptions.WaveInProgressException; import de.towerdefence.server.player.Player; import lombok.Getter; +import org.joml.Vector2i; public class GameSession { final static int START_HITPOINTS = 100; final static int START_MONEY = 50; - final static int MAP_SIZE_X = 10; - final static int MAP_SIZE_Y = 20; - - private final Tower[][] towers = new Tower[MAP_SIZE_X][MAP_SIZE_Y]; + final static Vector2i MAP_SIZE = new Vector2i(10, 20); + final static int WAVE_SPAWN_DELAY = 200; // ms + final static Vector2i WAVE_SPAWN = new Vector2i(5, 0); + final static Vector2i WAVE_TARGET = new Vector2i(5, 19); private final Player player; @Getter @@ -20,19 +29,77 @@ public class GameSession { @Getter private int playerHitpoints; private final PlayerMoneyCallback moneyCallback; - //private final PlayerHitpointsCallback hitpointsCallback; + private final PlayerHitpointsCallback hitpointsCallback; + private final EnemiesChangedCallback enemyCallback; - public GameSession(Player player, PlayerMoneyCallback moneyCallback) { + private final Tower[][] towers = new Tower[MAP_SIZE.x][MAP_SIZE.y]; + private final Enemy[][] enemies = new Enemy[MAP_SIZE.x][MAP_SIZE.y]; + private List wave = new ArrayList<>(); + private long lastSpawn = Instant.now().toEpochMilli(); + + public GameSession( + Player player, + PlayerMoneyCallback moneyCallback, + PlayerHitpointsCallback hitpointsCallback, + EnemiesChangedCallback enemyCallback) { this.player = player; this.moneyCallback = moneyCallback; - //this.hitpointsCallback = hitpointsCallback; + this.hitpointsCallback = hitpointsCallback; + this.enemyCallback = enemyCallback; this.money = START_MONEY; this.playerHitpoints = START_HITPOINTS; } + public void startWave(List enemies) throws WaveInProgressException { + if (wave.size() > 0) { + throw new WaveInProgressException(); + } + wave = enemies; + } + + public void update() { + List newEnemies = new ArrayList<>(); + + List changedEnemies = moveEnemies(); + + if (wave.size() > 0 && shouldSpawn()) { + enemies[WAVE_SPAWN.x][WAVE_SPAWN.y] = wave.removeFirst(); + newEnemies.add(enemies[WAVE_SPAWN.x][WAVE_SPAWN.y]); + } + + if (newEnemies.size() > 0 || changedEnemies.size() > 0) { + enemyCallback.call( + player, + newEnemies.toArray(new Enemy[newEnemies.size()]), + changedEnemies.toArray(new Enemy[changedEnemies.size()])); + } + } + + private boolean shouldSpawn() { + return lastSpawn + WAVE_SPAWN_DELAY < Instant.now().toEpochMilli() + && enemies[WAVE_SPAWN.x][WAVE_SPAWN.y] == null; + } + + private List moveEnemies() { + // List changedEnemies = moveEnemies(); + + // TODO: Implement Moving of Enemies (possibly through A*) + throw new RuntimeException("NOT IMPLEMENTED"); + + // return changedEnemies; + + } + + /** + * @return the next position to go to + */ + private Vector2i pathfinding(Vector2i start, Vector2i target) { + + } + public void placeTower(Tower tower, int x, int y) throws InvalidPlacementException { - if (x < 0 || y < 0 || x + 1 > MAP_SIZE_X || y + 1 > MAP_SIZE_Y) { + if (x < 0 || y < 0 || x + 1 > MAP_SIZE.x || y + 1 > MAP_SIZE.y) { throw new InvalidPlacementException(InvalidPlacementReason.OUT_OF_BOUNDS); } if (towers[x][y] != null) { @@ -41,8 +108,13 @@ public class GameSession { if (money < Tower.COST) { throw new InvalidPlacementException(InvalidPlacementReason.NOT_ENOUGH_MONEY); } + + // TODO: Do Pathfinding check for the placement + money -= Tower.COST; moneyCallback.call(player, money); + tower.x = x; + tower.y = y; towers[x][y] = tower; } diff --git a/src/main/java/de/towerdefence/server/game/Tower.java b/src/main/java/de/towerdefence/server/game/Tower.java index 48a0c3d..a4a9f64 100644 --- a/src/main/java/de/towerdefence/server/game/Tower.java +++ b/src/main/java/de/towerdefence/server/game/Tower.java @@ -1,5 +1,13 @@ package de.towerdefence.server.game; +import lombok.Getter; + +@Getter public class Tower { public static final int COST = 20; + protected int x; + protected int y; + + public Tower() { + } } diff --git a/src/main/java/de/towerdefence/server/game/callbacks/EnemiesChangedCallback.java b/src/main/java/de/towerdefence/server/game/callbacks/EnemiesChangedCallback.java new file mode 100644 index 0000000..06f4a49 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/callbacks/EnemiesChangedCallback.java @@ -0,0 +1,9 @@ +package de.towerdefence.server.game.callbacks; + +import de.towerdefence.server.game.Enemy; +import de.towerdefence.server.player.Player; + +@FunctionalInterface +public interface EnemiesChangedCallback { + void call(Player player, Enemy[] newEnemies, Enemy[] changedEnemies); +} diff --git a/src/main/java/de/towerdefence/server/game/callbacks/PlayerHitpointsCallback.java b/src/main/java/de/towerdefence/server/game/callbacks/PlayerHitpointsCallback.java index ed1e959..83badd3 100644 --- a/src/main/java/de/towerdefence/server/game/callbacks/PlayerHitpointsCallback.java +++ b/src/main/java/de/towerdefence/server/game/callbacks/PlayerHitpointsCallback.java @@ -1,4 +1,4 @@ -package de.towerdefence.server.match.callbacks; +package de.towerdefence.server.game.callbacks; import de.towerdefence.server.player.Player; diff --git a/src/main/java/de/towerdefence/server/game/exeptions/WaveInProgressException.java b/src/main/java/de/towerdefence/server/game/exeptions/WaveInProgressException.java new file mode 100644 index 0000000..90c8518 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/exeptions/WaveInProgressException.java @@ -0,0 +1,4 @@ +package de.towerdefence.server.game.exeptions; + +public class WaveInProgressException extends RuntimeException { +} diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java b/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java new file mode 100644 index 0000000..d4a4f00 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java @@ -0,0 +1,5 @@ +package de.towerdefence.server.game.pathfinding; + +public class AStar { + +} diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/NoPathException.java b/src/main/java/de/towerdefence/server/game/pathfinding/NoPathException.java new file mode 100644 index 0000000..ce36e57 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/NoPathException.java @@ -0,0 +1,4 @@ +package de.towerdefence.server.game.pathfinding; + +public class NoPathException extends RuntimeException { +} diff --git a/src/main/java/de/towerdefence/server/match/Match.java b/src/main/java/de/towerdefence/server/match/Match.java index dad8e97..b9cd016 100644 --- a/src/main/java/de/towerdefence/server/match/Match.java +++ b/src/main/java/de/towerdefence/server/match/Match.java @@ -1,7 +1,12 @@ package de.towerdefence.server.match; -import de.towerdefence.server.match.callbacks.PlayerHitpointsCallback; -import de.towerdefence.server.match.callbacks.PlayerMoneyCallback; +import de.towerdefence.server.game.callbacks.PlayerHitpointsCallback; +import java.util.Optional; + +import de.towerdefence.server.game.GameSession; +import de.towerdefence.server.game.callbacks.EnemiesChangedCallback; +import de.towerdefence.server.game.callbacks.PlayerMoneyCallback; +import de.towerdefence.server.game.callbacks.PlayerHitpointsCallback; import de.towerdefence.server.player.Player; import lombok.Getter; @@ -47,21 +52,21 @@ public class Match { } public void connectPlayer( - Player player, - PlayerMoneyCallback moneyCallback, - PlayerHitpointsCallback hitpointsCallback - ) { + Player player, + PlayerMoneyCallback moneyCallback, + PlayerHitpointsCallback hitpointsCallback, + EnemiesChangedCallback enemyCallback) { boolean isPlayer1 = player1.equals(player); boolean isPlayer2 = player2.equals(player); if (!isPlayer1 && !isPlayer2) { return; } if (isPlayer1 && player1Session == null) { - this.player1Session = new GameSession(player, moneyCallback); + this.player1Session = new GameSession(player, moneyCallback, hitpointsCallback, enemyCallback); return; } if (isPlayer2 && player2Session == null) { - this.player2Session = new GameSession(player, moneyCallback); + this.player2Session = new GameSession(player, moneyCallback, hitpointsCallback, enemyCallback); return; } } diff --git a/src/main/java/de/towerdefence/server/match/MatchService.java b/src/main/java/de/towerdefence/server/match/MatchService.java index 31792e4..63729e7 100644 --- a/src/main/java/de/towerdefence/server/match/MatchService.java +++ b/src/main/java/de/towerdefence/server/match/MatchService.java @@ -1,10 +1,9 @@ package de.towerdefence.server.match; -import de.towerdefence.server.match.callbacks.PlayerHitpointsCallback; -import de.towerdefence.server.match.callbacks.PlayerMoneyCallback; -import de.towerdefence.server.match.exeptions.InvalidPlacementException; -import de.towerdefence.server.match.exeptions.InvalidPlacementReason; -import de.towerdefence.server.match.exeptions.NotInMatchException; +import de.towerdefence.server.game.callbacks.PlayerHitpointsCallback; +import de.towerdefence.server.game.callbacks.PlayerMoneyCallback; +import de.towerdefence.server.game.exeptions.InvalidPlacementException; +import de.towerdefence.server.game.exeptions.InvalidPlacementReason; import de.towerdefence.server.player.Player; import org.springframework.stereotype.Service; @@ -16,14 +15,9 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.springframework.stereotype.Service; - import de.towerdefence.server.game.GameSession; import de.towerdefence.server.game.Tower; -import de.towerdefence.server.game.callbacks.PlayerMoneyCallback; -import de.towerdefence.server.game.exeptions.InvalidPlacementException; -import de.towerdefence.server.game.exeptions.InvalidPlacementReason; -import de.towerdefence.server.player.Player; +import de.towerdefence.server.game.callbacks.EnemiesChangedCallback; @Service public class MatchService { @@ -62,19 +56,19 @@ public class MatchService { } public void playerConnected( - Player player, - PlayerMoneyCallback moneyCallback, - PlayerHitpointsCallback hitpointsCallback - ) { + Player player, + PlayerMoneyCallback moneyCallback, + PlayerHitpointsCallback hitpointsCallback, + EnemiesChangedCallback enemyCallback) { Match match = playerMatches.get(player); - match.connectPlayer(player, moneyCallback, hitpointsCallback); + match.connectPlayer(player, moneyCallback, hitpointsCallback, enemyCallback); Optional optionalPlayerSession = match.getPlayerGameSession(player); if (optionalPlayerSession.isEmpty()) { return; } GameSession playerSession = optionalPlayerSession.get(); moneyCallback.call(player, playerSession.getMoney()); - hitpointsCallback.call(player, playerSession.getPlayerHitpoints() ); + hitpointsCallback.call(player, playerSession.getPlayerHitpoints()); if (!match.hasMatchStarted()) { return; } diff --git a/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java index fa69142..866b332 100644 --- a/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java +++ b/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java @@ -11,6 +11,7 @@ import org.springframework.web.socket.WebSocketSession; import com.fasterxml.jackson.databind.ObjectMapper; +import de.towerdefence.server.game.Enemy; import de.towerdefence.server.game.exeptions.InvalidPlacementException; import de.towerdefence.server.match.MatchService; import de.towerdefence.server.match.NotInMatchException; @@ -41,12 +42,20 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { super.afterConnectionEstablished(session); playerSessions.put(sessionPlayers.get(session), session); matchService.playerConnected( - sessionPlayers.get(session), - this::onPlayerMoneyChanged, - this::onPlayerHitpointsChanged + sessionPlayers.get(session), + this::onPlayerMoneyChanged, + this::onPlayerHitpointsChanged, + this::onEnemiesChanged ); } + @Override + protected void closeSession(WebSocketSession session, CloseStatus reason) { + Player player = sessionPlayers.get(session); + super.closeSession(session, reason); + playerSessions.remove(player); + } + private void onPlayerMoneyChanged(Player player, int playerMoney) { WebSocketSession session = playerSessions.get(player); try { @@ -63,11 +72,8 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { } } - @Override - protected void closeSession(WebSocketSession session, CloseStatus reason) { - Player player = sessionPlayers.get(session); - super.closeSession(session, reason); - playerSessions.remove(player); + private void onEnemiesChanged(Player player, Enemy[] newEnemies, Enemy[] changedEnemies) { + } @Override @@ -75,9 +81,8 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { try { String payload = message.getPayload(); if (!Objects.equals( - objectMapper.readTree(payload).get("$id").asText(), - RequestTowerPlacingMessage.MESSAGE_ID - )) { + objectMapper.readTree(payload).get("$id").asText(), + RequestTowerPlacingMessage.MESSAGE_ID)) { this.closeSession(session, CloseStatus.BAD_DATA); return; } @@ -89,16 +94,15 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { } private void handleRequestTowerPlacingMessage( - WebSocketSession session, - String payload - ) throws IOException { + WebSocketSession session, + String payload) throws IOException { RequestTowerPlacingMessage msg = objectMapper.readValue(payload, RequestTowerPlacingMessage.class); Player opponent; try { opponent = this.matchService.placeTower( - this.sessionPlayers.get(session), - msg.getX(), - msg.getY() + this.sessionPlayers.get(session), + msg.getX(), + msg.getY() ); } catch (InvalidPlacementException exception) { new InvalidPlacementMessage(msg.getX(), msg.getY(), exception.getReason()).send(session); diff --git a/src/main/java/de/towerdefence/server/server/channels/match/enemy/EnemyUpdateMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/enemy/EnemyUpdateMessage.java new file mode 100644 index 0000000..c83c359 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/enemy/EnemyUpdateMessage.java @@ -0,0 +1,61 @@ +package de.towerdefence.server.server.channels.match.enemy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import de.towerdefence.server.game.Enemy; +import de.towerdefence.server.server.JsonMessage; +import lombok.AllArgsConstructor; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class EnemyUpdateMessage extends JsonMessage { + + private final Enemy[] newEnemies; + private final Enemy[] changedEnemies; + + @Override + protected String getMessageId() { + return "EnemyUpdate"; + } + + @Override + protected Map getData(JsonNodeFactory factory) { + + List newArray = Arrays + .stream(newEnemies) + .map((Enemy enemy) -> { + ObjectNode o = factory.objectNode(); + o.set("id", factory.numberNode(enemy.getId())); + o.set("x", factory.numberNode(enemy.getX())); + o.set("y", factory.numberNode(enemy.getY())); + return o; + }) + .map(JsonNode.class::cast) + .toList(); + + List changedArray = Arrays + .stream(changedEnemies) + .map((Enemy enemy) -> { + ObjectNode o = factory.objectNode(); + o.set("id", factory.numberNode(enemy.getId())); + o.set("x", factory.numberNode(enemy.getX())); + o.set("y", factory.numberNode(enemy.getY())); + return o; + }) + .map(JsonNode.class::cast) + .toList(); + + return Map.of( + "new", new ArrayNode(factory, newArray), + "changed", new ArrayNode(factory, changedArray) + ); + } +} diff --git a/ws/ws.yml b/ws/ws.yml index 2ac309b..d512df7 100644 --- a/ws/ws.yml +++ b/ws/ws.yml @@ -253,8 +253,23 @@ channels: required: - $id - playerHitpoints - - + EnemyUpdate: + description: All Information needed to updated the Enemy Information in Game + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + new: + type: array + changed: + type: array + required: + - $id + - new + - changed operations: requestConnectionToken: @@ -343,6 +358,13 @@ operations: $ref: "#/channels/match" messages: - $ref: "#/channels/match/messages/PlayerHitpoints" + enemyUpdate: + title: EnemyUpdate + action: receive + channel: + $ref: "#/channels/match" + messages: + - $ref: "#/channels/match/messages/EnemyUpdate"