From f1a6f0fd759bc8968d7c1ce8cb10878dc8a2e17a Mon Sep 17 00:00:00 2001 From: wfz <1040079213@qq.com> Date: Sun, 26 Apr 2026 11:00:59 +0800 Subject: [PATCH] 1 --- backend/pom.xml | 5 + .../java/com/zombie/game/GameServerMain.java | 16 +- .../java/com/zombie/game/model/Bullet.java | 6 +- .../java/com/zombie/game/model/Constants.java | 2 +- .../java/com/zombie/game/model/GameMap.java | 193 ++++++++++------- .../java/com/zombie/game/model/GameWorld.java | 76 ++++++- .../main/java/com/zombie/game/model/Loot.java | 2 + .../java/com/zombie/game/model/MapData.java | 120 ++++++++++ .../java/com/zombie/game/model/Player.java | 2 + .../main/java/com/zombie/game/model/Room.java | 2 + .../java/com/zombie/game/model/Zombie.java | 162 +++++++++----- .../game/server/GameWebSocketServer.java | 1 + .../game/server/MapDesignerApiServer.java | 160 ++++++++++++++ .../com/zombie/game/server/MapStorage.java | 125 +++++++++++ frontend/map-designer/index.html | 205 ++++++++++++++++++ frontend/src/game/engine.js | 5 + frontend/src/game/scene.js | 81 +++++++ frontend/vite.config.js | 4 + maps/d540209a.json | 1 + 19 files changed, 1026 insertions(+), 142 deletions(-) create mode 100644 backend/src/main/java/com/zombie/game/model/MapData.java create mode 100644 backend/src/main/java/com/zombie/game/server/MapDesignerApiServer.java create mode 100644 backend/src/main/java/com/zombie/game/server/MapStorage.java create mode 100644 frontend/map-designer/index.html create mode 100644 maps/d540209a.json diff --git a/backend/pom.xml b/backend/pom.xml index 0d23ef9..f63a2f2 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -32,6 +32,11 @@ gson 2.10.1 + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + org.slf4j slf4j-simple diff --git a/backend/src/main/java/com/zombie/game/GameServerMain.java b/backend/src/main/java/com/zombie/game/GameServerMain.java index e4a2d7e..54d4101 100644 --- a/backend/src/main/java/com/zombie/game/GameServerMain.java +++ b/backend/src/main/java/com/zombie/game/GameServerMain.java @@ -1,16 +1,21 @@ package com.zombie.game; import com.zombie.game.server.GameWebSocketServer; +import com.zombie.game.server.MapDesignerApiServer; +import com.zombie.game.server.MapStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + /** * 游戏服务器主入口类 * * 职责: * 1. 解析命令行参数(端口号) * 2. 启动 WebSocket 游戏服务器 - * 3. 注册 JVM 关闭钩子,确保服务器优雅退出 + * 3. 启动地图设计器 API 服务器 + * 4. 注册 JVM 关闭钩子,确保服务器优雅退出 */ public class GameServerMain { private static final Logger logger = LoggerFactory.getLogger(GameServerMain.class); @@ -20,7 +25,7 @@ public class GameServerMain { * * @param args 命令行参数,第一个参数可选为端口号(默认 8080) */ - public static void main(String[] args) { + public static void main(String[] args) throws IOException { // 默认监听端口 int port = 8080; @@ -37,13 +42,20 @@ public class GameServerMain { GameWebSocketServer server = new GameWebSocketServer(port); server.start(); + // 创建并启动地图设计器 API 服务器 + MapStorage mapStorage = new MapStorage(); + MapDesignerApiServer apiServer = new MapDesignerApiServer(mapStorage); + apiServer.start(); + logger.info("Zombie Crisis 3 Server started on port {}", port); + logger.info("Map Designer API Server started on port 8081"); logger.info("Press Ctrl+C to stop the server"); // 注册关闭钩子:当 JVM 退出时(Ctrl+C 或系统信号),优雅地停止服务器 Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("Shutting down server..."); try { + apiServer.stop(); server.stop(); } catch (Exception e) { logger.error("Error during server shutdown", e); diff --git a/backend/src/main/java/com/zombie/game/model/Bullet.java b/backend/src/main/java/com/zombie/game/model/Bullet.java index c233106..e18a419 100644 --- a/backend/src/main/java/com/zombie/game/model/Bullet.java +++ b/backend/src/main/java/com/zombie/game/model/Bullet.java @@ -3,6 +3,8 @@ package com.zombie.game.model; import lombok.Getter; import java.util.*; +import static com.zombie.game.model.Constants.*; + /** * 子弹/投掷物类 * @@ -123,7 +125,9 @@ public class Bullet { int gx = (int) Math.floor(x); int gy = (int) Math.floor(y); - if (map.isWall(gx, gy)) return false; + // 只有碰到静态墙壁才销毁子弹,碰到坚果墙体让碰撞检测处理 + Wall wall = map.getWall(gx, gy); + if (wall instanceof StaticWall) return false; return true; } diff --git a/backend/src/main/java/com/zombie/game/model/Constants.java b/backend/src/main/java/com/zombie/game/model/Constants.java index aece0d9..f0fa99e 100644 --- a/backend/src/main/java/com/zombie/game/model/Constants.java +++ b/backend/src/main/java/com/zombie/game/model/Constants.java @@ -47,7 +47,7 @@ public class Constants { /** 僵尸生成间隔基础值(秒),难度提升后会逐渐缩短 */ public static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f; /** 僵尸生成间隔最小值(秒),防止生成过快 */ - public static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.5f; + public static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.2f; /** 每次难度提升增加的僵尸生命值 */ public static final float ZOMBIE_HEALTH_INCREASE = 20; /** 每次难度提升增加的僵尸速度 */ diff --git a/backend/src/main/java/com/zombie/game/model/GameMap.java b/backend/src/main/java/com/zombie/game/model/GameMap.java index d58273f..0a42b79 100644 --- a/backend/src/main/java/com/zombie/game/model/GameMap.java +++ b/backend/src/main/java/com/zombie/game/model/GameMap.java @@ -1,6 +1,9 @@ package com.zombie.game.model; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Getter; +import java.io.File; import java.util.*; /** @@ -23,99 +26,97 @@ public class GameMap { private final Map walls; /** 流场导航 */ private final FlowField flowField; + /** 玩家出生点 */ + private final List spawnPoints; + /** 僵尸出生点 */ + private final List zombieSpawnPoints; /** - * 构造函数 - 初始化地图并生成默认布局 + * 构造函数 - 从 JSON 文件加载地图 + * + * @param mapFilePath 地图 JSON 文件路径 */ - public GameMap() { - this.width = Constants.GRID_SIZE; - this.height = Constants.GRID_SIZE; + public GameMap(String mapFilePath) { + JsonNode root = loadMapFile(mapFilePath); + this.width = root.get("width").asInt(); + this.height = root.get("height").asInt(); this.cells = new int[height][width]; this.walls = new HashMap<>(); this.flowField = new FlowField(width, height); - generateDefaultMap(); + this.spawnPoints = new ArrayList<>(); + this.zombieSpawnPoints = new ArrayList<>(); + loadFromJson(root); } /** - * 生成默认地图布局 - * - * 创建边界墙壁、内部障碍物、玩家出生点和僵尸出生点 + * 加载地图 JSON 文件 */ - private void generateDefaultMap() { - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - if (x == 0 || x == width - 1 || y == 0 || y == height - 1) { - cells[y][x] = 0; + private JsonNode loadMapFile(String mapFilePath) { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(new File(mapFilePath)); + } catch (Exception e) { + throw new RuntimeException("Failed to load map file: " + mapFilePath, e); + } + } + + /** + * 从 JSON 数据加载地图 + */ + private void loadFromJson(JsonNode root) { + // 加载墙体 + JsonNode wallsNode = root.get("walls"); + if (wallsNode != null && wallsNode.isArray()) { + for (JsonNode wallNode : wallsNode) { + int x = (int) wallNode.get("x").asDouble(); + int y = (int) wallNode.get("y").asDouble(); + String type = wallNode.get("type").asText(); + + if (x < 0 || x >= width || y < 0 || y >= height) continue; + + if ("static".equals(type)) { walls.put(key(x, y), new StaticWall(x, y)); - } else { - cells[y][x] = 0; + } else if ("nut".equals(type)) { + walls.put(key(x, y), new NutWall(x, y)); } } } - int[][] wallSegments = { - {5, 5, 5, 10}, {10, 3, 15, 3}, {20, 5, 20, 12}, - {8, 15, 14, 15}, {25, 10, 25, 18}, {3, 20, 8, 20}, - {15, 20, 15, 26}, {22, 22, 28, 22}, {5, 25, 10, 25}, - {12, 8, 12, 12}, {18, 16, 22, 16}, {27, 5, 27, 9}, - {8, 27, 13, 27}, {18, 26, 18, 30} - }; - - for (int[] seg : wallSegments) { - if (seg[0] == seg[2]) { - for (int y = seg[1]; y <= seg[3]; y++) { - if (seg[0] > 0 && seg[0] < width - 1 && y > 0 && y < height - 1) { - walls.put(key(seg[0], y), new StaticWall(seg[0], y)); - } - } - } else { - for (int x = seg[0]; x <= seg[2]; x++) { - if (x > 0 && x < width - 1 && seg[1] > 0 && seg[1] < height - 1) { - walls.put(key(x, seg[1]), new StaticWall(x, seg[1])); - } + // 加载玩家出生点 + JsonNode playerSpawns = root.get("playerSpawns"); + if (playerSpawns != null && playerSpawns.isArray()) { + for (JsonNode spawn : playerSpawns) { + int x = spawn.get("x").asInt(); + int y = spawn.get("y").asInt(); + if (x >= 0 && x < width && y >= 0 && y < height) { + cells[y][x] = 2; + spawnPoints.add(new int[]{x, y}); } } } - int[][] spawnPoints = {{2, 2}, {29, 2}, {2, 29}, {29, 29}}; - for (int[] sp : spawnPoints) { - cells[sp[1]][sp[0]] = 2; - for (int dy = -1; dy <= 1; dy++) { - for (int dx = -1; dx <= 1; dx++) { - int ny = sp[1] + dy, nx = sp[0] + dx; - if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) { - walls.remove(key(nx, ny)); - } - } - } - } - - int[][] zombieSpawnAreas = {{16, 2}, {2, 16}, {29, 16}, {16, 29}, {16, 16}}; - for (int[] sp : zombieSpawnAreas) { - for (int dy = -1; dy <= 1; dy++) { - for (int dx = -1; dx <= 1; dx++) { - int ny = sp[1] + dy, nx = sp[0] + dx; - if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) { - walls.remove(key(nx, ny)); - } - } - } - } - - int[][] zombieSpawnPoints = {{8, 8}, {24, 24}}; - for (int[] sp : zombieSpawnPoints) { - cells[sp[1]][sp[0]] = 3; - for (int dy = -1; dy <= 1; dy++) { - for (int dx = -1; dx <= 1; dx++) { - int ny = sp[1] + dy, nx = sp[0] + dx; - if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) { - walls.remove(key(nx, ny)); - } + // 加载僵尸出生点 + JsonNode zombieSpawns = root.get("zombieSpawns"); + if (zombieSpawns != null && zombieSpawns.isArray()) { + for (JsonNode spawn : zombieSpawns) { + int x = spawn.get("x").asInt(); + int y = spawn.get("y").asInt(); + if (x >= 0 && x < width && y >= 0 && y < height) { + cells[y][x] = 3; + zombieSpawnPoints.add(new int[]{x, y}); } } } } + /** + * 添加坚果墙体 + * + * @param gx 格子X坐标 + * @param gy 格子Y坐标 + * @return true 表示添加成功 + */ + /** * 添加坚果墙体 * @@ -201,8 +202,53 @@ public class GameMap { return true; } + /** + * 获取地图数据(包含墙体信息) + * + * 返回的二维数组格式与前端兼容: + * - 0 = 空地 + * - 1 = 静态墙壁 + * - 2 = 玩家出生点 + * - 3 = 僵尸出生点 + * - 4 = 坚果墙体 + * + * @return 地图格子数据 + */ public int[][] getCells() { - return cells; + int[][] result = new int[height][width]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + Wall wall = walls.get(key(x, y)); + if (wall instanceof StaticWall) { + result[y][x] = 1; + } else if (wall instanceof NutWall) { + result[y][x] = 4; + } else { + result[y][x] = cells[y][x]; + } + } + } + return result; + } + + /** + * 获取所有坚果墙体的状态(用于同步给前端) + * + * @return 坚果墙体状态列表,每个元素包含 x, y, health, maxHealth + */ + public List> getNutWallStates() { + List> states = new ArrayList<>(); + for (Wall wall : walls.values()) { + if (wall instanceof NutWall && !wall.isDestroyed()) { + Map state = new LinkedHashMap<>(); + state.put("x", wall.getGridX()); + state.put("y", wall.getGridY()); + state.put("health", wall.getHealth()); + state.put("maxHealth", NutWall.MAX_HEALTH); + states.add(state); + } + } + return states; } /** @@ -228,12 +274,7 @@ public class GameMap { * @return 僵尸出生点坐标列表 */ public List getZombieSpawnPoints() { - List points = new ArrayList<>(); - int[][] zombieSpawns = {{8, 8}, {24, 24}}; - for (int[] sp : zombieSpawns) { - points.add(sp); - } - return points; + return zombieSpawnPoints; } /** diff --git a/backend/src/main/java/com/zombie/game/model/GameWorld.java b/backend/src/main/java/com/zombie/game/model/GameWorld.java index d98a17d..19ca980 100644 --- a/backend/src/main/java/com/zombie/game/model/GameWorld.java +++ b/backend/src/main/java/com/zombie/game/model/GameWorld.java @@ -3,6 +3,8 @@ package com.zombie.game.model; import lombok.Getter; import java.util.*; +import static com.zombie.game.model.Constants.*; + /** * 游戏世界类 * @@ -39,7 +41,11 @@ public class GameWorld { private List removedZombieBullets; public GameWorld() { - this.map = new GameMap(); + this("/Users/wfz/workspace/zp1/maps/d540209a.json"); + } + + public GameWorld(String mapFilePath) { + this.map = new GameMap(mapFilePath); this.players = new LinkedHashMap<>(); this.zombies = new LinkedHashMap<>(); this.bullets = new LinkedHashMap<>(); @@ -206,17 +212,17 @@ public class GameWorld { /** * 更新所有僵尸 - * + * * 处理僵尸移动、攻击和死亡 - * + * * @param dt 时间增量(秒) */ private void updateZombies(float dt) { long now = System.currentTimeMillis(); - + List sortedZombies = new ArrayList<>(zombies.values()); sortedZombies.sort((a, b) -> Integer.compare(a.getId(), b.getId())); - + for (Zombie z : sortedZombies) { if (!z.isAlive()) { onZombieKilled(z); @@ -235,7 +241,14 @@ public class GameWorld { } } - z.move(map, dt, zombies.values()); + Wall attackedWall = z.move(map, dt, zombies.values(), now); + if (attackedWall != null && z.canAttack(now)) { + attackedWall.takeDamage(1.0f); // 每次攻击造成1点伤害 + z.attack(now); + if (attackedWall.isDestroyed()) { + map.removeWall(attackedWall.getGridX(), attackedWall.getGridY()); + } + } } } @@ -278,19 +291,40 @@ public class GameWorld { } /** - * 检测僵尸子弹与玩家的碰撞 + * 检测僵尸子弹与玩家/墙体的碰撞 */ private void checkZombieBulletCollisions() { List bulletsToRemove = new ArrayList<>(); for (Bullet b : new ArrayList<>(zombieBullets.values())) { + boolean hit = false; + + // 检测是否命中玩家 for (Player p : new ArrayList<>(players.values())) { if (!p.isAlive()) continue; if (b.hitsEntity(p.getX(), p.getY(), Constants.PLAYER_SIZE)) { p.takeDamage(b.getDamage()); - bulletsToRemove.add(b.getId()); + hit = true; break; } } + + // 检测是否命中坚果墙体 + if (!hit) { + int gx = (int) Math.floor(b.getX()); + int gy = (int) Math.floor(b.getY()); + Wall wall = map.getWall(gx, gy); + if (wall instanceof NutWall && !wall.isDestroyed()) { + wall.takeDamage(b.getDamage()); + hit = true; + if (wall.isDestroyed()) { + map.removeWall(gx, gy); + } + } + } + + if (hit) { + bulletsToRemove.add(b.getId()); + } } for (int id : bulletsToRemove) { zombieBullets.remove(id); @@ -352,20 +386,41 @@ public class GameWorld { } /** - * 检测玩家子弹与僵尸的碰撞 + * 检测玩家子弹与僵尸/墙体的碰撞 */ private void checkBulletCollisions() { List bulletsToRemove = new ArrayList<>(); for (Bullet b : new ArrayList<>(bullets.values())) { if (b.isGrenade()) continue; + + // 检测是否命中僵尸 + boolean hit = false; for (Zombie z : new ArrayList<>(zombies.values())) { if (!z.isAlive()) continue; if (b.hitsEntity(z.getX(), z.getY(), Constants.ZOMBIE_SIZE)) { z.takeDamage(b.getDamage()); - bulletsToRemove.add(b.getId()); + hit = true; break; } } + + // 检测是否命中坚果墙体 + if (!hit) { + int gx = (int) Math.floor(b.getX()); + int gy = (int) Math.floor(b.getY()); + Wall wall = map.getWall(gx, gy); + if (wall instanceof NutWall && !wall.isDestroyed()) { + wall.takeDamage(b.getDamage()); + hit = true; + if (wall.isDestroyed()) { + map.removeWall(gx, gy); + } + } + } + + if (hit) { + bulletsToRemove.add(b.getId()); + } } for (int id : bulletsToRemove) { bullets.remove(id); @@ -615,6 +670,7 @@ public class GameWorld { state.put("explosions", new ArrayList<>(explosions)); state.put("removedBullets", new ArrayList<>(removedBullets)); + state.put("nutWalls", map.getNutWallStates()); state.put("gameTime", gameTime); state.put("waveNumber", waveNumber); state.put("score", score); diff --git a/backend/src/main/java/com/zombie/game/model/Loot.java b/backend/src/main/java/com/zombie/game/model/Loot.java index 352e554..33a7e53 100644 --- a/backend/src/main/java/com/zombie/game/model/Loot.java +++ b/backend/src/main/java/com/zombie/game/model/Loot.java @@ -3,6 +3,8 @@ package com.zombie.game.model; import lombok.Getter; import java.util.*; +import static com.zombie.game.model.Constants.*; + /** * 掉落物类 * diff --git a/backend/src/main/java/com/zombie/game/model/MapData.java b/backend/src/main/java/com/zombie/game/model/MapData.java new file mode 100644 index 0000000..7e927c3 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/model/MapData.java @@ -0,0 +1,120 @@ +package com.zombie.game.model; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; + +/** + * 地图数据类 + * + * 用于JSON序列化和反序列化 + */ +public class MapData { + private String id; + private String name; + private int width; + private int height; + private List> walls; + private List> playerSpawns; + private List> zombieSpawns; + + public MapData() { + this.walls = new ArrayList<>(); + this.playerSpawns = new ArrayList<>(); + this.zombieSpawns = new ArrayList<>(); + } + + public MapData(String id, String name, int width, int height, + List> walls, + List> playerSpawns, + List> zombieSpawns) { + this.id = id; + this.name = name; + this.width = width; + this.height = height; + this.walls = walls; + this.playerSpawns = playerSpawns; + this.zombieSpawns = zombieSpawns; + } + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getWidth() { return width; } + public void setWidth(int width) { this.width = width; } + public int getHeight() { return height; } + public void setHeight(int height) { this.height = height; } + public List> getWalls() { return walls; } + public void setWalls(List> walls) { this.walls = walls; } + public List> getPlayerSpawns() { return playerSpawns; } + public void setPlayerSpawns(List> playerSpawns) { this.playerSpawns = playerSpawns; } + public List> getZombieSpawns() { return zombieSpawns; } + public void setZombieSpawns(List> zombieSpawns) { this.zombieSpawns = zombieSpawns; } + + public int[][] toCells() { + int[][] cells = new int[height][width]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + cells[y][x] = 0; + } + } + for (Map wall : walls) { + int x = (Integer) wall.get("x"); + int y = (Integer) wall.get("y"); + if (x >= 0 && x < width && y >= 0 && y < height) { + cells[y][x] = "nut".equals(wall.get("type")) ? 2 : 1; + } + } + for (Map spawn : playerSpawns) { + int x = spawn.get("x"); + int y = spawn.get("y"); + if (x >= 0 && x < width && y >= 0 && y < height) { + cells[y][x] = 2; + } + } + for (Map spawn : zombieSpawns) { + int x = spawn.get("x"); + int y = spawn.get("y"); + if (x >= 0 && x < width && y >= 0 && y < height) { + cells[y][x] = 3; + } + } + return cells; + } + + public static MapData fromGameMap(String id, String name, GameMap map) { + List> walls = new ArrayList<>(); + List> playerSpawns = new ArrayList<>(); + List> zombieSpawns = new ArrayList<>(); + + for (Map.Entry entry : map.getWalls().entrySet()) { + String[] parts = entry.getKey().split(","); + int x = Integer.parseInt(parts[0]); + int y = Integer.parseInt(parts[1]); + Wall wall = entry.getValue(); + Map wallData = new HashMap<>(); + wallData.put("x", x); + wallData.put("y", y); + wallData.put("type", wall instanceof NutWall ? "nut" : "static"); + walls.add(wallData); + } + + for (int[] spawn : map.getSpawnPoints()) { + Map sp = new HashMap<>(); + sp.put("x", spawn[0]); + sp.put("y", spawn[1]); + playerSpawns.add(sp); + } + + for (int[] spawn : map.getZombieSpawnPoints()) { + Map sp = new HashMap<>(); + sp.put("x", spawn[0]); + sp.put("y", spawn[1]); + zombieSpawns.add(sp); + } + + return new MapData(id, name, map.getWidth(), map.getHeight(), walls, playerSpawns, zombieSpawns); + } +} diff --git a/backend/src/main/java/com/zombie/game/model/Player.java b/backend/src/main/java/com/zombie/game/model/Player.java index 2818ac9..0c1a89d 100644 --- a/backend/src/main/java/com/zombie/game/model/Player.java +++ b/backend/src/main/java/com/zombie/game/model/Player.java @@ -3,6 +3,8 @@ package com.zombie.game.model; import lombok.Getter; import java.util.*; +import static com.zombie.game.model.Constants.*; + /** * 玩家类 * diff --git a/backend/src/main/java/com/zombie/game/model/Room.java b/backend/src/main/java/com/zombie/game/model/Room.java index b3127bc..ac98b63 100644 --- a/backend/src/main/java/com/zombie/game/model/Room.java +++ b/backend/src/main/java/com/zombie/game/model/Room.java @@ -3,6 +3,8 @@ package com.zombie.game.model; import lombok.Getter; import java.util.*; +import static com.zombie.game.model.Constants.*; + /** * 游戏房间类 * diff --git a/backend/src/main/java/com/zombie/game/model/Zombie.java b/backend/src/main/java/com/zombie/game/model/Zombie.java index 92d025f..7fff99a 100644 --- a/backend/src/main/java/com/zombie/game/model/Zombie.java +++ b/backend/src/main/java/com/zombie/game/model/Zombie.java @@ -3,6 +3,8 @@ package com.zombie.game.model; import lombok.Getter; import java.util.*; +import static com.zombie.game.model.Constants.*; + /** * 僵尸类 * @@ -80,49 +82,84 @@ public class Zombie { /** * 移动僵尸 - * + * * 基于流场导航移动,包含: - * - 路径规划 + * - 路径规划(支持加权障碍物,自动权衡绕道 vs 摧毁) * - 避免与其他僵尸重叠 * - 墙壁碰撞检测 - * + * - 攻击坚果墙体逻辑 + * * @param map 游戏地图 * @param dt 时间增量(秒) * @param otherZombies 其他僵尸集合 + * @param now 当前时间戳 + * @return 如果正在攻击墙体,返回被攻击的墙体对象;否则返回 null */ - public void move(GameMap map, float dt, Collection otherZombies) { - if (!map.isFlowFieldValid()) return; - + public Wall move(GameMap map, float dt, Collection otherZombies, long now) { + if (!map.isFlowFieldValid()) return null; + int currentGridX = (int) Math.floor(x); int currentGridY = (int) Math.floor(y); - + + // 如果正在攻击墙体,检查是否到达攻击位置 + if (attackingWall && attackingWallGridX >= 0) { + float wallCenterX = attackingWallGridX + 0.5f; + float wallCenterY = attackingWallGridY + 0.5f; + float distToWall = distanceTo(wallCenterX, wallCenterY); + + if (distToWall < 0.8f) { + // 到达攻击位置,返回要攻击的墙体 + Wall wall = map.getWall(attackingWallGridX, attackingWallGridY); + if (wall != null && !wall.isDestroyed()) { + return wall; + } + // 墙体已被破坏或被移除,停止攻击 + attackingWall = false; + attackingWallGridX = -1; + attackingWallGridY = -1; + } + } + float centerDist = Float.MAX_VALUE; if (hasTarget) { float dx = targetX - x; float dy = targetY - y; centerDist = (float) Math.sqrt(dx * dx + dy * dy); } - + if (!hasTarget || centerDist < 0.15f) { float[] flowDir = map.getFlowDirection(x, y); float dirX = flowDir[0]; float dirY = flowDir[1]; - - if (dirX == 0 && dirY == 0) return; - + + if (dirX == 0 && dirY == 0) return null; + float len = (float) Math.sqrt(dirX * dirX + dirY * dirY); if (len > 0) { dirX /= len; dirY /= len; } - + int nextGridX = currentGridX + (int) Math.round(dirX); int nextGridY = currentGridY + (int) Math.round(dirY); - - if (map.isWall(nextGridX, nextGridY)) { + + // 检查下一个格子是否是坚果墙体 + if (map.isNutWall(nextGridX, nextGridY)) { + // 流场指引我们走向坚果,说明摧毁代价比绕道低 + // 设置攻击状态 + attackingWall = true; + attackingWallGridX = nextGridX; + attackingWallGridY = nextGridY; + reservedGridX = nextGridX; + reservedGridY = nextGridY; + reservation = true; + targetX = nextGridX + 0.5f; + targetY = nextGridY + 0.5f; + hasTarget = true; + } else if (map.isWall(nextGridX, nextGridY)) { nextGridX = currentGridX + (int) Math.signum(dirX); nextGridY = currentGridY + (int) Math.signum(dirY); - + if (map.isWall(nextGridX, nextGridY)) { if (!map.isWall(currentGridX + (int) Math.signum(dirX), currentGridY)) { nextGridX = currentGridX + (int) Math.signum(dirX); @@ -132,67 +169,86 @@ public class Zombie { nextGridY = currentGridY + (int) Math.signum(dirY); } else { reservation = false; - return; + return null; } } - } - - if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) { - int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies); - if (altDirs != null) { - nextGridX = altDirs[0]; - nextGridY = altDirs[1]; - } else { - reservation = false; - return; + + if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) { + int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies); + if (altDirs != null) { + nextGridX = altDirs[0]; + nextGridY = altDirs[1]; + } else { + reservation = false; + return null; + } } + + reservedGridX = nextGridX; + reservedGridY = nextGridY; + reservation = true; + + targetX = nextGridX + 0.5f; + targetY = nextGridY + 0.5f; + hasTarget = true; + } else { + if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) { + int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies); + if (altDirs != null) { + nextGridX = altDirs[0]; + nextGridY = altDirs[1]; + } else { + reservation = false; + return null; + } + } + + reservedGridX = nextGridX; + reservedGridY = nextGridY; + reservation = true; + + targetX = nextGridX + 0.5f; + targetY = nextGridY + 0.5f; + hasTarget = true; } - - reservedGridX = nextGridX; - reservedGridY = nextGridY; - reservation = true; - - targetX = nextGridX + 0.5f; - targetY = nextGridY + 0.5f; - hasTarget = true; } - + float dx = targetX - x; float dy = targetY - y; float dist = (float) Math.sqrt(dx * dx + dy * dy); - + if (dist < 0.01f) { hasTarget = false; - return; + return null; } - + float dirX = dx / dist; float dirY = dy / dist; - + float moveX = dirX * speed * dt; float moveY = dirY * speed * dt; - + float newX = x + moveX; float newY = y + moveY; - + boolean canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE); boolean canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE); boolean canMoveDiagonal = map.isWalkable(newX, newY, Constants.ZOMBIE_SIZE); - + if (moveX != 0 && moveY != 0) { int checkX = (int) Math.floor(newX); int checkY = (int) Math.floor(newY); int checkCurrentX = (int) Math.floor(x); int checkCurrentY = (int) Math.floor(y); - + boolean blockedByCorner = false; if (checkX != checkCurrentX && checkY != checkCurrentY) { boolean wallInX = map.isWall(checkX, checkCurrentY); boolean wallInY = map.isWall(checkCurrentX, checkY); - + if (wallInX || wallInY) { blockedByCorner = true; - + if (!wallInX && canMoveX) { x = newX; canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE); @@ -202,7 +258,7 @@ public class Zombie { } } } - + if (!blockedByCorner && canMoveDiagonal) { x = newX; y = newY; @@ -214,26 +270,26 @@ public class Zombie { if (canMoveX) x = newX; if (canMoveY) y = newY; } - + float minSeparationDist = Constants.ZOMBIE_SIZE; for (Zombie other : otherZombies) { if (other.getId() == this.id) continue; if (!other.isAlive()) continue; - + float ox = other.getX(); float oy = other.getY(); float sepDx = x - ox; float sepDy = y - oy; float sepDist = (float) Math.sqrt(sepDx * sepDx + sepDy * sepDy); - + if (sepDist < minSeparationDist && sepDist > 0.01f) { float overlap = minSeparationDist - sepDist; float pushX = (sepDx / sepDist) * overlap * 0.5f; float pushY = (sepDy / sepDist) * overlap * 0.5f; - + float pushedX = x + pushX; float pushedY = y + pushY; - + if (map.isWalkable(pushedX, pushedY, Constants.ZOMBIE_SIZE)) { x = pushedX; y = pushedY; @@ -247,10 +303,12 @@ public class Zombie { } } } - + if (dirX != 0 || dirY != 0) { angle = (float) Math.atan2(dirX, dirY); } + + return null; } /** diff --git a/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java b/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java index f13111b..59e1521 100644 --- a/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java +++ b/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java @@ -3,6 +3,7 @@ package com.zombie.game.server; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.zombie.game.model.*; +import com.zombie.game.model.Constants; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; diff --git a/backend/src/main/java/com/zombie/game/server/MapDesignerApiServer.java b/backend/src/main/java/com/zombie/game/server/MapDesignerApiServer.java new file mode 100644 index 0000000..80a5bc2 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/server/MapDesignerApiServer.java @@ -0,0 +1,160 @@ +package com.zombie.game.server; + +import com.sun.net.httpserver.*; +import com.google.gson.Gson; +import com.zombie.game.model.MapData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; + +/** + * 地图设计器 HTTP API 服务器 + * + * 提供REST API用于地图的保存、加载和列表查询 + */ +public class MapDesignerApiServer { + private static final Logger logger = LoggerFactory.getLogger(MapDesignerApiServer.class); + private static final int PORT = 8081; + private static final String MAPS_ENDPOINT = "/api/maps"; + private static final String MAP_ID_ENDPOINT = "/api/maps/"; + + private final HttpServer server; + private final Gson gson; + private final MapStorage mapStorage; + + public MapDesignerApiServer(MapStorage mapStorage) throws IOException { + this.mapStorage = mapStorage; + this.gson = new Gson(); + this.server = HttpServer.create(new InetSocketAddress(PORT), 0); + } + + public void start() { + server.createContext(MAPS_ENDPOINT, new MapsHandler()); + server.createContext(MAP_ID_ENDPOINT, new MapIdHandler()); + server.setExecutor(null); + server.start(); + logger.info("Map Designer API Server started on port {}", PORT); + } + + public void stop() { + server.stop(0); + logger.info("Map Designer API Server stopped"); + } + + private class MapsHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if ("OPTIONS".equals(exchange.getRequestMethod())) { + sendResponse(exchange, 200, "{\"ok\":true}"); + return; + } + + String method = exchange.getRequestMethod(); + + try { + switch (method) { + case "GET": + handleList(exchange); + break; + case "POST": + handleSave(exchange); + break; + default: + sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}"); + } + } catch (Exception e) { + logger.error("Error handling request", e); + sendResponse(exchange, 500, "{\"error\":\"" + e.getMessage() + "\"}"); + } + } + } + + private class MapIdHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String path = exchange.getRequestURI().getPath(); + String id = path.substring(MAP_ID_ENDPOINT.length()); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + sendResponse(exchange, 200, "{\"ok\":true}"); + return; + } + + String method = exchange.getRequestMethod(); + + try { + switch (method) { + case "GET": + handleLoad(exchange, id); + break; + case "DELETE": + handleDelete(exchange, id); + break; + default: + sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}"); + } + } catch (Exception e) { + logger.error("Error handling request", e); + sendResponse(exchange, 500, "{\"error\":\"" + e.getMessage() + "\"}"); + } + } + } + + private void handleList(HttpExchange exchange) throws IOException { + List maps = mapStorage.listMaps(); + String response = gson.toJson(Map.of("maps", maps)); + sendJson(exchange, 200, response); + } + + private void handleSave(HttpExchange exchange) throws IOException { + java.io.Reader reader = new java.io.InputStreamReader(exchange.getRequestBody()); + MapData mapData = gson.fromJson(reader, MapData.class); + MapData saved = mapStorage.save(mapData); + sendJson(exchange, 201, gson.toJson(Map.of("id", saved.getId(), "ok", true))); + } + + private void handleLoad(HttpExchange exchange, String id) throws IOException { + MapData mapData = mapStorage.load(id); + if (mapData == null) { + sendResponse(exchange, 404, "{\"error\":\"Map not found\"}"); + } else { + sendJson(exchange, 200, gson.toJson(mapData)); + } + } + + private void handleDelete(HttpExchange exchange, String id) throws IOException { + boolean deleted = mapStorage.delete(id); + if (deleted) { + sendResponse(exchange, 200, "{\"ok\":true}"); + } else { + sendResponse(exchange, 404, "{\"error\":\"Map not found\"}"); + } + } + + private void sendJson(HttpExchange exchange, int statusCode, String json) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type"); + byte[] bytes = json.getBytes("UTF-8"); + exchange.sendResponseHeaders(statusCode, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException { + byte[] bytes = response.getBytes("UTF-8"); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + exchange.sendResponseHeaders(statusCode, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } +} diff --git a/backend/src/main/java/com/zombie/game/server/MapStorage.java b/backend/src/main/java/com/zombie/game/server/MapStorage.java new file mode 100644 index 0000000..2f45777 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/server/MapStorage.java @@ -0,0 +1,125 @@ +package com.zombie.game.server; + +import com.google.gson.Gson; +import com.zombie.game.model.MapData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.file.*; +import java.util.*; + +/** + * 地图存储服务 + * + * 负责将地图数据保存到JSON文件 + */ +public class MapStorage { + private static final Logger logger = LoggerFactory.getLogger(MapStorage.class); + private static final String MAPS_DIR = "maps"; + private final Gson gson; + private final Map cache; + + public MapStorage() { + this.gson = new Gson(); + this.cache = new HashMap<>(); + ensureMapsDir(); + } + + private void ensureMapsDir() { + Path mapsPath = Paths.get(MAPS_DIR); + if (!Files.exists(mapsPath)) { + try { + Files.createDirectories(mapsPath); + } catch (IOException e) { + logger.error("Failed to create maps directory", e); + } + } + } + + public MapData save(MapData mapData) { + if (mapData.getId() == null || mapData.getId().isEmpty()) { + mapData.setId(UUID.randomUUID().toString().substring(0, 8)); + } + if (mapData.getName() == null || mapData.getName().isEmpty()) { + mapData.setName("untitled"); + } + + String filename = mapData.getId() + ".json"; + Path filePath = Paths.get(MAPS_DIR, filename); + + try (Writer writer = Files.newBufferedWriter(filePath)) { + gson.toJson(mapData, writer); + cache.put(mapData.getId(), mapData); + logger.info("Map saved: {} ({})", mapData.getName(), mapData.getId()); + return mapData; + } catch (IOException e) { + logger.error("Failed to save map: {}", mapData.getId(), e); + throw new RuntimeException("Failed to save map", e); + } + } + + public MapData load(String id) { + MapData cached = cache.get(id); + if (cached != null) { + return cached; + } + + String filename = id + ".json"; + Path filePath = Paths.get(MAPS_DIR, filename); + + if (!Files.exists(filePath)) { + return null; + } + + try (Reader reader = Files.newBufferedReader(filePath)) { + MapData mapData = gson.fromJson(reader, MapData.class); + cache.put(id, mapData); + return mapData; + } catch (IOException e) { + logger.error("Failed to load map: {}", id, e); + return null; + } + } + + public List listMaps() { + List maps = new ArrayList<>(); + Path mapsPath = Paths.get(MAPS_DIR); + + if (!Files.exists(mapsPath)) { + return maps; + } + + try (DirectoryStream stream = Files.newDirectoryStream(mapsPath, "*.json")) { + for (Path file : stream) { + try (Reader reader = Files.newBufferedReader(file)) { + MapData mapData = gson.fromJson(reader, MapData.class); + maps.add(mapData); + } catch (IOException e) { + logger.warn("Failed to read map file: {}", file); + } + } + } catch (IOException e) { + logger.error("Failed to list maps", e); + } + + return maps; + } + + public boolean delete(String id) { + String filename = id + ".json"; + Path filePath = Paths.get(MAPS_DIR, filename); + + try { + boolean deleted = Files.deleteIfExists(filePath); + if (deleted) { + cache.remove(id); + logger.info("Map deleted: {}", id); + } + return deleted; + } catch (IOException e) { + logger.error("Failed to delete map: {}", id, e); + return false; + } + } +} diff --git a/frontend/map-designer/index.html b/frontend/map-designer/index.html new file mode 100644 index 0000000..e080b8a --- /dev/null +++ b/frontend/map-designer/index.html @@ -0,0 +1,205 @@ + + + + + 地图设计器 + + + +
+

地图设计器

+
+ + + +
+
+ +
+
+ +
+
+
+

统计

+
静态墙0
+
坚果墙0
+
玩家点0/4
+
出怪点0/7
+
+
+

放置

+
空地
+
静态墙
+
坚果墙
+
玩家点
+
出怪点
+
+
+ + +
+
放置至少1个出怪点和1个玩家点
+
+
+ + + + diff --git a/frontend/src/game/engine.js b/frontend/src/game/engine.js index 7449d0e..d9dfa3d 100644 --- a/frontend/src/game/engine.js +++ b/frontend/src/game/engine.js @@ -524,6 +524,11 @@ export class GameEngine { } } + // 更新坚果墙体 + if (state.nutWalls !== undefined) { + this.scene.updateNutWalls(state.nutWalls) + } + // 更新游戏状态 if (state.gameTime !== undefined) this.gameTime = state.gameTime if (state.waveNumber !== undefined) this.waveNumber = state.waveNumber diff --git a/frontend/src/game/scene.js b/frontend/src/game/scene.js index 68b6fcd..c97eb16 100644 --- a/frontend/src/game/scene.js +++ b/frontend/src/game/scene.js @@ -33,6 +33,7 @@ export class GameScene { this.loots = new Map() // Map this.effects = [] // 特效数组 this.wallMeshes = [] // 墙壁网格 + this.nutWalls = new Map() // Map // 摄像机辅助 this.gridHelper = null @@ -228,6 +229,10 @@ export class GameScene { const zombieSpawnGeo = new THREE.BoxGeometry(1, 0.15, 1) const zombieSpawnMat = new THREE.MeshLambertMaterial({ color: 0xff4400, transparent: true, opacity: 0.7 }) + // 坚果墙体 + const nutWallGeo = new THREE.BoxGeometry(1, 1.2, 1) + const nutWallMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 }) + // 遍历地图数据 for (let y = 0; y < GRID_SIZE; y++) { for (let x = 0; x < GRID_SIZE; x++) { @@ -256,6 +261,82 @@ export class GameScene { } } + /** + * 更新坚果墙体状态 + * @param {Array} nutWalls 坚果墙体状态数组 [{x, y, health, maxHealth}] + */ + updateNutWalls(nutWalls) { + const currentKeys = new Set() + + for (const wall of nutWalls) { + const key = `${wall.x},${wall.y}` + currentKeys.add(key) + + if (this.nutWalls.has(key)) { + // 更新已有墙体的血量表现 + const data = this.nutWalls.get(key) + const healthPercent = wall.health / wall.maxHealth + // 根据血量改变颜色:满血棕色 -> 低血红棕色 + const r = 0.545 + (1 - healthPercent) * 0.4 + const g = 0.271 - (1 - healthPercent) * 0.1 + const b = 0.075 - (1 - healthPercent) * 0.05 + data.mesh.material.color.setRGB(r, g, b) + // 更新血条 + if (data.healthBar) { + data.healthBar.scale.x = healthPercent + data.healthBar.material.color.setHex(healthPercent > 0.5 ? 0x00ff00 : healthPercent > 0.25 ? 0xffff00 : 0xff0000) + } + } else { + // 创建新的坚果墙体 + const nutWallGeo = new THREE.BoxGeometry(1, 1.2, 1) + const nutWallMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 }) + const mesh = new THREE.Mesh(nutWallGeo, nutWallMat) + mesh.position.set(wall.x + 0.5, 0.6, wall.y + 0.5) + mesh.castShadow = true + mesh.receiveShadow = true + this.scene.add(mesh) + + // 创建血条背景 + const barBgGeo = new THREE.PlaneGeometry(0.8, 0.1) + const barBgMat = new THREE.MeshBasicMaterial({ color: 0x333333 }) + const barBg = new THREE.Mesh(barBgGeo, barBgMat) + barBg.position.set(wall.x + 0.5, 1.3, wall.y + 0.5) + barBg.rotation.x = -Math.PI / 2 + this.scene.add(barBg) + + // 创建血条前景 + const barFgGeo = new THREE.PlaneGeometry(0.8, 0.1) + const barFgMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) + const barFg = new THREE.Mesh(barFgGeo, barFgMat) + barFg.position.set(wall.x + 0.5, 1.31, wall.y + 0.5) + barFg.rotation.x = -Math.PI / 2 + this.scene.add(barFg) + + this.nutWalls.set(key, { mesh, healthBar: barFg, barBg }) + } + } + + // 移除已经不存在的墙体 + for (const [key, data] of this.nutWalls) { + if (!currentKeys.has(key)) { + this.scene.remove(data.mesh) + data.mesh.geometry.dispose() + data.mesh.material.dispose() + if (data.healthBar) { + this.scene.remove(data.healthBar) + data.healthBar.geometry.dispose() + data.healthBar.material.dispose() + } + if (data.barBg) { + this.scene.remove(data.barBg) + data.barBg.geometry.dispose() + data.barBg.material.dispose() + } + this.nutWalls.delete(key) + } + } + } + /** * 创建玩家3D模型 * @param {number} color 玩家颜色 diff --git a/frontend/vite.config.js b/frontend/vite.config.js index dbb7301..a968ae8 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,6 +8,10 @@ export default defineConfig({ '/ws': { target: 'ws://localhost:8080', ws: true + }, + '/api': { + target: 'http://localhost:8081', + changeOrigin: true } } } diff --git a/maps/d540209a.json b/maps/d540209a.json new file mode 100644 index 0000000..7f7d7e8 --- /dev/null +++ b/maps/d540209a.json @@ -0,0 +1 @@ + {"id":"d540209a","name":"my-map","width":32,"height":32,"walls":[{"x":0.0,"y":0.0,"type":"static"},{"x":0.0,"y":1.0,"type":"static"},{"x":0.0,"y":2.0,"type":"static"},{"x":0.0,"y":3.0,"type":"static"},{"x":0.0,"y":4.0,"type":"static"},{"x":0.0,"y":5.0,"type":"static"},{"x":0.0,"y":6.0,"type":"static"},{"x":0.0,"y":7.0,"type":"static"},{"x":23.0,"y":7.0,"type":"static"},{"x":0.0,"y":8.0,"type":"static"},{"x":8.0,"y":8.0,"type":"nut"},{"x":9.0,"y":8.0,"type":"nut"},{"x":10.0,"y":8.0,"type":"nut"},{"x":11.0,"y":8.0,"type":"nut"},{"x":12.0,"y":8.0,"type":"nut"},{"x":13.0,"y":8.0,"type":"nut"},{"x":14.0,"y":8.0,"type":"nut"},{"x":15.0,"y":8.0,"type":"nut"},{"x":16.0,"y":8.0,"type":"nut"},{"x":17.0,"y":8.0,"type":"nut"},{"x":18.0,"y":8.0,"type":"nut"},{"x":19.0,"y":8.0,"type":"nut"},{"x":20.0,"y":8.0,"type":"nut"},{"x":21.0,"y":8.0,"type":"nut"},{"x":22.0,"y":8.0,"type":"nut"},{"x":23.0,"y":8.0,"type":"static"},{"x":24.0,"y":8.0,"type":"static"},{"x":0.0,"y":9.0,"type":"static"},{"x":24.0,"y":9.0,"type":"static"},{"x":0.0,"y":10.0,"type":"static"},{"x":24.0,"y":10.0,"type":"static"},{"x":29.0,"y":10.0,"type":"static"},{"x":0.0,"y":11.0,"type":"static"},{"x":1.0,"y":11.0,"type":"nut"},{"x":2.0,"y":11.0,"type":"nut"},{"x":3.0,"y":11.0,"type":"nut"},{"x":4.0,"y":11.0,"type":"nut"},{"x":5.0,"y":11.0,"type":"nut"},{"x":6.0,"y":11.0,"type":"nut"},{"x":7.0,"y":11.0,"type":"nut"},{"x":8.0,"y":11.0,"type":"nut"},{"x":9.0,"y":11.0,"type":"nut"},{"x":10.0,"y":11.0,"type":"nut"},{"x":11.0,"y":11.0,"type":"nut"},{"x":12.0,"y":11.0,"type":"nut"},{"x":13.0,"y":11.0,"type":"nut"},{"x":14.0,"y":11.0,"type":"nut"},{"x":15.0,"y":11.0,"type":"nut"},{"x":16.0,"y":11.0,"type":"nut"},{"x":24.0,"y":11.0,"type":"static"},{"x":29.0,"y":11.0,"type":"static"},{"x":0.0,"y":12.0,"type":"static"},{"x":24.0,"y":12.0,"type":"static"},{"x":0.0,"y":13.0,"type":"static"},{"x":7.0,"y":13.0,"type":"nut"},{"x":8.0,"y":13.0,"type":"nut"},{"x":9.0,"y":13.0,"type":"nut"},{"x":10.0,"y":13.0,"type":"nut"},{"x":11.0,"y":13.0,"type":"nut"},{"x":12.0,"y":13.0,"type":"nut"},{"x":13.0,"y":13.0,"type":"nut"},{"x":14.0,"y":13.0,"type":"nut"},{"x":15.0,"y":13.0,"type":"nut"},{"x":16.0,"y":13.0,"type":"nut"},{"x":17.0,"y":13.0,"type":"nut"},{"x":18.0,"y":13.0,"type":"nut"},{"x":19.0,"y":13.0,"type":"nut"},{"x":20.0,"y":13.0,"type":"nut"},{"x":21.0,"y":13.0,"type":"nut"},{"x":22.0,"y":13.0,"type":"nut"},{"x":23.0,"y":13.0,"type":"nut"},{"x":24.0,"y":13.0,"type":"static"},{"x":29.0,"y":13.0,"type":"static"},{"x":0.0,"y":14.0,"type":"static"},{"x":7.0,"y":14.0,"type":"nut"},{"x":24.0,"y":14.0,"type":"static"},{"x":29.0,"y":14.0,"type":"static"},{"x":24.0,"y":15.0,"type":"static"},{"x":29.0,"y":15.0,"type":"static"},{"x":24.0,"y":16.0,"type":"static"},{"x":0.0,"y":17.0,"type":"static"},{"x":1.0,"y":17.0,"type":"static"},{"x":2.0,"y":17.0,"type":"static"},{"x":3.0,"y":17.0,"type":"static"},{"x":6.0,"y":17.0,"type":"static"},{"x":7.0,"y":17.0,"type":"static"},{"x":10.0,"y":17.0,"type":"static"},{"x":11.0,"y":17.0,"type":"static"},{"x":12.0,"y":17.0,"type":"static"},{"x":13.0,"y":17.0,"type":"static"},{"x":14.0,"y":17.0,"type":"static"},{"x":15.0,"y":17.0,"type":"static"},{"x":16.0,"y":17.0,"type":"static"},{"x":17.0,"y":17.0,"type":"static"},{"x":18.0,"y":17.0,"type":"static"},{"x":19.0,"y":17.0,"type":"static"},{"x":20.0,"y":17.0,"type":"static"},{"x":21.0,"y":17.0,"type":"static"},{"x":22.0,"y":17.0,"type":"static"},{"x":23.0,"y":17.0,"type":"static"},{"x":24.0,"y":17.0,"type":"static"},{"x":29.0,"y":17.0,"type":"static"},{"x":29.0,"y":19.0,"type":"static"},{"x":29.0,"y":20.0,"type":"static"},{"x":29.0,"y":21.0,"type":"static"},{"x":29.0,"y":22.0,"type":"static"},{"x":28.0,"y":23.0,"type":"static"},{"x":29.0,"y":23.0,"type":"static"},{"x":28.0,"y":24.0,"type":"static"},{"x":9.0,"y":25.0,"type":"static"},{"x":10.0,"y":25.0,"type":"static"},{"x":11.0,"y":25.0,"type":"static"},{"x":12.0,"y":25.0,"type":"static"},{"x":13.0,"y":25.0,"type":"static"},{"x":14.0,"y":25.0,"type":"static"},{"x":15.0,"y":25.0,"type":"static"},{"x":28.0,"y":25.0,"type":"static"},{"x":15.0,"y":26.0,"type":"static"},{"x":17.0,"y":26.0,"type":"static"},{"x":18.0,"y":26.0,"type":"static"},{"x":20.0,"y":26.0,"type":"static"},{"x":22.0,"y":26.0,"type":"static"},{"x":24.0,"y":26.0,"type":"static"},{"x":25.0,"y":26.0,"type":"static"},{"x":26.0,"y":26.0,"type":"static"},{"x":27.0,"y":26.0,"type":"static"},{"x":28.0,"y":26.0,"type":"static"}],"playerSpawns":[{"x":4,"y":1},{"x":9,"y":2},{"x":14,"y":2},{"x":24,"y":2}],"zombieSpawns":[{"x":23,"y":15},{"x":6,"y":21}]} \ No newline at end of file