1
This commit is contained in:
@@ -32,6 +32,11 @@
|
|||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>2.10.1</version>
|
<version>2.10.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.15.2</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-simple</artifactId>
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
package com.zombie.game;
|
package com.zombie.game;
|
||||||
|
|
||||||
import com.zombie.game.server.GameWebSocketServer;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏服务器主入口类
|
* 游戏服务器主入口类
|
||||||
*
|
*
|
||||||
* 职责:
|
* 职责:
|
||||||
* 1. 解析命令行参数(端口号)
|
* 1. 解析命令行参数(端口号)
|
||||||
* 2. 启动 WebSocket 游戏服务器
|
* 2. 启动 WebSocket 游戏服务器
|
||||||
* 3. 注册 JVM 关闭钩子,确保服务器优雅退出
|
* 3. 启动地图设计器 API 服务器
|
||||||
|
* 4. 注册 JVM 关闭钩子,确保服务器优雅退出
|
||||||
*/
|
*/
|
||||||
public class GameServerMain {
|
public class GameServerMain {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GameServerMain.class);
|
private static final Logger logger = LoggerFactory.getLogger(GameServerMain.class);
|
||||||
@@ -20,7 +25,7 @@ public class GameServerMain {
|
|||||||
*
|
*
|
||||||
* @param args 命令行参数,第一个参数可选为端口号(默认 8080)
|
* @param args 命令行参数,第一个参数可选为端口号(默认 8080)
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) throws IOException {
|
||||||
// 默认监听端口
|
// 默认监听端口
|
||||||
int port = 8080;
|
int port = 8080;
|
||||||
|
|
||||||
@@ -37,13 +42,20 @@ public class GameServerMain {
|
|||||||
GameWebSocketServer server = new GameWebSocketServer(port);
|
GameWebSocketServer server = new GameWebSocketServer(port);
|
||||||
server.start();
|
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("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");
|
logger.info("Press Ctrl+C to stop the server");
|
||||||
|
|
||||||
// 注册关闭钩子:当 JVM 退出时(Ctrl+C 或系统信号),优雅地停止服务器
|
// 注册关闭钩子:当 JVM 退出时(Ctrl+C 或系统信号),优雅地停止服务器
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
logger.info("Shutting down server...");
|
logger.info("Shutting down server...");
|
||||||
try {
|
try {
|
||||||
|
apiServer.stop();
|
||||||
server.stop();
|
server.stop();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error during server shutdown", e);
|
logger.error("Error during server shutdown", e);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zombie.game.model;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.zombie.game.model.Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 子弹/投掷物类
|
* 子弹/投掷物类
|
||||||
*
|
*
|
||||||
@@ -123,7 +125,9 @@ public class Bullet {
|
|||||||
|
|
||||||
int gx = (int) Math.floor(x);
|
int gx = (int) Math.floor(x);
|
||||||
int gy = (int) Math.floor(y);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class Constants {
|
|||||||
/** 僵尸生成间隔基础值(秒),难度提升后会逐渐缩短 */
|
/** 僵尸生成间隔基础值(秒),难度提升后会逐渐缩短 */
|
||||||
public static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
|
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;
|
public static final float ZOMBIE_HEALTH_INCREASE = 20;
|
||||||
/** 每次难度提升增加的僵尸速度 */
|
/** 每次难度提升增加的僵尸速度 */
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.zombie.game.model;
|
package com.zombie.game.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,98 +26,96 @@ public class GameMap {
|
|||||||
private final Map<String, Wall> walls;
|
private final Map<String, Wall> walls;
|
||||||
/** 流场导航 */
|
/** 流场导航 */
|
||||||
private final FlowField flowField;
|
private final FlowField flowField;
|
||||||
|
/** 玩家出生点 */
|
||||||
|
private final List<int[]> spawnPoints;
|
||||||
|
/** 僵尸出生点 */
|
||||||
|
private final List<int[]> zombieSpawnPoints;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造函数 - 初始化地图并生成默认布局
|
* 构造函数 - 从 JSON 文件加载地图
|
||||||
|
*
|
||||||
|
* @param mapFilePath 地图 JSON 文件路径
|
||||||
*/
|
*/
|
||||||
public GameMap() {
|
public GameMap(String mapFilePath) {
|
||||||
this.width = Constants.GRID_SIZE;
|
JsonNode root = loadMapFile(mapFilePath);
|
||||||
this.height = Constants.GRID_SIZE;
|
this.width = root.get("width").asInt();
|
||||||
|
this.height = root.get("height").asInt();
|
||||||
this.cells = new int[height][width];
|
this.cells = new int[height][width];
|
||||||
this.walls = new HashMap<>();
|
this.walls = new HashMap<>();
|
||||||
this.flowField = new FlowField(width, height);
|
this.flowField = new FlowField(width, height);
|
||||||
generateDefaultMap();
|
this.spawnPoints = new ArrayList<>();
|
||||||
|
this.zombieSpawnPoints = new ArrayList<>();
|
||||||
|
loadFromJson(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成默认地图布局
|
* 加载地图 JSON 文件
|
||||||
*
|
|
||||||
* 创建边界墙壁、内部障碍物、玩家出生点和僵尸出生点
|
|
||||||
*/
|
*/
|
||||||
private void generateDefaultMap() {
|
private JsonNode loadMapFile(String mapFilePath) {
|
||||||
for (int y = 0; y < height; y++) {
|
try {
|
||||||
for (int x = 0; x < width; x++) {
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) {
|
return mapper.readTree(new File(mapFilePath));
|
||||||
cells[y][x] = 0;
|
} 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));
|
walls.put(key(x, y), new StaticWall(x, y));
|
||||||
} else {
|
} else if ("nut".equals(type)) {
|
||||||
cells[y][x] = 0;
|
walls.put(key(x, y), new NutWall(x, y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int[][] wallSegments = {
|
// 加载玩家出生点
|
||||||
{5, 5, 5, 10}, {10, 3, 15, 3}, {20, 5, 20, 12},
|
JsonNode playerSpawns = root.get("playerSpawns");
|
||||||
{8, 15, 14, 15}, {25, 10, 25, 18}, {3, 20, 8, 20},
|
if (playerSpawns != null && playerSpawns.isArray()) {
|
||||||
{15, 20, 15, 26}, {22, 22, 28, 22}, {5, 25, 10, 25},
|
for (JsonNode spawn : playerSpawns) {
|
||||||
{12, 8, 12, 12}, {18, 16, 22, 16}, {27, 5, 27, 9},
|
int x = spawn.get("x").asInt();
|
||||||
{8, 27, 13, 27}, {18, 26, 18, 30}
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int[] seg : wallSegments) {
|
// 加载僵尸出生点
|
||||||
if (seg[0] == seg[2]) {
|
JsonNode zombieSpawns = root.get("zombieSpawns");
|
||||||
for (int y = seg[1]; y <= seg[3]; y++) {
|
if (zombieSpawns != null && zombieSpawns.isArray()) {
|
||||||
if (seg[0] > 0 && seg[0] < width - 1 && y > 0 && y < height - 1) {
|
for (JsonNode spawn : zombieSpawns) {
|
||||||
walls.put(key(seg[0], y), new StaticWall(seg[0], y));
|
int x = spawn.get("x").asInt();
|
||||||
}
|
int y = spawn.get("y").asInt();
|
||||||
}
|
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||||
} else {
|
cells[y][x] = 3;
|
||||||
for (int x = seg[0]; x <= seg[2]; x++) {
|
zombieSpawnPoints.add(new int[]{x, y});
|
||||||
if (x > 0 && x < width - 1 && seg[1] > 0 && seg[1] < height - 1) {
|
|
||||||
walls.put(key(x, seg[1]), new StaticWall(x, seg[1]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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++) {
|
* @param gx 格子X坐标
|
||||||
for (int dx = -1; dx <= 1; dx++) {
|
* @param gy 格子Y坐标
|
||||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
* @return true 表示添加成功
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加坚果墙体
|
* 添加坚果墙体
|
||||||
@@ -201,8 +202,53 @@ public class GameMap {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地图数据(包含墙体信息)
|
||||||
|
*
|
||||||
|
* 返回的二维数组格式与前端兼容:
|
||||||
|
* - 0 = 空地
|
||||||
|
* - 1 = 静态墙壁
|
||||||
|
* - 2 = 玩家出生点
|
||||||
|
* - 3 = 僵尸出生点
|
||||||
|
* - 4 = 坚果墙体
|
||||||
|
*
|
||||||
|
* @return 地图格子数据
|
||||||
|
*/
|
||||||
public int[][] getCells() {
|
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 僵尸出生点坐标列表
|
* @return 僵尸出生点坐标列表
|
||||||
*/
|
*/
|
||||||
public List<int[]> getZombieSpawnPoints() {
|
public List<int[]> getZombieSpawnPoints() {
|
||||||
List<int[]> points = new ArrayList<>();
|
return zombieSpawnPoints;
|
||||||
int[][] zombieSpawns = {{8, 8}, {24, 24}};
|
|
||||||
for (int[] sp : zombieSpawns) {
|
|
||||||
points.add(sp);
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zombie.game.model;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.zombie.game.model.Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏世界类
|
* 游戏世界类
|
||||||
*
|
*
|
||||||
@@ -39,7 +41,11 @@ public class GameWorld {
|
|||||||
private List<Integer> removedZombieBullets;
|
private List<Integer> removedZombieBullets;
|
||||||
|
|
||||||
public GameWorld() {
|
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.players = new LinkedHashMap<>();
|
||||||
this.zombies = new LinkedHashMap<>();
|
this.zombies = new LinkedHashMap<>();
|
||||||
this.bullets = new LinkedHashMap<>();
|
this.bullets = new LinkedHashMap<>();
|
||||||
@@ -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() {
|
private void checkZombieBulletCollisions() {
|
||||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||||
for (Bullet b : new ArrayList<>(zombieBullets.values())) {
|
for (Bullet b : new ArrayList<>(zombieBullets.values())) {
|
||||||
|
boolean hit = false;
|
||||||
|
|
||||||
|
// 检测是否命中玩家
|
||||||
for (Player p : new ArrayList<>(players.values())) {
|
for (Player p : new ArrayList<>(players.values())) {
|
||||||
if (!p.isAlive()) continue;
|
if (!p.isAlive()) continue;
|
||||||
if (b.hitsEntity(p.getX(), p.getY(), Constants.PLAYER_SIZE)) {
|
if (b.hitsEntity(p.getX(), p.getY(), Constants.PLAYER_SIZE)) {
|
||||||
p.takeDamage(b.getDamage());
|
p.takeDamage(b.getDamage());
|
||||||
bulletsToRemove.add(b.getId());
|
hit = true;
|
||||||
break;
|
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) {
|
for (int id : bulletsToRemove) {
|
||||||
zombieBullets.remove(id);
|
zombieBullets.remove(id);
|
||||||
@@ -352,20 +386,41 @@ public class GameWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测玩家子弹与僵尸的碰撞
|
* 检测玩家子弹与僵尸/墙体的碰撞
|
||||||
*/
|
*/
|
||||||
private void checkBulletCollisions() {
|
private void checkBulletCollisions() {
|
||||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||||
for (Bullet b : new ArrayList<>(bullets.values())) {
|
for (Bullet b : new ArrayList<>(bullets.values())) {
|
||||||
if (b.isGrenade()) continue;
|
if (b.isGrenade()) continue;
|
||||||
|
|
||||||
|
// 检测是否命中僵尸
|
||||||
|
boolean hit = false;
|
||||||
for (Zombie z : new ArrayList<>(zombies.values())) {
|
for (Zombie z : new ArrayList<>(zombies.values())) {
|
||||||
if (!z.isAlive()) continue;
|
if (!z.isAlive()) continue;
|
||||||
if (b.hitsEntity(z.getX(), z.getY(), Constants.ZOMBIE_SIZE)) {
|
if (b.hitsEntity(z.getX(), z.getY(), Constants.ZOMBIE_SIZE)) {
|
||||||
z.takeDamage(b.getDamage());
|
z.takeDamage(b.getDamage());
|
||||||
bulletsToRemove.add(b.getId());
|
hit = true;
|
||||||
break;
|
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) {
|
for (int id : bulletsToRemove) {
|
||||||
bullets.remove(id);
|
bullets.remove(id);
|
||||||
@@ -615,6 +670,7 @@ public class GameWorld {
|
|||||||
|
|
||||||
state.put("explosions", new ArrayList<>(explosions));
|
state.put("explosions", new ArrayList<>(explosions));
|
||||||
state.put("removedBullets", new ArrayList<>(removedBullets));
|
state.put("removedBullets", new ArrayList<>(removedBullets));
|
||||||
|
state.put("nutWalls", map.getNutWallStates());
|
||||||
state.put("gameTime", gameTime);
|
state.put("gameTime", gameTime);
|
||||||
state.put("waveNumber", waveNumber);
|
state.put("waveNumber", waveNumber);
|
||||||
state.put("score", score);
|
state.put("score", score);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zombie.game.model;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.zombie.game.model.Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 掉落物类
|
* 掉落物类
|
||||||
*
|
*
|
||||||
|
|||||||
120
backend/src/main/java/com/zombie/game/model/MapData.java
Normal file
120
backend/src/main/java/com/zombie/game/model/MapData.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package com.zombie.game.model;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.zombie.game.model.Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家类
|
* 玩家类
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zombie.game.model;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.zombie.game.model.Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏房间类
|
* 游戏房间类
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zombie.game.model;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.zombie.game.model.Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 僵尸类
|
* 僵尸类
|
||||||
*
|
*
|
||||||
@@ -82,20 +84,42 @@ public class Zombie {
|
|||||||
* 移动僵尸
|
* 移动僵尸
|
||||||
*
|
*
|
||||||
* 基于流场导航移动,包含:
|
* 基于流场导航移动,包含:
|
||||||
* - 路径规划
|
* - 路径规划(支持加权障碍物,自动权衡绕道 vs 摧毁)
|
||||||
* - 避免与其他僵尸重叠
|
* - 避免与其他僵尸重叠
|
||||||
* - 墙壁碰撞检测
|
* - 墙壁碰撞检测
|
||||||
|
* - 攻击坚果墙体逻辑
|
||||||
*
|
*
|
||||||
* @param map 游戏地图
|
* @param map 游戏地图
|
||||||
* @param dt 时间增量(秒)
|
* @param dt 时间增量(秒)
|
||||||
* @param otherZombies 其他僵尸集合
|
* @param otherZombies 其他僵尸集合
|
||||||
|
* @param now 当前时间戳
|
||||||
|
* @return 如果正在攻击墙体,返回被攻击的墙体对象;否则返回 null
|
||||||
*/
|
*/
|
||||||
public void move(GameMap map, float dt, Collection<Zombie> otherZombies) {
|
public Wall move(GameMap map, float dt, Collection<Zombie> otherZombies, long now) {
|
||||||
if (!map.isFlowFieldValid()) return;
|
if (!map.isFlowFieldValid()) return null;
|
||||||
|
|
||||||
int currentGridX = (int) Math.floor(x);
|
int currentGridX = (int) Math.floor(x);
|
||||||
int currentGridY = (int) Math.floor(y);
|
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;
|
float centerDist = Float.MAX_VALUE;
|
||||||
if (hasTarget) {
|
if (hasTarget) {
|
||||||
float dx = targetX - x;
|
float dx = targetX - x;
|
||||||
@@ -108,7 +132,7 @@ public class Zombie {
|
|||||||
float dirX = flowDir[0];
|
float dirX = flowDir[0];
|
||||||
float dirY = flowDir[1];
|
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);
|
float len = (float) Math.sqrt(dirX * dirX + dirY * dirY);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
@@ -119,7 +143,20 @@ public class Zombie {
|
|||||||
int nextGridX = currentGridX + (int) Math.round(dirX);
|
int nextGridX = currentGridX + (int) Math.round(dirX);
|
||||||
int nextGridY = currentGridY + (int) Math.round(dirY);
|
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);
|
nextGridX = currentGridX + (int) Math.signum(dirX);
|
||||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||||
|
|
||||||
@@ -132,8 +169,7 @@ public class Zombie {
|
|||||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||||
} else {
|
} else {
|
||||||
reservation = false;
|
reservation = false;
|
||||||
return;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +180,26 @@ public class Zombie {
|
|||||||
nextGridY = altDirs[1];
|
nextGridY = altDirs[1];
|
||||||
} else {
|
} else {
|
||||||
reservation = false;
|
reservation = false;
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +211,7 @@ public class Zombie {
|
|||||||
targetY = nextGridY + 0.5f;
|
targetY = nextGridY + 0.5f;
|
||||||
hasTarget = true;
|
hasTarget = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float dx = targetX - x;
|
float dx = targetX - x;
|
||||||
float dy = targetY - y;
|
float dy = targetY - y;
|
||||||
@@ -163,7 +219,7 @@ public class Zombie {
|
|||||||
|
|
||||||
if (dist < 0.01f) {
|
if (dist < 0.01f) {
|
||||||
hasTarget = false;
|
hasTarget = false;
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
float dirX = dx / dist;
|
float dirX = dx / dist;
|
||||||
@@ -251,6 +307,8 @@ public class Zombie {
|
|||||||
if (dirX != 0 || dirY != 0) {
|
if (dirX != 0 || dirY != 0) {
|
||||||
angle = (float) Math.atan2(dirX, dirY);
|
angle = (float) Math.atan2(dirX, dirY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.zombie.game.server;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.zombie.game.model.*;
|
import com.zombie.game.model.*;
|
||||||
|
import com.zombie.game.model.Constants;
|
||||||
import org.java_websocket.WebSocket;
|
import org.java_websocket.WebSocket;
|
||||||
import org.java_websocket.handshake.ClientHandshake;
|
import org.java_websocket.handshake.ClientHandshake;
|
||||||
import org.java_websocket.server.WebSocketServer;
|
import org.java_websocket.server.WebSocketServer;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
backend/src/main/java/com/zombie/game/server/MapStorage.java
Normal file
125
backend/src/main/java/com/zombie/game/server/MapStorage.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
frontend/map-designer/index.html
Normal file
205
frontend/map-designer/index.html
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>地图设计器</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: Arial, sans-serif; background: #1a1a2e; color: #fff; height: 100vh; display: flex; flex-direction: column; }
|
||||||
|
.header { background: #16213e; padding: 12px 20px; display: flex; align-items: center; gap: 16px; border-bottom: 2px solid #0f3460; }
|
||||||
|
.header h1 { font-size: 18px; color: #e94560; }
|
||||||
|
.toolbar { display: flex; gap: 8px; }
|
||||||
|
.size-set { display: flex; gap: 8px; align-items: center; margin-left: 20px; }
|
||||||
|
.size-set label { font-size: 14px; }
|
||||||
|
.size-set input { width: 60px; padding: 4px 8px; background: #0f3460; border: none; color: #fff; border-radius: 4px; }
|
||||||
|
.size-set button { padding: 6px 12px; background: #e94560; border: none; color: #fff; border-radius: 4px; cursor: pointer; }
|
||||||
|
.main { flex: 1; display: flex; overflow: hidden; }
|
||||||
|
.canvas-wrap { flex: 1; display: flex; align-items: center; justify-content: center; background: #0f0f1a; overflow: auto; padding: 20px; }
|
||||||
|
#canvas { background: #2d2d44; cursor: crosshair; }
|
||||||
|
.panel { width: 200px; background: #16213e; padding: 16px; border-left: 1px solid #0f3460; display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.panel h3 { font-size: 14px; color: #e94560; margin-bottom: 8px; }
|
||||||
|
.stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #0f3460; }
|
||||||
|
.stat-label { color: #888; }
|
||||||
|
.actions { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.actions button { padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
||||||
|
.btn-save { background: #e94560; color: #fff; }
|
||||||
|
.btn-clear { background: #16213e; color: #fff; border: 1px solid #0f3460; }
|
||||||
|
.msg { font-size: 12px; padding: 8px; border-radius: 4px; background: #0f0f1a; }
|
||||||
|
.msg.error { color: #ff6b6b; }
|
||||||
|
.msg.ok { color: #4ade80; }
|
||||||
|
.legend { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; padding: 4px 6px; border-radius: 4px; }
|
||||||
|
.legend-item:hover { background: #0f3460; }
|
||||||
|
.legend-item.active { background: #e94560; }
|
||||||
|
.legend-swatch { width: 16px; height: 16px; border-radius: 3px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>地图设计器</h1>
|
||||||
|
<div class="size-set">
|
||||||
|
<label>宽:<input type="number" id="w" value="32" min="8" max="128"></label>
|
||||||
|
<label>高:<input type="number" id="h" value="32" min="8" max="128"></label>
|
||||||
|
<button id="apply">应用</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="canvas-wrap">
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div>
|
||||||
|
<h3>统计</h3>
|
||||||
|
<div class="stat"><span class="stat-label">静态墙</span><span id="s0">0</span></div>
|
||||||
|
<div class="stat"><span class="stat-label">坚果墙</span><span id="s1">0</span></div>
|
||||||
|
<div class="stat"><span class="stat-label">玩家点</span><span id="s2">0/4</span></div>
|
||||||
|
<div class="stat"><span class="stat-label">出怪点</span><span id="s3">0/7</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>放置</h3>
|
||||||
|
<div class="legend"><div class="legend-item" data-tool="0"><div class="legend-swatch" style="background:#ffffff;border:1px solid #666"></div>空地</div>
|
||||||
|
<div class="legend-item active" data-tool="1"><div class="legend-swatch" style="background:#4a4a6a"></div>静态墙</div>
|
||||||
|
<div class="legend-item" data-tool="2"><div class="legend-swatch" style="background:#8B4513"></div>坚果墙</div>
|
||||||
|
<div class="legend-item" data-tool="3"><div class="legend-swatch" style="background:#e94560"></div>玩家点</div>
|
||||||
|
<div class="legend-item" data-tool="4"><div class="legend-swatch" style="background:#4ade80"></div>出怪点</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-clear" id="clear">清空</button>
|
||||||
|
<button class="btn-save" id="save">保存JSON</button>
|
||||||
|
</div>
|
||||||
|
<div class="msg" id="msg">放置至少1个出怪点和1个玩家点</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const COLORS = ['#ffffff','#4a4a6a','#8B4513','#e94560','#4ade80'];
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
let w = 32, h = 32, cs = 16, tool = 1;
|
||||||
|
let cells = [];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
cells = [];
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
cells[y] = new Array(w).fill(0);
|
||||||
|
}
|
||||||
|
canvas.width = w * cs;
|
||||||
|
canvas.height = h * cs;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
ctx.fillStyle = COLORS[cells[y][x]];
|
||||||
|
ctx.fillRect(x * cs, y * cs, cs, cs);
|
||||||
|
ctx.strokeStyle = '#222';
|
||||||
|
ctx.strokeRect(x * cs, y * cs, cs, cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
let c = [0,0,0,0,0];
|
||||||
|
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) c[cells[y][x]]++;
|
||||||
|
document.getElementById('s0').textContent = c[1];
|
||||||
|
document.getElementById('s1').textContent = c[2];
|
||||||
|
document.getElementById('s2').textContent = c[3] + '/4';
|
||||||
|
document.getElementById('s3').textContent = c[4] + '/7';
|
||||||
|
const msg = document.getElementById('msg');
|
||||||
|
if (c[3] < 1 || c[4] < 1) {
|
||||||
|
msg.className = 'msg error';
|
||||||
|
msg.textContent = '需要至少1个出怪点和1个玩家点';
|
||||||
|
document.getElementById('save').disabled = true;
|
||||||
|
} else if (c[3] > 4 || c[4] > 7) {
|
||||||
|
msg.className = 'msg error';
|
||||||
|
msg.textContent = c[3] > 4 ? '玩家点最多4个' : '出怪点最多7个';
|
||||||
|
document.getElementById('save').disabled = true;
|
||||||
|
} else {
|
||||||
|
msg.className = 'msg ok';
|
||||||
|
msg.textContent = '可以保存';
|
||||||
|
document.getElementById('save').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCell(e) {
|
||||||
|
const r = canvas.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - r.left) / cs);
|
||||||
|
const y = Math.floor((e.clientY - r.top) / cs);
|
||||||
|
if (x >= 0 && x < w && y >= 0 && y < h) return { x, y };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', e => {
|
||||||
|
const c = getCell(e);
|
||||||
|
if (c) { cells[c.y][c.x] = tool; draw(); }
|
||||||
|
});
|
||||||
|
canvas.addEventListener('mousemove', e => {
|
||||||
|
if (e.buttons) {
|
||||||
|
const c = getCell(e);
|
||||||
|
if (c) { cells[c.y][c.x] = tool; draw(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
canvas.addEventListener('contextmenu', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const c = getCell(e);
|
||||||
|
if (c) { cells[c.y][c.x] = 0; draw(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.legend-item').forEach(b => {
|
||||||
|
b.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.legend-item').forEach(x => x.classList.remove('active'));
|
||||||
|
b.classList.add('active');
|
||||||
|
tool = parseInt(b.dataset.tool);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('apply').addEventListener('click', () => {
|
||||||
|
const nw = parseInt(document.getElementById('w').value) || 32;
|
||||||
|
const nh = parseInt(document.getElementById('h').value) || 32;
|
||||||
|
w = Math.max(8, Math.min(128, nw));
|
||||||
|
h = Math.max(8, Math.min(128, nh));
|
||||||
|
document.getElementById('w').value = w;
|
||||||
|
document.getElementById('h').value = h;
|
||||||
|
cs = Math.max(4, Math.min(20, Math.floor(600 / Math.max(w, h))));
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clear').addEventListener('click', () => {
|
||||||
|
if (confirm('清空?')) init();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save').addEventListener('click', async () => {
|
||||||
|
const name = prompt('地图名称:', 'my-map') || 'my-map';
|
||||||
|
const walls = [], ps = [], zs = [];
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
if (cells[y][x] === 1) walls.push({x, y, type: 'static'});
|
||||||
|
if (cells[y][x] === 2) walls.push({x, y, type: 'nut'});
|
||||||
|
if (cells[y][x] === 3) ps.push({x, y});
|
||||||
|
if (cells[y][x] === 4) zs.push({x, y});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = { name, width: w, height: h, walls, playerSpawns: ps, zombieSpawns: zs };
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/maps', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error('保存失败');
|
||||||
|
const result = await resp.json();
|
||||||
|
alert('保存成功!ID: ' + result.id);
|
||||||
|
} catch (e) {
|
||||||
|
alert('保存失败: ' + e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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.gameTime !== undefined) this.gameTime = state.gameTime
|
||||||
if (state.waveNumber !== undefined) this.waveNumber = state.waveNumber
|
if (state.waveNumber !== undefined) this.waveNumber = state.waveNumber
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class GameScene {
|
|||||||
this.loots = new Map() // Map<lootId, {mesh, type}>
|
this.loots = new Map() // Map<lootId, {mesh, type}>
|
||||||
this.effects = [] // 特效数组
|
this.effects = [] // 特效数组
|
||||||
this.wallMeshes = [] // 墙壁网格
|
this.wallMeshes = [] // 墙壁网格
|
||||||
|
this.nutWalls = new Map() // Map<key, {mesh, healthBar}>
|
||||||
|
|
||||||
// 摄像机辅助
|
// 摄像机辅助
|
||||||
this.gridHelper = null
|
this.gridHelper = null
|
||||||
@@ -228,6 +229,10 @@ export class GameScene {
|
|||||||
const zombieSpawnGeo = new THREE.BoxGeometry(1, 0.15, 1)
|
const zombieSpawnGeo = new THREE.BoxGeometry(1, 0.15, 1)
|
||||||
const zombieSpawnMat = new THREE.MeshLambertMaterial({ color: 0xff4400, transparent: true, opacity: 0.7 })
|
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 y = 0; y < GRID_SIZE; y++) {
|
||||||
for (let x = 0; x < GRID_SIZE; x++) {
|
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模型
|
* 创建玩家3D模型
|
||||||
* @param {number} color 玩家颜色
|
* @param {number} color 玩家颜色
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export default defineConfig({
|
|||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://localhost:8080',
|
target: 'ws://localhost:8080',
|
||||||
ws: true
|
ws: true
|
||||||
|
},
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8081',
|
||||||
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
maps/d540209a.json
Normal file
1
maps/d540209a.json
Normal file
@@ -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}]}
|
||||||
Reference in New Issue
Block a user