This commit is contained in:
wfz
2026-04-26 11:00:59 +08:00
parent 7bffe41d41
commit f1a6f0fd75
19 changed files with 1026 additions and 142 deletions

View File

@@ -32,6 +32,11 @@
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
/** 每次难度提升增加的僵尸速度 */

View File

@@ -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<String, Wall> walls;
/** 流场导航 */
private final FlowField flowField;
/** 玩家出生点 */
private final List<int[]> spawnPoints;
/** 僵尸出生点 */
private final List<int[]> 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<Map<String, Object>> getNutWallStates() {
List<Map<String, Object>> states = new ArrayList<>();
for (Wall wall : walls.values()) {
if (wall instanceof NutWall && !wall.isDestroyed()) {
Map<String, Object> 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<int[]> getZombieSpawnPoints() {
List<int[]> points = new ArrayList<>();
int[][] zombieSpawns = {{8, 8}, {24, 24}};
for (int[] sp : zombieSpawns) {
points.add(sp);
}
return points;
return zombieSpawnPoints;
}
/**

View File

@@ -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<Integer> 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<Zombie> 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<Integer> 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<Integer> 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);

View File

@@ -3,6 +3,8 @@ package com.zombie.game.model;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 掉落物类
*

View File

@@ -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<Map<String, Object>> walls;
private List<Map<String, Integer>> playerSpawns;
private List<Map<String, Integer>> 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<Map<String, Object>> walls,
List<Map<String, Integer>> playerSpawns,
List<Map<String, Integer>> 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<Map<String, Object>> getWalls() { return walls; }
public void setWalls(List<Map<String, Object>> walls) { this.walls = walls; }
public List<Map<String, Integer>> getPlayerSpawns() { return playerSpawns; }
public void setPlayerSpawns(List<Map<String, Integer>> playerSpawns) { this.playerSpawns = playerSpawns; }
public List<Map<String, Integer>> getZombieSpawns() { return zombieSpawns; }
public void setZombieSpawns(List<Map<String, Integer>> 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<String, Object> 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<String, Integer> 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<String, Integer> 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<Map<String, Object>> walls = new ArrayList<>();
List<Map<String, Integer>> playerSpawns = new ArrayList<>();
List<Map<String, Integer>> zombieSpawns = new ArrayList<>();
for (Map.Entry<String, Wall> 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<String, Object> 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<String, Integer> sp = new HashMap<>();
sp.put("x", spawn[0]);
sp.put("y", spawn[1]);
playerSpawns.add(sp);
}
for (int[] spawn : map.getZombieSpawnPoints()) {
Map<String, Integer> 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);
}
}

View File

@@ -3,6 +3,8 @@ package com.zombie.game.model;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 玩家类
*

View File

@@ -3,6 +3,8 @@ package com.zombie.game.model;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 游戏房间类
*

View File

@@ -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<Zombie> otherZombies) {
if (!map.isFlowFieldValid()) return;
public Wall move(GameMap map, float dt, Collection<Zombie> 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;
}
/**

View File

@@ -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;

View File

@@ -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<MapData> 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);
}
}
}

View File

@@ -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<String, MapData> 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<MapData> listMaps() {
List<MapData> maps = new ArrayList<>();
Path mapsPath = Paths.get(MAPS_DIR);
if (!Files.exists(mapsPath)) {
return maps;
}
try (DirectoryStream<Path> 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;
}
}
}