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 new file mode 100644 index 0000000..cbd0112 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/Enemy.java @@ -0,0 +1,13 @@ +package de.towerdefence.server.game; + +import org.joml.Vector2i; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Enemy { + protected int id; + protected Vector2i pos; +} diff --git a/src/main/java/de/towerdefence/server/game/GameSession.java b/src/main/java/de/towerdefence/server/game/GameSession.java new file mode 100644 index 0000000..ec5f976 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/GameSession.java @@ -0,0 +1,146 @@ +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.game.pathfinding.AStar; +import de.towerdefence.server.game.pathfinding.NoPathException; +import de.towerdefence.server.game.pathfinding.OutOfBoundsException; +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 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 + private int money; + @Getter + private int playerHitpoints; + private final PlayerMoneyCallback moneyCallback; + private final PlayerHitpointsCallback hitpointsCallback; + private final EnemiesChangedCallback enemyCallback; + + 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.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(); + + for (int x = 0; x < enemies.length; x++) { + Enemy[] column = enemies[x]; + for (int y = 0; y < column.length; y++) { + Enemy enemy = column[y]; + if (changedEnemies.contains(enemy)) { + continue; + } + AStar a = new AStar((Vector2i pos) -> { + return pos.x >= 0 + && pos.x < MAP_SIZE.x + && pos.y >= 0 + && pos.y < MAP_SIZE.y + && (towers[pos.x] == null || towers[pos.x][pos.y] == null) + && (enemies[pos.x] == null || enemies[pos.x][pos.y] != null); + }); + Vector2i next; + try { + next = a.next(enemy.pos, WAVE_TARGET); + } catch (NoPathException | OutOfBoundsException exceptionO) { + throw new RuntimeException("TODO: Implement"); + } + enemy.pos = next; + enemies[x][y] = null; + enemies[next.x][next.y] = enemy; + changedEnemies.add(enemy); + } + } + + return changedEnemies; + } + + public void placeTower(Tower tower, Vector2i pos) throws InvalidPlacementException { + if (pos.x < 0 || pos.y < 0 || pos.x + 1 > MAP_SIZE.x || pos.y + 1 > MAP_SIZE.y) { + throw new InvalidPlacementException(InvalidPlacementReason.OUT_OF_BOUNDS); + } + if (towers[pos.x][pos.y] != null) { + throw new InvalidPlacementException(InvalidPlacementReason.LOCATION_USED); + } + 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.pos = pos; + towers[pos.x][pos.y] = tower; + } + + public void addMoney(int amount) { + money += amount; + moneyCallback.call(player, money); + } +} diff --git a/src/main/java/de/towerdefence/server/game/Tower.java b/src/main/java/de/towerdefence/server/game/Tower.java new file mode 100644 index 0000000..4c30055 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/Tower.java @@ -0,0 +1,14 @@ +package de.towerdefence.server.game; + +import org.joml.Vector2i; + +import lombok.Getter; + +@Getter +public class Tower { + public static final int COST = 20; + protected Vector2i pos; + + 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/match/callbacks/PlayerHitpointsCallback.java b/src/main/java/de/towerdefence/server/game/callbacks/PlayerHitpointsCallback.java similarity index 77% rename from src/main/java/de/towerdefence/server/match/callbacks/PlayerHitpointsCallback.java rename to src/main/java/de/towerdefence/server/game/callbacks/PlayerHitpointsCallback.java index ed1e959..83badd3 100644 --- a/src/main/java/de/towerdefence/server/match/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/match/callbacks/PlayerMoneyCallback.java b/src/main/java/de/towerdefence/server/game/callbacks/PlayerMoneyCallback.java similarity index 76% rename from src/main/java/de/towerdefence/server/match/callbacks/PlayerMoneyCallback.java rename to src/main/java/de/towerdefence/server/game/callbacks/PlayerMoneyCallback.java index ae35600..ebaacd0 100644 --- a/src/main/java/de/towerdefence/server/match/callbacks/PlayerMoneyCallback.java +++ b/src/main/java/de/towerdefence/server/game/callbacks/PlayerMoneyCallback.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/match/exeptions/InvalidPlacementException.java b/src/main/java/de/towerdefence/server/game/exeptions/InvalidPlacementException.java similarity index 80% rename from src/main/java/de/towerdefence/server/match/exeptions/InvalidPlacementException.java rename to src/main/java/de/towerdefence/server/game/exeptions/InvalidPlacementException.java index eea167d..9a3727b 100644 --- a/src/main/java/de/towerdefence/server/match/exeptions/InvalidPlacementException.java +++ b/src/main/java/de/towerdefence/server/game/exeptions/InvalidPlacementException.java @@ -1,4 +1,4 @@ -package de.towerdefence.server.match.exeptions; +package de.towerdefence.server.game.exeptions; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/de/towerdefence/server/match/exeptions/InvalidPlacementReason.java b/src/main/java/de/towerdefence/server/game/exeptions/InvalidPlacementReason.java similarity index 86% rename from src/main/java/de/towerdefence/server/match/exeptions/InvalidPlacementReason.java rename to src/main/java/de/towerdefence/server/game/exeptions/InvalidPlacementReason.java index f741f39..17cea2c 100644 --- a/src/main/java/de/towerdefence/server/match/exeptions/InvalidPlacementReason.java +++ b/src/main/java/de/towerdefence/server/game/exeptions/InvalidPlacementReason.java @@ -1,4 +1,4 @@ -package de.towerdefence.server.match.exeptions; +package de.towerdefence.server.game.exeptions; import lombok.AllArgsConstructor; import lombok.Getter; 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..00cbbdd --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java @@ -0,0 +1,103 @@ +package de.towerdefence.server.game.pathfinding; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.joml.Vector2i; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class AStar { + private final WalkableCallback walkable; + + public Vector2i next(Vector2i current, Vector2i target) throws NoPathException, OutOfBoundsException { + if (!walkable.call(current) || !walkable.call(target)) { + throw new OutOfBoundsException(); + } + + if (current.equals(target)) { + return target; + } + + Map remaining = new HashMap<>(); + Set evaluated = new HashSet<>(); + Map takenPath = new HashMap<>(); + + PathNode origin = new PathNode(current, 0, current.gridDistance(target)); + remaining.put(current, origin); + + while (!remaining.isEmpty()) { + PathNode currentNode = getLowestEstimate(remaining); + Vector2i currentPos = currentNode.pos; + + if (currentPos.equals(target)) { + return reconstructPath(takenPath, current); + } + + remaining.remove(currentPos); + evaluated.add(currentPos); + + long nextCost = currentNode.cost + 1; + for (PathNode neighbor : getNeighbors(currentPos, nextCost, target)) { + if (evaluated.contains(neighbor.pos)) { + continue; + } + if (!remaining.containsKey(neighbor.pos) || nextCost < remaining.get(neighbor.pos).cost) { + takenPath.put(neighbor.pos, currentPos); + remaining.put(neighbor.pos, neighbor); + } + } + } + + throw new NoPathException(); + } + + private Vector2i reconstructPath(Map takenPath, Vector2i origin) { + Vector2i current = takenPath.keySet().stream().reduce((first, second) -> second).orElse(null); + if (current == null) + return null; + while (takenPath.get(current) != null && !takenPath.get(current).equals(origin)) { + current = takenPath.get(current); + } + return current; + } + + private PathNode getLowestEstimate(Map openSet) { + PathNode lowestNode = null; + for (PathNode node : openSet.values()) { + if (lowestNode == null || node.estimate < lowestNode.estimate) { + lowestNode = node; + } + } + return lowestNode; + } + + private List getNeighbors(Vector2i pos, long nextCost, Vector2i target) { + List neighbors = new ArrayList<>(); + + addNeighbor(neighbors, new Vector2i(pos.x + 1, pos.y), nextCost, target); + addNeighbor(neighbors, new Vector2i(pos.x - 1, pos.y), nextCost, target); + addNeighbor(neighbors, new Vector2i(pos.x, pos.y + 1), nextCost, target); + addNeighbor(neighbors, new Vector2i(pos.x, pos.y - 1), nextCost, target); + return neighbors; + } + + private void addNeighbor(List accumulator, Vector2i pos, long nextCost, Vector2i target) { + Vector2i right = new Vector2i(pos.x + 1, pos.y); + if (!walkable.call(pos)) { + return; + } + accumulator.add( + new PathNode( + right, + nextCost, + right.gridDistance(target) + nextCost + ) + ); + } +} 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/game/pathfinding/OutOfBoundsException.java b/src/main/java/de/towerdefence/server/game/pathfinding/OutOfBoundsException.java new file mode 100644 index 0000000..3b77118 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/OutOfBoundsException.java @@ -0,0 +1,4 @@ +package de.towerdefence.server.game.pathfinding; + +public class OutOfBoundsException extends RuntimeException { +} diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/PathNode.java b/src/main/java/de/towerdefence/server/game/pathfinding/PathNode.java new file mode 100644 index 0000000..f7e951d --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/PathNode.java @@ -0,0 +1,12 @@ +package de.towerdefence.server.game.pathfinding; + +import org.joml.Vector2i; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +final class PathNode { + final Vector2i pos; + final long cost; // This is the cost to this Node + final long estimate; // This is the estimated remaining cost to the Target +} diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/WalkableCallback.java b/src/main/java/de/towerdefence/server/game/pathfinding/WalkableCallback.java new file mode 100644 index 0000000..90c737c --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/WalkableCallback.java @@ -0,0 +1,8 @@ +package de.towerdefence.server.game.pathfinding; + +import org.joml.Vector2i; + +@FunctionalInterface +public interface WalkableCallback { + boolean call(Vector2i pos); +} diff --git a/src/main/java/de/towerdefence/server/match/Enemy.java b/src/main/java/de/towerdefence/server/match/Enemy.java deleted file mode 100644 index c1997ff..0000000 --- a/src/main/java/de/towerdefence/server/match/Enemy.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.towerdefence.server.match; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@AllArgsConstructor -public class Enemy { - private int poxX; - private int posY; -} diff --git a/src/main/java/de/towerdefence/server/match/GameSession.java b/src/main/java/de/towerdefence/server/match/GameSession.java deleted file mode 100644 index 9bea44c..0000000 --- a/src/main/java/de/towerdefence/server/match/GameSession.java +++ /dev/null @@ -1,54 +0,0 @@ -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.player.Player; -import lombok.Getter; - -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]; - - private final Player player; - @Getter - private int money; - @Getter - private int playerHitpoints; - private final PlayerMoneyCallback moneyCallback; - //private final PlayerHitpointsCallback hitpointsCallback; - - public GameSession(Player player, PlayerMoneyCallback moneyCallback) { - this.player = player; - this.moneyCallback = moneyCallback; - //this.hitpointsCallback = hitpointsCallback; - this.money = START_MONEY; - this.playerHitpoints = START_HITPOINTS; - - } - - 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) { - throw new InvalidPlacementException(InvalidPlacementReason.OUT_OF_BOUNDS); - } - if (towers[x][y] != null) { - throw new InvalidPlacementException(InvalidPlacementReason.LOCATION_USED); - } - if (money < Tower.COST) { - throw new InvalidPlacementException(InvalidPlacementReason.NOT_ENOUGH_MONEY); - } - money -= Tower.COST; - moneyCallback.call(player, money); - towers[x][y] = tower; - } - - public void addMoney(int amount) { - money += amount; - moneyCallback.call(player, money); - } -} diff --git a/src/main/java/de/towerdefence/server/match/Match.java b/src/main/java/de/towerdefence/server/match/Match.java index 959592a..43655ed 100644 --- a/src/main/java/de/towerdefence/server/match/Match.java +++ b/src/main/java/de/towerdefence/server/match/Match.java @@ -1,12 +1,15 @@ 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; -import java.util.Optional; - @Getter public class Match { @@ -51,7 +54,8 @@ public class Match { public void connectPlayer( Player player, PlayerMoneyCallback moneyCallback, - PlayerHitpointsCallback hitpointsCallback + PlayerHitpointsCallback hitpointsCallback, + EnemiesChangedCallback enemyCallback ) { boolean isPlayer1 = player1.equals(player); boolean isPlayer2 = player2.equals(player); @@ -59,11 +63,11 @@ public class Match { 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 a337e29..306eaae 100644 --- a/src/main/java/de/towerdefence/server/match/MatchService.java +++ b/src/main/java/de/towerdefence/server/match/MatchService.java @@ -1,17 +1,25 @@ 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.joml.Vector2i; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import de.towerdefence.server.game.GameSession; +import de.towerdefence.server.game.Tower; +import de.towerdefence.server.game.callbacks.EnemiesChangedCallback; @Service public class MatchService { @@ -45,24 +53,25 @@ public class MatchService { if (opponent.isEmpty()) { throw new NotInMatchException(); } - playerSession.get().placeTower(new Tower(), x, y); + playerSession.get().placeTower(new Tower(), new Vector2i(x, y)); return opponent.get(); } public void playerConnected( Player player, PlayerMoneyCallback moneyCallback, - PlayerHitpointsCallback hitpointsCallback + 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; } @@ -77,13 +86,13 @@ public class MatchService { } GameSession opponentSession = optionalOpponentSession.get(); ScheduledFuture moneyTask = scheduler.scheduleAtFixedRate( - () -> { - playerSession.addMoney(3); - opponentSession.addMoney(3); - }, - 5, - 5, - TimeUnit.SECONDS); + () -> { + playerSession.addMoney(3); + opponentSession.addMoney(3); + }, + 5, + 5, + TimeUnit.SECONDS); moneyTasks.put(match, moneyTask); } } diff --git a/src/main/java/de/towerdefence/server/match/exeptions/NotInMatchException.java b/src/main/java/de/towerdefence/server/match/NotInMatchException.java similarity index 56% rename from src/main/java/de/towerdefence/server/match/exeptions/NotInMatchException.java rename to src/main/java/de/towerdefence/server/match/NotInMatchException.java index 8297e23..e704683 100644 --- a/src/main/java/de/towerdefence/server/match/exeptions/NotInMatchException.java +++ b/src/main/java/de/towerdefence/server/match/NotInMatchException.java @@ -1,4 +1,4 @@ -package de.towerdefence.server.match.exeptions; +package de.towerdefence.server.match; public class NotInMatchException extends RuntimeException { } diff --git a/src/main/java/de/towerdefence/server/match/Tower.java b/src/main/java/de/towerdefence/server/match/Tower.java deleted file mode 100644 index ae8b146..0000000 --- a/src/main/java/de/towerdefence/server/match/Tower.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.towerdefence.server.match; - -public class Tower { - public static final int COST = 20; -} 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 bc64f77..a155f33 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 @@ -1,11 +1,13 @@ package de.towerdefence.server.server.channels.match; 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.exeptions.InvalidPlacementException; -import de.towerdefence.server.match.exeptions.NotInMatchException; +import de.towerdefence.server.match.NotInMatchException; import de.towerdefence.server.player.Player; import de.towerdefence.server.server.JsonWebsocketHandler; +import de.towerdefence.server.server.channels.match.enemy.EnemyUpdateMessage; import de.towerdefence.server.server.channels.match.hitpoints.PlayerHitpointsMessage; import de.towerdefence.server.server.channels.match.money.PlayerMoneyMessage; import de.towerdefence.server.server.channels.match.placing.GamePlayerMap; @@ -14,14 +16,13 @@ import de.towerdefence.server.server.channels.match.placing.RequestTowerPlacingM import de.towerdefence.server.server.channels.match.placing.TowerPlacedMessage; import de.towerdefence.server.session.Channel; import de.towerdefence.server.session.SessionsService; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; - import java.io.IOException; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; public class MatchWebsocketHandler extends JsonWebsocketHandler { @@ -41,10 +42,18 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { matchService.playerConnected( sessionPlayers.get(session), this::onPlayerMoneyChanged, - this::onPlayerHitpointsChanged + 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 { @@ -61,11 +70,12 @@ 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) { + WebSocketSession session = playerSessions.get(player); + try { + new EnemyUpdateMessage(newEnemies, changedEnemies).send(session); + } catch (IOException ignored) { + } } @Override @@ -73,9 +83,9 @@ 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; } @@ -83,17 +93,15 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { } catch (IOException ignored) { this.closeSession(session, CloseStatus.BAD_DATA); } - } - private void handleRequestTowerPlacingMessage( - WebSocketSession session, - String payload - ) throws IOException { - RequestTowerPlacingMessage msg = objectMapper.readValue(payload, RequestTowerPlacingMessage.class); + private void handleRequestTowerPlacingMessage(WebSocketSession session, String payload) + throws IOException { + RequestTowerPlacingMessage msg = + objectMapper.readValue(payload, RequestTowerPlacingMessage.class); Player opponent; try { - opponent = this.matchService.placeTower( + opponent =this.matchService.placeTower( this.sessionPlayers.get(session), msg.getX(), msg.getY() 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..909e4f7 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/enemy/EnemyUpdateMessage.java @@ -0,0 +1,59 @@ +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; + +@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.getPos().x)); + o.set("y", factory.numberNode(enemy.getPos().y)); + 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.getPos().x)); + o.set("y", factory.numberNode(enemy.getPos().y)); + return o; + }) + .map(JsonNode.class::cast) + .toList(); + + return Map.of( + "new", new ArrayNode(factory, newArray), + "changed", new ArrayNode(factory, changedArray) + ); + } +} diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java index e96f542..0597fa8 100644 --- a/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java @@ -1,14 +1,14 @@ package de.towerdefence.server.server.channels.match.placing; +import java.util.Map; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import de.towerdefence.server.match.exeptions.InvalidPlacementReason; +import de.towerdefence.server.game.exeptions.InvalidPlacementReason; import de.towerdefence.server.server.JsonMessage; import lombok.AllArgsConstructor; -import java.util.Map; - @AllArgsConstructor public class InvalidPlacementMessage extends JsonMessage { @@ -24,8 +24,9 @@ public class InvalidPlacementMessage extends JsonMessage { @Override protected Map getData(JsonNodeFactory factory) { return Map.of( - "x", factory.numberNode(this.x), - "y", factory.numberNode(this.y), - "reason", factory.textNode(this.reason.getJsonName())); + "x", factory.numberNode(this.x), + "y", factory.numberNode(this.y), + "reason", factory.textNode(this.reason.getJsonName()) + ); } } diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java index fcf3a31..ed5be87 100644 --- a/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java @@ -22,9 +22,9 @@ public class TowerPlacedMessage extends JsonMessage { @Override protected Map getData(JsonNodeFactory factory) { return Map.of( - "x", factory.numberNode(this.x), - "y", factory.numberNode(this.y), - "map", factory.textNode(this.map.getJsonName()) - ); + "x", factory.numberNode(this.x), + "y", factory.numberNode(this.y), + "map", factory.textNode(this.map.getJsonName()) + ); } } diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml index d737fbf..d3a00dc 100644 --- a/src/main/resources/spotbugs-exclude.xml +++ b/src/main/resources/spotbugs-exclude.xml @@ -25,4 +25,10 @@ + + + + + + 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"