迁移证esc

This commit is contained in:
wfz
2026-05-02 18:07:11 +08:00
parent 990b31a12a
commit 9fd572c8c4
51 changed files with 3659 additions and 1820 deletions

View File

@@ -0,0 +1,429 @@
package com.zombie.game.ecs;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.GameMap;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* ECS 游戏世界
*
* 中央实体管理器,持有所有实体的组件存储和系统列表。
* 每帧按顺序调度所有系统更新。
*/
@Getter
public class ECSWorld {
private final Object lock = new Object();
/** 游戏地图 */
private final GameMap map;
// ==================== 实体管理 ====================
/** 下一个可用的实体ID */
private int nextEntityId;
/** 所有实体ID集合 */
private final Set<Integer> entities = new LinkedHashSet<>();
/** 玩家实体集合 */
private final Set<Integer> players = new LinkedHashSet<>();
/** 僵尸实体集合 */
private final Set<Integer> zombies = new LinkedHashSet<>();
/** 玩家子弹实体集合 */
private final Set<Integer> playerBullets = new LinkedHashSet<>();
/** 僵尸子弹实体集合 */
private final Set<Integer> zombieBullets = new LinkedHashSet<>();
/** 掉落物实体集合 */
private final Set<Integer> loots = new LinkedHashSet<>();
/** 燃烧区域实体集合 */
private final Set<Integer> fireZones = new LinkedHashSet<>();
/** 机枪塔实体集合 */
private final Set<Integer> turrets = new LinkedHashSet<>();
/** 墙体实体集合(坚果墙、机枪塔) */
private final Set<Integer> wallEntities = new LinkedHashSet<>();
// ==================== 组件存储 ====================
/** 实体名称 */
private final Map<Integer, String> entityNames = new HashMap<>();
/** 位置组件 */
private final Map<Integer, Position> positions = new HashMap<>();
/** 生命值组件 */
private final Map<Integer, Health> healths = new HashMap<>();
/** 碰撞组件 */
private final Map<Integer, Collision> collisions = new HashMap<>();
/** 渲染信息组件 */
private final Map<Integer, RenderInfo> renderInfos = new HashMap<>();
/** 玩家输入组件 */
private final Map<Integer, PlayerInput> playerInputs = new HashMap<>();
/** 武器状态组件 */
private final Map<Integer, WeaponState> weaponStates = new HashMap<>();
/** 僵尸AI组件 */
private final Map<Integer, ZombieAI> zombieAIs = new HashMap<>();
/** 速度组件 */
private final Map<Integer, Velocity> velocities = new HashMap<>();
/** 子弹数据组件 */
private final Map<Integer, BulletData> bulletDatas = new HashMap<>();
/** 爆炸组件 */
private final Map<Integer, Explosive> explosives = new HashMap<>();
/** 掉落物数据组件 */
private final Map<Integer, LootData> lootDatas = new HashMap<>();
/** 重生状态组件 */
private final Map<Integer, RespawnState> respawnStates = new HashMap<>();
/** 燃烧区域组件 */
private final Map<Integer, FireZone> fireZonesData = new HashMap<>();
/** 机枪塔状态组件 */
private final Map<Integer, TurretState> turretStates = new HashMap<>();
/** 墙体实体组件 */
private final Map<Integer, WallEntity> wallEntityDatas = new HashMap<>();
// ==================== 系统列表 ====================
private final List<System> systems = new ArrayList<>();
// ==================== 游戏状态 ====================
@lombok.Setter private float gameTime;
@lombok.Setter private int waveNumber;
@lombok.Setter private int score;
@lombok.Setter private float spawnTimer;
@lombok.Setter private float difficultyTimer;
@lombok.Setter private float spawnInterval;
private final Random random = new Random();
// ==================== 每帧临时数据 ====================
/** 本帧爆炸效果列表 */
private final List<Map<String, Object>> explosions = new ArrayList<>();
/** 本帧移除的玩家子弹ID */
private final List<Integer> removedBullets = new ArrayList<>();
/** 本帧移除的僵尸子弹ID */
private final List<Integer> removedZombieBullets = new ArrayList<>();
/** 玩家ID到实体ID的映射 */
private final Map<String, Integer> playerIdToEntity = new HashMap<>();
/** 僵尸模板缓存 */
private final Map<String, ZombieTemplate> zombieTemplateCache = new HashMap<>();
// 难度常量
private static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
public ECSWorld(String mapFilePath) {
this.map = new GameMap(mapFilePath);
this.nextEntityId = 0;
this.gameTime = 0;
this.waveNumber = 0;
this.score = 0;
this.spawnTimer = 0;
this.difficultyTimer = 0;
this.spawnInterval = ZOMBIE_SPAWN_INTERVAL_BASE;
for (String id : Arrays.asList("normal", "elite", "splitter")) {
ZombieTemplate t = TemplateManager.getInstance().getZombieTemplate(id);
if (t != null) zombieTemplateCache.put(id, t);
}
}
public ECSWorld() {
this("/Users/wfz/workspace/zp1/maps/d540209a.json");
}
// ==================== 实体管理 ====================
/** 创建新实体返回唯一ID */
public int createEntity() {
int id = nextEntityId++;
entities.add(id);
return id;
}
/** 销毁实体,清除所有组件和集合引用 */
public void destroyEntity(int id) {
entities.remove(id);
players.remove(id);
zombies.remove(id);
playerBullets.remove(id);
zombieBullets.remove(id);
loots.remove(id);
fireZones.remove(id);
turrets.remove(id);
wallEntities.remove(id);
entityNames.remove(id);
positions.remove(id);
healths.remove(id);
collisions.remove(id);
renderInfos.remove(id);
playerInputs.remove(id);
weaponStates.remove(id);
zombieAIs.remove(id);
velocities.remove(id);
bulletDatas.remove(id);
explosives.remove(id);
lootDatas.remove(id);
respawnStates.remove(id);
fireZonesData.remove(id);
turretStates.remove(id);
wallEntityDatas.remove(id);
}
// ==================== 玩家实体创建 ====================
/**
* 创建玩家实体,附加位置、生命、碰撞、输入、武器等组件
* @param playerId 玩家唯一ID
* @param name 玩家名称
* @param x 初始X坐标
* @param y 初始Y坐标
*/
public int createPlayerEntity(String playerId, String name, float x, float y) {
int entityId = createEntity();
entityNames.put(entityId, name);
positions.put(entityId, new Position(x, y));
healths.put(entityId, new Health(
TemplateManager.getInstance().getPlayerTemplate().getMaxHealth(),
TemplateManager.getInstance().getPlayerTemplate().getInvulnerableTime()
));
collisions.put(entityId, new Collision(TemplateManager.getInstance().getPlayerTemplate().getSize()));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.PLAYER));
playerInputs.put(entityId, new PlayerInput());
weaponStates.put(entityId, new WeaponState());
respawnStates.put(entityId, new RespawnState());
players.add(entityId);
playerIdToEntity.put(playerId, entityId);
return entityId;
}
/** 根据玩家ID查找实体ID */
public Integer getPlayerEntity(String playerId) {
return playerIdToEntity.get(playerId);
}
// ==================== 僵尸实体创建 ====================
/**
* 创建僵尸实体根据模板ID初始化属性
* @param x X坐标
* @param y Y坐标
* @param templateId 模板IDnormal/elite/splitter
*/
public int createZombieEntity(float x, float y, String templateId) {
ZombieTemplate template = zombieTemplateCache.get(templateId);
if (template == null) {
template = TemplateManager.getInstance().getZombieTemplate(templateId);
zombieTemplateCache.put(templateId, template);
}
int entityId = createEntity();
positions.put(entityId, new Position(x, y));
healths.put(entityId, new Health(template.getBaseHealth()));
collisions.put(entityId, new Collision(ZOMBIE_SIZE));
String subType = template.isCanRangedAttack() ? "elite" : (template.isCanSplit() ? "splitter" : "normal");
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.ZOMBIE, subType));
zombieAIs.put(entityId, new ZombieAI(templateId));
zombies.add(entityId);
return entityId;
}
// ==================== 子弹实体创建 ====================
/**
* 创建标准子弹实体(直线飞行)
*/
public int createBulletEntity(float x, float y, float angle, float speed, int damage,
String ownerId, int weaponIndex, float range, boolean isZombieBullet) {
int entityId = createEntity();
positions.put(entityId, new Position(x, y, angle));
velocities.put(entityId, new Velocity(
(float) Math.sin(angle) * speed,
(float) Math.cos(angle) * speed
));
bulletDatas.put(entityId, new BulletData(damage, ownerId, weaponIndex, range));
collisions.put(entityId, new Collision(0.2f));
renderInfos.put(entityId, new RenderInfo(
isZombieBullet ? RenderInfo.EntityType.ZOMBIE_BULLET : RenderInfo.EntityType.BULLET
));
if (isZombieBullet) {
zombieBullets.add(entityId);
} else {
playerBullets.add(entityId);
}
return entityId;
}
/**
* 创建手榴弹实体(抛物线飞行,落地爆炸)
*/
public int createGrenadeEntity(float startX, float startY, float targetX, float targetY,
float flightDuration, int damage, String ownerId, float explosionRadius) {
int entityId = createEntity();
positions.put(entityId, new Position(startX, startY));
float vx = (targetX - startX) / flightDuration;
float vy = (targetY - startY) / flightDuration;
velocities.put(entityId, new Velocity(vx, vy, 3.0f));
BulletData data = BulletData.createGrenade(damage, ownerId, targetX, targetY, flightDuration);
data.setStartX(startX);
data.setStartY(startY);
bulletDatas.put(entityId, data);
explosives.put(entityId, new Explosive(explosionRadius, damage));
collisions.put(entityId, new Collision(0.2f));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.BULLET));
playerBullets.add(entityId);
return entityId;
}
/**
* 创建燃烧瓶实体(抛物线飞行,落地后产生火焰区域)
*/
public int createMolotovEntity(float startX, float startY, float targetX, float targetY,
float flightDuration, int damage, String ownerId,
float explosionRadius, float fireZoneRadius,
float fireZoneDamage, float fireZoneDuration) {
int entityId = createEntity();
positions.put(entityId, new Position(startX, startY));
float vx = (targetX - startX) / flightDuration;
float vy = (targetY - startY) / flightDuration;
velocities.put(entityId, new Velocity(vx, vy, 3.0f));
BulletData data = BulletData.createMolotov(damage, ownerId, targetX, targetY, flightDuration);
data.setStartX(startX);
data.setStartY(startY);
bulletDatas.put(entityId, data);
// 存储爆炸参数(落地初始爆炸使用)
explosives.put(entityId, new Explosive(explosionRadius, damage));
// 存储火焰区域参数落地时创建FireZone使用
fireZonesData.put(entityId, new FireZone(fireZoneRadius, fireZoneDamage, fireZoneDuration, ownerId));
collisions.put(entityId, new Collision(0.2f));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.BULLET));
playerBullets.add(entityId);
return entityId;
}
// ==================== 掉落物实体创建 ====================
/**
* 创建掉落物实体(弹药或生命值)
*/
public int createLootEntity(float x, float y, String type) {
int entityId = createEntity();
positions.put(entityId, new Position(x, y));
lootDatas.put(entityId, new LootData(type));
collisions.put(entityId, new Collision(0.8f));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.LOOT));
loots.add(entityId);
return entityId;
}
// ==================== 燃烧区域实体创建 ====================
/**
* 创建火焰区域实体(燃烧瓶落地后产生)
*/
public int createFireZoneEntity(float x, float y, float radius, float damagePerTick,
float duration, String ownerId) {
int entityId = createEntity();
positions.put(entityId, new Position(x, y));
fireZonesData.put(entityId, new FireZone(radius, damagePerTick, duration, ownerId));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.FIRE_ZONE));
fireZones.add(entityId);
return entityId;
}
// ==================== 机枪塔实体创建 ====================
/**
* 创建机枪塔实体(自动射击范围内僵尸)
*/
public int createTurretEntity(float x, float y, int gridX, int gridY,
float fireRange, long fireRate, int damage, float bulletSpeed, float hp) {
int entityId = createEntity();
positions.put(entityId, new Position(x, y));
healths.put(entityId, new Health(hp));
collisions.put(entityId, new Collision(ZOMBIE_SIZE));
turretStates.put(entityId, new TurretState(fireRange, fireRate, damage, bulletSpeed));
wallEntityDatas.put(entityId, new WallEntity(gridX, gridY));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.TURRET));
turrets.add(entityId);
wallEntities.add(entityId);
return entityId;
}
// ==================== 查询辅助方法 ====================
/** 获取所有实体ID的只读视图 */
public Set<Integer> getEntities() { return Collections.unmodifiableSet(entities); }
/** 获取僵尸模板(带缓存) */
public ZombieTemplate getZombieTemplate(String templateId) {
return zombieTemplateCache.computeIfAbsent(templateId,
id -> TemplateManager.getInstance().getZombieTemplate(id));
}
/** 获取当前僵尸数量 */
public int getZombieCount() { return zombies.size(); }
/** 获取地图网格数据 */
public int[][] getMapData() { return map.getCells(); }
// ==================== 分数 ====================
/** 增加分数 */
public void addScore(int amount) { score += amount; }
/**
* 更新流场(根据存活玩家位置重新计算寻路)
* 每帧在系统更新前调用
*/
private void updateFlowField() {
List<float[]> playerPositions = new ArrayList<>();
for (int entityId : players) {
Health h = healths.get(entityId);
Position p = positions.get(entityId);
if (h != null && h.isAlive() && p != null) {
playerPositions.add(new float[]{p.getX(), p.getY()});
}
}
if (!playerPositions.isEmpty()) {
map.updateFlowField(playerPositions);
}
}
// ==================== 主更新循环 ====================
/**
* 每帧更新:清空临时数据,更新流场,按顺序执行所有系统
* @param dt 帧间隔(秒)
*/
public void update(float dt) {
explosions.clear();
removedBullets.clear();
removedZombieBullets.clear();
gameTime += dt;
// 根据存活玩家位置更新流场
updateFlowField();
for (System system : systems) {
system.update(dt, this);
}
}
/** 注册系统到更新列表 */
public void addSystem(System system) {
systems.add(system);
}
}

View File

@@ -0,0 +1,10 @@
package com.zombie.game.ecs;
/**
* ECS 系统接口
*
* 所有系统实现此接口,在每帧 update 中处理具有特定组件组合的实体。
*/
public interface System {
void update(float dt, ECSWorld world);
}

View File

@@ -0,0 +1,84 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 子弹数据组件
* 存储子弹/投掷物的伤害、所有者、飞行状态等信息。
*/
@Data
public class BulletData {
/** 伤害值 */
private int damage;
/** 所有者ID玩家ID或僵尸ID */
private String ownerId;
/** 武器索引 */
private int weaponIndex;
/** 射程(标准子弹使用) */
private float range;
/** 已飞行距离(标准子弹使用) */
private float distanceTraveled;
/** 是否为手榴弹 */
private boolean isGrenade;
/** 是否为燃烧瓶 */
private boolean isMolotov;
/** 当前飞行时间(投掷物使用) */
private float flightTime;
/** 最大飞行时间(投掷物使用) */
private float maxFlightTime;
/** 起始位置 */
private float startX, startY;
/** 目标位置(投掷物使用) */
private float targetX, targetY;
/** 是否有目标位置 */
private boolean hasTarget;
public BulletData(int damage, String ownerId, int weaponIndex, float range) {
this.damage = damage;
this.ownerId = ownerId;
this.weaponIndex = weaponIndex;
this.range = range;
this.distanceTraveled = 0;
this.isGrenade = false;
this.isMolotov = false;
this.flightTime = 0;
this.maxFlightTime = 0;
this.hasTarget = false;
}
/**
* 创建手榴弹数据
* @param damage 伤害
* @param ownerId 投掷者ID
* @param targetX 目标X坐标
* @param targetY 目标Y坐标
* @param flightDuration 飞行时长
*/
public static BulletData createGrenade(int damage, String ownerId, float targetX, float targetY, float flightDuration) {
BulletData data = new BulletData(damage, ownerId, -1, 0);
data.isGrenade = true;
data.targetX = targetX;
data.targetY = targetY;
data.maxFlightTime = flightDuration;
data.hasTarget = true;
return data;
}
/**
* 创建燃烧瓶数据
* @param damage 伤害
* @param ownerId 投掷者ID
* @param targetX 目标X坐标
* @param targetY 目标Y坐标
* @param flightDuration 飞行时长
*/
public static BulletData createMolotov(int damage, String ownerId, float targetX, float targetY, float flightDuration) {
BulletData data = new BulletData(damage, ownerId, -1, 0);
data.isMolotov = true;
data.targetX = targetX;
data.targetY = targetY;
data.maxFlightTime = flightDuration;
data.hasTarget = true;
return data;
}
}

View File

@@ -0,0 +1,17 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 碰撞组件
* 存储实体的碰撞尺寸(直径)。
*/
@Data
public class Collision {
/** 碰撞尺寸(直径) */
private float size;
public Collision(float size) {
this.size = size;
}
}

View File

@@ -0,0 +1,20 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 爆炸组件
* 标记实体具有爆炸属性,用于手榴弹和燃烧瓶的爆炸伤害计算。
*/
@Data
public class Explosive {
/** 爆炸半径 */
private float explosionRadius;
/** 爆炸伤害 */
private int explosionDamage;
public Explosive(float explosionRadius, int explosionDamage) {
this.explosionRadius = explosionRadius;
this.explosionDamage = explosionDamage;
}
}

View File

@@ -0,0 +1,42 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 燃烧区域组件
* 燃烧瓶落地后产生的持续伤害火焰区域。
*/
@Data
public class FireZone {
/** 火焰半径 */
private float radius;
/** 每帧伤害值 */
private float damagePerTick;
/** 持续时间(秒) */
private float duration;
/** 已持续时间(秒) */
private float elapsed;
/** 创建者ID */
private String ownerId;
public FireZone(float radius, float damagePerTick, float duration, String ownerId) {
this.radius = radius;
this.damagePerTick = damagePerTick;
this.duration = duration;
this.elapsed = 0;
this.ownerId = ownerId;
}
/** 判断火焰区域是否已过期 */
public boolean isExpired() {
return elapsed >= duration;
}
/**
* 更新已持续时间
* @param dt 帧间隔(秒)
*/
public void update(float dt) {
elapsed += dt;
}
}

View File

@@ -0,0 +1,65 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 生命值组件
* 管理实体的血量、最大血量、无敌时间和受伤冷却。
*/
@Data
public class Health {
/** 当前血量 */
private float health;
/** 最大血量 */
private float maxHealth;
/** 无敌时间(秒),受伤后短时间内免疫伤害 */
private float invulnerableTime;
/** 上次受伤时间戳(毫秒) */
private long lastDamageTime;
public Health(float maxHealth, float invulnerableTime) {
this.health = maxHealth;
this.maxHealth = maxHealth;
this.invulnerableTime = invulnerableTime;
this.lastDamageTime = 0;
}
public Health(float maxHealth) {
this(maxHealth, 0);
}
/**
* 判断实体是否存活
*/
public boolean isAlive() {
return health > 0;
}
/**
* 受到伤害,考虑无敌时间
* @param damage 伤害值
*/
public void takeDamage(float damage) {
long now = System.currentTimeMillis();
if (invulnerableTime > 0 && now - lastDamageTime < invulnerableTime * 1000) return;
this.health -= damage;
if (this.health < 0) this.health = 0;
this.lastDamageTime = now;
}
/**
* 恢复血量,不超过最大值
* @param amount 恢复量
*/
public void heal(float amount) {
this.health = Math.min(maxHealth, this.health + amount);
}
/**
* 重置血量至满值(重生时使用)
*/
public void reset() {
this.health = maxHealth;
this.lastDamageTime = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,17 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 掉落物数据组件
* 存储掉落物类型(弹药或生命值)。
*/
@Data
public class LootData {
/** 掉落物类型("ammo" 或 "health" */
private String type;
public LootData(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,41 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 玩家输入组件
* 存储客户端发送的输入状态,每帧由网络层写入。
*/
@Data
public class PlayerInput {
/** 移动方向X-1到1 */
private float dx;
/** 移动方向Y-1到1 */
private float dy;
/** 鼠标瞄准点X世界坐标 */
private float aimX;
/** 鼠标瞄准点Y世界坐标 */
private float aimY;
/** 是否开火 */
private boolean firing;
/** 当前武器索引(-1表示未切换 */
private int weaponIndex;
/** 输入序列号(用于客户端预测校正) */
private int seq;
/** 投掷武器蓄力百分比0-1 */
private float grenadeCharge;
/** 投掷武器是否释放 */
private boolean grenadeReleased;
public PlayerInput() {
this.dx = 0;
this.dy = 0;
this.aimX = 0;
this.aimY = 0;
this.firing = false;
this.weaponIndex = -1;
this.seq = 0;
this.grenadeCharge = 0;
this.grenadeReleased = false;
}
}

View File

@@ -0,0 +1,41 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 位置组件
* 存储实体在世界坐标系中的位置和朝向角度。
*/
@Data
public class Position {
/** X坐标 */
private float x;
/** Y坐标 */
private float y;
/** 朝向角度(弧度) */
private float angle;
public Position(float x, float y) {
this.x = x;
this.y = y;
this.angle = 0;
}
public Position(float x, float y, float angle) {
this.x = x;
this.y = y;
this.angle = angle;
}
/**
* 计算到指定点的距离
* @param px 目标点X坐标
* @param py 目标点Y坐标
* @return 距离值
*/
public float distanceTo(float px, float py) {
float dx = px - x;
float dy = py - y;
return (float) Math.sqrt(dx * dx + dy * dy);
}
}

View File

@@ -0,0 +1,42 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 渲染信息组件
* 供前端渲染使用的实体类型、子类型和武器索引。
*/
@Data
public class RenderInfo {
/**
* 实体类型枚举
*/
public enum EntityType {
PLAYER, // 玩家
ZOMBIE, // 僵尸
BULLET, // 玩家子弹
ZOMBIE_BULLET, // 僵尸子弹
LOOT, // 掉落物
FIRE_ZONE, // 燃烧区域
TURRET // 机枪塔
}
/** 实体类型 */
private EntityType type;
/** 子类型(如僵尸的 elite/splitter/normal */
private String subType;
/** 武器索引(子弹使用) */
private int weaponIndex;
public RenderInfo(EntityType type) {
this.type = type;
this.subType = "";
this.weaponIndex = -1;
}
public RenderInfo(EntityType type, String subType) {
this.type = type;
this.subType = subType;
this.weaponIndex = -1;
}
}

View File

@@ -0,0 +1,50 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 重生状态组件
* 管理玩家死亡后的重生倒计时逻辑。
*/
@Data
public class RespawnState {
/** 是否正在等待重生 */
private boolean waitingForRespawn;
/** 重生倒计时(秒) */
private float respawnTimer;
public RespawnState() {
this.waitingForRespawn = false;
this.respawnTimer = 0;
}
/**
* 开始重生计时
* @param respawnTime 重生等待时间(秒)
*/
public void startRespawnTimer(float respawnTime) {
this.waitingForRespawn = true;
this.respawnTimer = respawnTime;
}
/**
* 更新倒计时
* @param dt 帧间隔(秒)
*/
public void updateTimer(float dt) {
if (waitingForRespawn && respawnTimer > 0) {
respawnTimer -= dt;
}
}
/** 判断是否可以重生(倒计时结束) */
public boolean canRespawn() {
return waitingForRespawn && respawnTimer <= 0;
}
/** 重置重生状态 */
public void reset() {
this.waitingForRespawn = false;
this.respawnTimer = 0;
}
}

View File

@@ -0,0 +1,37 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 机枪塔状态组件
* 存储自动机枪塔的射击参数。
*/
@Data
public class TurretState {
/** 射击范围 */
private float fireRange;
/** 射击间隔(毫秒) */
private long fireRate;
/** 上次射击时间戳(毫秒) */
private long lastFireTime;
/** 子弹伤害 */
private int damage;
/** 子弹速度 */
private float bulletSpeed;
public TurretState(float fireRange, long fireRate, int damage, float bulletSpeed) {
this.fireRange = fireRange;
this.fireRate = fireRate;
this.lastFireTime = 0;
this.damage = damage;
this.bulletSpeed = bulletSpeed;
}
/**
* 判断是否可以射击(冷却时间已过)
* @param now 当前时间戳
*/
public boolean canFire(long now) {
return now - lastFireTime >= fireRate;
}
}

View File

@@ -0,0 +1,27 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 速度组件
* 存储实体在X/Y/Z轴的速度分量主要用于子弹和投掷物。
*/
@Data
public class Velocity {
/** X轴速度 */
private float vx;
/** Y轴速度 */
private float vy;
/** Z轴速度投掷物抛物线使用 */
private float vz;
public Velocity(float vx, float vy, float vz) {
this.vx = vx;
this.vy = vy;
this.vz = vz;
}
public Velocity(float vx, float vy) {
this(vx, vy, 0);
}
}

View File

@@ -0,0 +1,28 @@
package com.zombie.game.ecs.components;
import com.zombie.game.model.Wall;
import lombok.Data;
/**
* 墙体实体组件
* 用于追踪放置类实体(坚果墙、机枪塔)在地图网格中的位置。
*/
@Data
public class WallEntity {
/** 网格X坐标 */
private int gridX;
/** 网格Y坐标 */
private int gridY;
/** 关联的地图墙体对象引用 */
private Wall wallRef;
public WallEntity(int gridX, int gridY, Wall wallRef) {
this.gridX = gridX;
this.gridY = gridY;
this.wallRef = wallRef;
}
public WallEntity(int gridX, int gridY) {
this(gridX, gridY, null);
}
}

View File

@@ -0,0 +1,124 @@
package com.zombie.game.ecs.components;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.WeaponTemplate;
import lombok.Data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 武器状态组件
* 管理玩家的武器切换、弹药、射击冷却和投掷蓄力。
*/
@Data
public class WeaponState {
/** 武器模板列表(从配置加载,不可变) */
private static final List<WeaponTemplate> WEAPON_TEMPLATES;
static {
List<WeaponTemplate> list = new ArrayList<>(TemplateManager.getInstance().getAllWeaponTemplates());
WEAPON_TEMPLATES = Collections.unmodifiableList(list);
}
/** 当前武器索引 */
private int weaponIndex;
/** 各武器弹药数组 */
private float[] ammo;
/** 上次攻击时间戳(毫秒) */
private long lastAttackTime;
/** 是否正在蓄力(投掷类武器) */
private boolean chargingGrenade;
/** 蓄力开始时间(毫秒) */
private float grenadeChargeStart;
/** 上次处理的输入序列号 */
private int lastProcessedSeq;
public WeaponState() {
this.weaponIndex = 0;
this.ammo = new float[WEAPON_TEMPLATES.size()];
for (int i = 0; i < WEAPON_TEMPLATES.size(); i++) {
this.ammo[i] = WEAPON_TEMPLATES.get(i).getMaxAmmo();
}
this.lastAttackTime = 0;
this.chargingGrenade = false;
this.grenadeChargeStart = 0;
this.lastProcessedSeq = 0;
}
/** 获取当前武器模板 */
public WeaponTemplate getCurrentWeapon() {
return WEAPON_TEMPLATES.get(weaponIndex);
}
/** 获取指定索引的武器模板 */
public WeaponTemplate getWeapon(int index) {
return WEAPON_TEMPLATES.get(index);
}
/** 获取武器总数 */
public int getWeaponCount() {
return WEAPON_TEMPLATES.size();
}
/** 切换武器,自动限制在有效范围内 */
public void setWeaponIndex(int idx) {
this.weaponIndex = Math.max(0, Math.min(WEAPON_TEMPLATES.size() - 1, idx));
}
/**
* 判断当前是否可以开火(冷却时间已过)
* @param now 当前时间戳
*/
public boolean canFire(long now) {
WeaponTemplate weapon = WEAPON_TEMPLATES.get(weaponIndex);
return now - lastAttackTime >= weapon.getFireRate();
}
/**
* 执行开火,更新攻击时间并消耗弹药(手枪除外)
* @param now 当前时间戳
*/
public void fire(long now) {
lastAttackTime = now;
if (weaponIndex != 0 && ammo[weaponIndex] > 0) {
ammo[weaponIndex]--;
}
}
/** 判断当前武器是否有弹药(手枪无限) */
public boolean hasAmmo() {
if (weaponIndex == 0) return true;
return ammo[weaponIndex] > 0;
}
/** 随机补充一把非手枪武器的弹药(拾取掉落物时使用) */
public void refillRandomWeapon() {
java.util.Random rand = new java.util.Random();
int idx = rand.nextInt(WEAPON_TEMPLATES.size() - 1) + 1;
ammo[idx] = WEAPON_TEMPLATES.get(idx).getMaxAmmo();
}
/** 开始投掷武器蓄力 */
public void startGrenadeCharge() {
if (!chargingGrenade) {
chargingGrenade = true;
grenadeChargeStart = System.currentTimeMillis();
}
}
/**
* 获取蓄力百分比0-1
* 按最大蓄力时间2秒计算
*/
public float getGrenadeChargePercent() {
if (!chargingGrenade) return 0;
float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f;
return Math.min(1.0f, elapsed);
}
/** 停止蓄力 */
public void stopGrenadeCharge() {
chargingGrenade = false;
}
}

View File

@@ -0,0 +1,46 @@
package com.zombie.game.ecs.components;
import lombok.Data;
/**
* 僵尸AI组件
* 存储僵尸的寻路目标、格子预留、墙体攻击等AI状态。
*/
@Data
public class ZombieAI {
/** 僵尸模板IDnormal/elite/splitter */
private String templateId;
/** 当前移动目标X坐标 */
private float targetX, targetY;
/** 是否有移动目标 */
private boolean hasTarget;
/** 预留的网格X坐标防止僵尸重叠 */
private int reservedGridX, reservedGridY;
/** 是否持有网格预留 */
private boolean reservation;
/** 正在攻击的墙体网格X坐标 */
private int attackingWallGridX = -1;
/** 正在攻击的墙体网格Y坐标 */
private int attackingWallGridY = -1;
/** 是否正在攻击墙体 */
private boolean attackingWall;
/** 上次近战攻击时间戳(毫秒) */
private long lastAttackTime;
/** 上次远程攻击时间戳(毫秒,精英僵尸使用) */
private long lastRangedAttackTime;
public ZombieAI(String templateId) {
this.templateId = templateId;
this.targetX = 0;
this.targetY = 0;
this.hasTarget = false;
this.reservedGridX = -1;
this.reservedGridY = -1;
this.reservation = false;
this.attackingWallGridX = -1;
this.attackingWallGridY = -1;
this.attackingWall = false;
this.lastAttackTime = 0;
this.lastRangedAttackTime = 0;
}
}

View File

@@ -1,173 +0,0 @@
package com.zombie.game.model;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 子弹/投掷物类
*
* 管理玩家和僵尸发射的子弹、手榴弹等投掷物。
* 支持普通子弹的直线飞行和手榴弹的抛物线轨迹。
*/
@Getter
public class Bullet {
private int id;
private float x, y;
private float z;
private float vx, vy, vz;
private float speed;
private int damage;
private String ownerId;
private int weaponIndex;
private float range;
private float distanceTraveled;
private boolean explosive;
private float explosionRadius;
private float flightTime;
private float maxFlightTime;
private float targetX, targetY;
private boolean isGrenade;
public Bullet(int id, float x, float y, float angle, float speed, int damage,
String ownerId, int weaponIndex, float range) {
this.id = id;
this.x = x;
this.y = y;
this.z = 0.5f;
this.speed = speed;
this.vx = (float) Math.sin(angle) * speed;
this.vy = (float) Math.cos(angle) * speed;
this.vz = 0;
this.damage = damage;
this.ownerId = ownerId;
this.weaponIndex = weaponIndex;
this.range = range;
this.distanceTraveled = 0;
this.explosive = false;
this.explosionRadius = 0;
this.flightTime = 0;
this.maxFlightTime = 0;
this.isGrenade = false;
this.targetX = x + (float) Math.sin(angle) * range;
this.targetY = y + (float) Math.cos(angle) * range;
}
/**
* 构造函数 - 手榴弹
*
* @param id 子弹ID
* @param startX 起始X坐标
* @param startY 起始Y坐标
* @param targetX 目标X坐标
* @param targetY 目标Y坐标
* @param flightDuration 飞行时长
* @param damage 伤害值
* @param ownerId 发射者ID
* @param explosionRadius 爆炸半径
*/
public Bullet(int id, float startX, float startY, float targetX, float targetY,
float flightDuration, int damage, String ownerId, float explosionRadius) {
this.id = id;
this.x = startX;
this.y = startY;
this.z = 0.5f;
this.targetX = targetX;
this.targetY = targetY;
this.damage = damage;
this.ownerId = ownerId;
this.weaponIndex = -1;
this.range = 0;
this.distanceTraveled = 0;
this.explosive = true;
this.explosionRadius = explosionRadius;
this.flightTime = 0;
this.maxFlightTime = flightDuration;
this.isGrenade = true;
this.speed = 0;
float dx = targetX - startX;
float dy = targetY - startY;
this.vx = dx / flightDuration;
this.vy = dy / flightDuration;
this.vz = 3.0f;
}
public boolean update(float dt, GameMap map) {
if (isGrenade) {
flightTime += dt;
x += vx * dt;
y += vy * dt;
float progress = flightTime / maxFlightTime;
z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
if (flightTime >= maxFlightTime || z <= 0.5f && progress > 0.5f) {
return false;
}
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
return false;
}
return true;
} else {
float moveX = vx * dt;
float moveY = vy * dt;
x += moveX;
y += moveY;
distanceTraveled += (float) Math.sqrt(moveX * moveX + moveY * moveY);
if (distanceTraveled >= range) return false;
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false;
int gx = (int) Math.floor(x);
int gy = (int) Math.floor(y);
// 只有碰到静态墙壁才销毁子弹,碰到坚果墙体让碰撞检测处理
Wall wall = map.getWall(gx, gy);
if (wall instanceof StaticWall) return false;
return true;
}
}
/**
* 检测子弹是否命中实体
*
* @param ex 实体X坐标
* @param ey 实体Y坐标
* @param size 实体碰撞体大小
* @return true 表示命中
*/
public boolean hitsEntity(float ex, float ey, float size) {
float dx = x - ex;
float dy = y - ey;
float dist = (float) Math.sqrt(dx * dx + dy * dy);
return dist < size / 2 + 0.1f;
}
/**
* 将子弹状态转换为Map格式用于网络传输
*
* @return 包含子弹状态的Map
*/
public Map<String, Object> toStateMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id);
map.put("x", x);
map.put("y", y);
map.put("z", z);
if (isGrenade) {
map.put("angle", (float) Math.atan2(vx, vy));
map.put("targetX", targetX);
map.put("targetY", targetY);
} else {
map.put("angle", (float) Math.atan2(vx, vy));
}
map.put("weaponIndex", weaponIndex);
map.put("ownerId", ownerId);
return map;
}
}

View File

@@ -1,693 +0,0 @@
package com.zombie.game.model;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 游戏世界类
*
* 管理整个游戏状态,包括:
* - 玩家、僵尸、子弹、掉落物等实体
* - 游戏时间、波数、分数
* - 难度递增系统
* - 碰撞检测和游戏逻辑更新
*/
@Getter
public class GameWorld {
private final Object lock = new Object();
private GameMap map;
private Map<String, Player> players;
private Map<Integer, Zombie> zombies;
private Map<Integer, Bullet> bullets;
private Map<Integer, Bullet> zombieBullets;
private Map<Integer, Loot> loots;
private int nextBulletId;
private int nextZombieId;
private int nextLootId;
private int nextZombieBulletId;
private float gameTime;
private int waveNumber;
private int score;
private float spawnTimer;
private float difficultyTimer;
private float zombieHealth;
private float zombieSpeed;
private float spawnInterval;
private Random random;
private List<Map<String, Object>> explosions;
private List<Integer> removedBullets;
private List<Integer> removedZombieBullets;
private static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
private static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.2f;
private static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
private static final float ZOMBIE_HEALTH_INCREASE = 20;
private static final float ZOMBIE_SPEED_INCREASE = 0.1f;
private static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
public GameWorld() {
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<>();
this.zombieBullets = new LinkedHashMap<>();
this.loots = new LinkedHashMap<>();
this.nextBulletId = 0;
this.nextZombieId = 0;
this.nextLootId = 0;
this.nextZombieBulletId = 0;
this.gameTime = 0;
this.waveNumber = 0;
this.score = 0;
this.spawnTimer = 0;
this.difficultyTimer = 0;
ZombieTemplate normal = TemplateManager.getInstance().getZombieTemplate("normal");
this.zombieHealth = normal.getBaseHealth();
this.zombieSpeed = normal.getBaseSpeed();
this.spawnInterval = ZOMBIE_SPAWN_INTERVAL_BASE;
this.random = new Random();
this.explosions = new ArrayList<>();
this.removedBullets = new ArrayList<>();
this.removedZombieBullets = new ArrayList<>();
}
/**
* 添加玩家到游戏世界
*
* @param player 玩家对象
*/
public void addPlayer(Player player) {
List<int[]> spawnPoints = map.getSpawnPoints();
int idx = players.size() % spawnPoints.size();
int[] sp = spawnPoints.get(idx);
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
player.applyMovement(0, 0, map);
players.put(player.getId(), player);
}
/**
* 从游戏世界移除玩家
*
* @param playerId 玩家ID
*/
public void removePlayer(String playerId) {
players.remove(playerId);
}
/** 获取指定ID的玩家 */
public Player getPlayer(String id) { return players.get(id); }
/** 获取所有玩家 */
public Collection<Player> getPlayers() { return players.values(); }
/** 获取所有僵尸 */
public Collection<Zombie> getZombies() { return zombies.values(); }
/** 获取所有玩家子弹 */
public Collection<Bullet> getBullets() { return bullets.values(); }
/** 获取所有掉落物 */
public Collection<Loot> getLoots() { return loots.values(); }
/**
* 更新游戏世界状态
*
* 每帧调用,处理:
* - 时间流逝和难度提升
* - 僵尸生成
* - 实体移动和碰撞
* - 掉落物收集
* - 玩家重生
*
* @param dt 时间增量(秒)
*/
public void update(float dt) {
explosions.clear();
removedBullets.clear();
removedZombieBullets.clear();
gameTime += dt;
spawnTimer += dt;
difficultyTimer += dt;
updateFlowField();
if (difficultyTimer >= DIFFICULTY_INCREASE_INTERVAL) {
difficultyTimer -= DIFFICULTY_INCREASE_INTERVAL;
waveNumber++;
spawnInterval = Math.max(ZOMBIE_SPAWN_INTERVAL_MIN,
spawnInterval - 0.3f);
}
if (spawnTimer >= spawnInterval) {
spawnTimer -= spawnInterval;
spawnZombie();
}
updateZombies(dt);
updateBullets(dt);
updateZombieBullets(dt);
checkBulletCollisions();
checkZombieBulletCollisions();
checkZombieAttacks();
checkLootCollection();
checkPlayerRespawn();
}
/**
* 更新流场导航
*
* 基于存活玩家的位置更新流场
*/
private void updateFlowField() {
List<float[]> playerPositions = new ArrayList<>();
for (Player p : players.values()) {
if (p.isAlive()) {
playerPositions.add(new float[]{p.getX(), p.getY()});
}
}
if (!playerPositions.isEmpty()) {
map.updateFlowField(playerPositions);
}
}
/**
* 生成僵尸
*
* 根据概率生成普通僵尸、精英僵尸或分裂僵尸
*/
private void spawnZombie() {
List<int[]> spawnPoints = map.getZombieSpawnPoints();
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
ZombieTemplate elite = TemplateManager.getInstance().getZombieTemplate("elite");
ZombieTemplate splitter = TemplateManager.getInstance().getZombieTemplate("splitter");
float eliteChance = elite.getSpawnWeight();
float splitterChance = splitter.getSpawnWeight();
float roll = random.nextFloat();
Zombie zombie;
if (roll < eliteChance) {
zombie = new Zombie(nextZombieId++, wx, wy, "elite");
} else if (roll < eliteChance + splitterChance) {
zombie = new Zombie(nextZombieId++, wx, wy, "splitter");
} else {
zombie = new Zombie(nextZombieId++, wx, wy, "normal");
}
zombies.put(zombie.getId(), zombie);
}
/**
* 查找最近的存活玩家
*
* @param x X坐标
* @param y Y坐标
* @return 最近的玩家,如果没有存活玩家则返回 null
*/
private Player findNearestPlayer(float x, float y) {
Player nearest = null;
float minDist = Float.MAX_VALUE;
for (Player p : players.values()) {
if (!p.isAlive()) continue;
float dist = p.distanceTo(x, y);
if (dist < minDist) {
minDist = dist;
nearest = p;
}
}
return nearest;
}
/**
* 更新所有僵尸
*
* 处理僵尸移动、攻击和死亡
*
* @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);
zombies.remove(z.getId());
continue;
}
if (z.isElite()) {
Player nearest = findNearestPlayer(z.getX(), z.getY());
if (nearest != null) {
float dist = z.distanceTo(nearest.getX(), nearest.getY());
if (dist <= z.getTemplate().getRangedRange() && z.canRangedAttack(now)) {
fireZombieBullet(z, nearest);
z.rangedAttack(now);
}
}
}
Wall attackedWall = z.move(map, dt, zombies.values(), now);
if (attackedWall != null && z.canAttack(now)) {
attackedWall.takeDamage(1.0f);
z.attack(now);
if (attackedWall.isDestroyed()) {
map.removeWall(attackedWall.getGridX(), attackedWall.getGridY());
}
}
}
}
/**
* 精英僵尸发射子弹
*
* @param zombie 发射子弹的僵尸
* @param target 目标玩家
*/
private void fireZombieBullet(Zombie zombie, Player target) {
float dx = target.getX() - zombie.getX();
float dy = target.getY() - zombie.getY();
float angle = (float) Math.atan2(dx, dy);
float startX = zombie.getX() + (float) Math.sin(angle) * 0.5f;
float startY = zombie.getY() + (float) Math.cos(angle) * 0.5f;
Bullet bullet = new Bullet(nextZombieBulletId++, startX, startY, angle,
zombie.getTemplate().getRangedBulletSpeed(), zombie.getTemplate().getRangedDamage(),
"zombie_" + zombie.getId(), -1, 15);
zombieBullets.put(bullet.getId(), bullet);
}
/**
* 更新所有僵尸子弹
*
* @param dt 时间增量(秒)
*/
private void updateZombieBullets(float dt) {
List<Integer> toRemove = new ArrayList<>();
for (Bullet b : zombieBullets.values()) {
if (!b.update(dt, map)) {
toRemove.add(b.getId());
}
}
for (int id : toRemove) {
zombieBullets.remove(id);
removedZombieBullets.add(id);
}
}
/**
* 检测僵尸子弹与玩家/墙体的碰撞
*/
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(), PLAYER_SIZE)) {
p.takeDamage(b.getDamage());
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);
removedZombieBullets.add(id);
}
}
/**
* 处理僵尸死亡
*
* - 分裂僵尸分裂成多个小僵尸
* - 增加分数
* - 可能掉落物品
*
* @param z 被击杀的僵尸
*/
private void onZombieKilled(Zombie z) {
if (z.isSplitter()) {
int splitCount = z.getTemplate().getMinSplit() +
random.nextInt(z.getTemplate().getMaxSplit() - z.getTemplate().getMinSplit() + 1);
for (int i = 0; i < splitCount; i++) {
float offsetX = (random.nextFloat() - 0.5f) * 1.0f;
float offsetY = (random.nextFloat() - 0.5f) * 1.0f;
Zombie splitZombie = new Zombie(nextZombieId++,
z.getX() + offsetX, z.getY() + offsetY, "normal");
zombies.put(splitZombie.getId(), splitZombie);
}
score += 20;
} else {
score += z.isElite() ? 50 : 10;
}
if (random.nextFloat() < ZOMBIE_LOOT_DROP_CHANCE) {
String lootType = random.nextFloat() < 0.5f ? LOOT_TYPE_AMMO : LOOT_TYPE_HEALTH;
Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType);
loots.put(loot.getId(), loot);
}
}
/**
* 更新所有玩家子弹
*
* @param dt 时间增量(秒)
*/
private void updateBullets(float dt) {
List<Integer> toRemove = new ArrayList<>();
for (Bullet b : bullets.values()) {
if (!b.update(dt, map)) {
if (b.isExplosive()) {
createExplosion(b.getX(), b.getY(), b.getExplosionRadius(), b.getOwnerId());
}
toRemove.add(b.getId());
}
}
for (int id : toRemove) {
bullets.remove(id);
removedBullets.add(id);
}
}
/**
* 检测玩家子弹与僵尸/墙体的碰撞
*/
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(), ZOMBIE_SIZE)) {
z.takeDamage(b.getDamage());
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);
removedBullets.add(id);
}
}
/**
* 创建爆炸效果
*
* 对范围内的僵尸造成伤害
*
* @param x 爆炸中心X坐标
* @param y 爆炸中心Y坐标
* @param radius 爆炸半径
* @param ownerId 爆炸发起者ID
*/
private void createExplosion(float x, float y, float radius, String ownerId) {
Map<String, Object> exp = new LinkedHashMap<>();
exp.put("x", x);
exp.put("y", y);
exp.put("radius", radius);
explosions.add(exp);
for (Zombie z : new ArrayList<>(zombies.values())) {
float dist = z.distanceTo(x, y);
if (dist < radius) {
z.takeDamage(120);
}
}
}
/**
* 检测僵尸近战攻击
*/
private void checkZombieAttacks() {
long now = System.currentTimeMillis();
for (Zombie z : zombies.values()) {
if (!z.isAlive()) continue;
for (Player p : players.values()) {
if (!p.isAlive()) continue;
float dist = z.distanceTo(p.getX(), p.getY());
if (dist < 1.0f && z.canAttack(now)) {
p.takeDamage(z.getTemplate().getDamage());
z.attack(now);
}
}
}
}
/**
* 检测掉落物收集
*/
private void checkLootCollection() {
List<Integer> toRemove = new ArrayList<>();
for (Loot loot : loots.values()) {
for (Player p : players.values()) {
if (!p.isAlive()) continue;
if (loot.isCollectedBy(p.getX(), p.getY())) {
if (loot.getType().equals(LOOT_TYPE_HEALTH)) {
p.heal(LOOT_HEALTH_AMOUNT);
} else {
p.refillRandomWeapon();
}
toRemove.add(loot.getId());
break;
}
}
}
for (int id : toRemove) {
loots.remove(id);
}
}
/**
* 检测玩家重生
*/
private void checkPlayerRespawn() {
boolean hasAlivePlayer = false;
for (Player p : players.values()) {
if (p.isAlive()) {
hasAlivePlayer = true;
break;
}
}
for (Player p : players.values()) {
if (p.isWaitingForRespawn()) {
p.updateRespawnTimer(TICK_INTERVAL);
if (hasAlivePlayer && p.canRespawn()) {
List<int[]> spawnPoints = map.getSpawnPoints();
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
p.respawn(wx, wy);
}
}
}
}
/**
* 玩家开火(无蓄力)
*
* @param player 玩家
* @param aimX 瞄准X坐标
* @param aimY 瞄准Y坐标
* @return 新创建的子弹ID列表
*/
public List<Integer> fireWeapon(Player player, float aimX, float aimY) {
return fireWeapon(player, aimX, aimY, 0);
}
/**
* 玩家开火(支持蓄力)
*
* @param player 玩家
* @param aimX 瞄准X坐标
* @param aimY 瞄准Y坐标
* @param chargePercent 蓄力百分比0-1影响手榴弹投掷距离
* @return 新创建的子弹ID列表
*/
public List<Integer> fireWeapon(Player player, float aimX, float aimY, float chargePercent) {
List<Integer> newBulletIds = new ArrayList<>();
long now = System.currentTimeMillis();
if (!player.canFire(now) || !player.hasAmmo()) return newBulletIds;
player.setAngle(aimX, aimY);
player.fire(now);
if (player.isChargeable()) {
float startX = player.getX();
float startY = player.getY();
float minDist = 3.0f;
float maxDist = 15.0f;
float dist = minDist + (maxDist - minDist) * chargePercent;
float dx = aimX - startX;
float dy = aimY - startY;
float targetDist = (float) Math.sqrt(dx * dx + dy * dy);
float targetX, targetY;
if (targetDist < 0.1f) {
targetX = startX + minDist;
targetY = startY;
} else {
float scale = Math.min(dist, targetDist) / targetDist;
targetX = startX + dx * scale;
targetY = startY + dy * scale;
}
targetX = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetX));
targetY = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetY));
float flightDuration = 0.8f + chargePercent * 0.7f;
Bullet bullet = new Bullet(nextBulletId++, startX, startY, targetX, targetY,
flightDuration, player.getDamage(), player.getId(), 3.0f);
bullets.put(bullet.getId(), bullet);
newBulletIds.add(bullet.getId());
} else {
int pellets = player.getPelletCount();
float spread = player.getSpread();
float range = player.getWeaponIndex() == 1 ? 25 : (player.getWeaponIndex() == 2 ? 12 : 30);
for (int i = 0; i < pellets; i++) {
float angle = player.getAngle();
if (spread > 0) {
angle += (random.nextFloat() - 0.5f) * spread * 2;
}
float startX = player.getX() + (float) Math.sin(angle) * 0.5f;
float startY = player.getY() + (float) Math.cos(angle) * 0.5f;
float speed = player.getBulletSpeed();
int damage = player.getDamage();
Bullet bullet = new Bullet(nextBulletId++, startX, startY, angle,
speed, damage, player.getId(), player.getWeaponIndex(), range);
bullets.put(bullet.getId(), bullet);
newBulletIds.add(bullet.getId());
}
}
return newBulletIds;
}
/** 获取地图数据 */
public int[][] getMapData() {
return map.getCells();
}
/**
* 构建游戏状态数据
*
* 将当前游戏世界的所有状态打包成Map格式用于网络传输
*
* @param forPlayerId 目标玩家ID用于发送该玩家的弹药信息
* @return 游戏状态Map
*/
public Map<String, Object> buildGameState(String forPlayerId) {
Map<String, Object> state = new LinkedHashMap<>();
List<Map<String, Object>> playerStates = new ArrayList<>();
for (Player p : players.values()) {
playerStates.add(p.toStateMap());
}
state.put("players", playerStates);
List<Map<String, Object>> zombieStates = new ArrayList<>();
for (Zombie z : zombies.values()) {
zombieStates.add(z.toStateMap());
}
state.put("zombies", zombieStates);
List<Map<String, Object>> bulletStates = new ArrayList<>();
for (Bullet b : bullets.values()) {
bulletStates.add(b.toStateMap());
}
state.put("bullets", bulletStates);
List<Map<String, Object>> zombieBulletStates = new ArrayList<>();
for (Bullet b : zombieBullets.values()) {
zombieBulletStates.add(b.toStateMap());
}
state.put("zombieBullets", zombieBulletStates);
List<Map<String, Object>> lootStates = new ArrayList<>();
for (Loot l : loots.values()) {
lootStates.add(l.toStateMap());
}
state.put("loots", lootStates);
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);
Player forPlayer = players.get(forPlayerId);
if (forPlayer != null) {
Map<String, Object> ammoMap = new LinkedHashMap<>();
int weaponCount = TemplateManager.getInstance().getAllWeaponTemplates().size();
for (int i = 0; i < weaponCount; i++) {
String weaponId = TemplateManager.getInstance().getWeaponId(i);
float ammo = forPlayer.getAmmo()[i];
ammoMap.put(weaponId, ammo == Integer.MAX_VALUE ? -1 : (int) ammo);
}
state.put("ammo", ammoMap);
}
return state;
}
}

View File

@@ -1,46 +0,0 @@
package com.zombie.game.model;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 掉落物类
*
* 管理僵尸死亡后掉落的物品,包括弹药和生命值补给。
*/
@Getter
public class Loot {
private final int id;
private final float x, y;
private final String type;
private final long spawnTime;
public Loot(int id, float x, float y) {
this(id, x, y, Constants.LOOT_TYPE_AMMO);
}
public Loot(int id, float x, float y, String type) {
this.id = id;
this.x = x;
this.y = y;
this.type = type;
this.spawnTime = System.currentTimeMillis();
}
public boolean isCollectedBy(float px, float py) {
float dx = px - x;
float dy = py - y;
return Math.sqrt(dx * dx + dy * dy) < 0.8f;
}
public Map<String, Object> toStateMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id);
map.put("x", x);
map.put("y", y);
map.put("type", type);
return map;
}
}

View File

@@ -1,242 +0,0 @@
package com.zombie.game.model;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.WeaponTemplate;
import com.zombie.game.template.PlayerTemplate;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 玩家类
*
* 管理玩家状态,包括:
* - 位置、朝向、生命值
* - 武器和弹药
* - 移动和射击
* - 重生机制
*/
@Getter
public class Player {
private String id;
private String name;
private float x, y;
private float angle;
private float health;
private int weaponIndex;
private boolean ready;
private long lastAttackTime;
private long lastDamageTime;
private float[] ammo;
private boolean firing;
private float grenadeChargeStart;
private boolean chargingGrenade;
private int lastProcessedSeq;
private float respawnTimer;
private boolean waitingForRespawn;
private static final List<WeaponTemplate> WEAPON_TEMPLATES;
static {
List<WeaponTemplate> list = new ArrayList<>(TemplateManager.getInstance().getAllWeaponTemplates());
WEAPON_TEMPLATES = Collections.unmodifiableList(list);
}
private final PlayerTemplate playerTemplate;
public Player(String id, String name, float x, float y) {
this.playerTemplate = TemplateManager.getInstance().getPlayerTemplate();
this.id = id;
this.name = name;
this.x = x;
this.y = y;
this.angle = 0;
this.health = playerTemplate.getMaxHealth();
this.weaponIndex = 0;
this.ready = false;
this.lastAttackTime = 0;
this.lastDamageTime = 0;
this.ammo = new float[WEAPON_TEMPLATES.size()];
for (int i = 0; i < WEAPON_TEMPLATES.size(); i++) {
this.ammo[i] = WEAPON_TEMPLATES.get(i).getMaxAmmo();
}
this.firing = false;
this.grenadeChargeStart = 0;
this.chargingGrenade = false;
this.lastProcessedSeq = 0;
this.respawnTimer = 0;
this.waitingForRespawn = false;
}
public void setReady(boolean ready) { this.ready = ready; }
public void setWeaponIndex(int idx) {
this.weaponIndex = Math.max(0, Math.min(WEAPON_TEMPLATES.size() - 1, idx));
}
public void setPosition(float x, float y) {
this.x = x;
this.y = y;
}
public void applyMovement(float dx, float dy, GameMap map) {
float speed = playerTemplate.getSpeed() * TICK_INTERVAL;
float newX = x + dx * speed;
float newY = y + dy * speed;
if (map.isWalkable(newX, y, playerTemplate.getSize())) {
x = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newX));
}
if (map.isWalkable(x, newY, playerTemplate.getSize())) {
y = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newY));
}
}
public void setAngle(float aimX, float aimY) {
this.angle = (float) Math.atan2(aimX - x, aimY - y);
}
public void takeDamage(float damage) {
long now = System.currentTimeMillis();
if (now - lastDamageTime < playerTemplate.getInvulnerableTime() * 1000) return;
this.health -= damage;
if (this.health < 0) this.health = 0;
if (this.health <= 0) {
startRespawnTimer();
}
}
public void startRespawnTimer() {
this.waitingForRespawn = true;
this.respawnTimer = playerTemplate.getRespawnTime();
}
public void updateRespawnTimer(float dt) {
if (waitingForRespawn && respawnTimer > 0) {
respawnTimer -= dt;
}
}
public boolean canRespawn() {
return waitingForRespawn && respawnTimer <= 0;
}
public void respawn(float newX, float newY) {
this.health = playerTemplate.getMaxHealth();
this.x = newX;
this.y = newY;
this.waitingForRespawn = false;
this.respawnTimer = 0;
this.lastDamageTime = System.currentTimeMillis();
}
public boolean isWaitingForRespawn() {
return waitingForRespawn;
}
public float getRespawnTimer() {
return respawnTimer;
}
public boolean isAlive() {
return health > 0;
}
public float distanceTo(float px, float py) {
float dx = px - x;
float dy = py - y;
return (float) Math.sqrt(dx * dx + dy * dy);
}
public boolean canFire(long now) {
WeaponTemplate weapon = WEAPON_TEMPLATES.get(weaponIndex);
return now - lastAttackTime >= weapon.getFireRate();
}
public void fire(long now) {
lastAttackTime = now;
if (weaponIndex != 0 && ammo[weaponIndex] > 0) {
ammo[weaponIndex]--;
}
}
public boolean hasAmmo() {
if (weaponIndex == 0) return true;
return ammo[weaponIndex] > 0;
}
public void refillRandomWeapon() {
Random rand = new Random();
int idx = rand.nextInt(WEAPON_TEMPLATES.size() - 1) + 1;
ammo[idx] = WEAPON_TEMPLATES.get(idx).getMaxAmmo();
}
public void heal(float amount) {
this.health = Math.min(playerTemplate.getMaxHealth(), this.health + amount);
}
public void setFiring(boolean firing) { this.firing = firing; }
public void setLastProcessedSeq(int seq) { this.lastProcessedSeq = seq; }
public void startGrenadeCharge() {
if (!chargingGrenade) {
chargingGrenade = true;
grenadeChargeStart = System.currentTimeMillis();
}
}
public float getGrenadeChargePercent() {
if (!chargingGrenade) return 0;
float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f;
return Math.min(1.0f, elapsed);
}
public void stopGrenadeCharge() {
chargingGrenade = false;
}
public long getFireRate() {
return WEAPON_TEMPLATES.get(weaponIndex).getFireRate();
}
public int getDamage() {
return WEAPON_TEMPLATES.get(weaponIndex).getDamage();
}
public float getBulletSpeed() {
return WEAPON_TEMPLATES.get(weaponIndex).getBulletSpeed();
}
public int getPelletCount() {
return WEAPON_TEMPLATES.get(weaponIndex).getPelletCount();
}
public float getSpread() {
return WEAPON_TEMPLATES.get(weaponIndex).getSpread();
}
public boolean isChargeable() {
return WEAPON_TEMPLATES.get(weaponIndex).isChargeable();
}
public boolean isExplosive() {
return WEAPON_TEMPLATES.get(weaponIndex).isExplosive();
}
public float getExplosionRadius() {
return WEAPON_TEMPLATES.get(weaponIndex).getExplosionRadius();
}
public Map<String, Object> toStateMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id);
map.put("x", x);
map.put("y", y);
map.put("angle", angle);
map.put("health", health);
map.put("weaponIndex", weaponIndex);
map.put("lastProcessedSeq", lastProcessedSeq);
map.put("waitingForRespawn", waitingForRespawn);
map.put("respawnTimer", respawnTimer);
return map;
}
}

View File

@@ -0,0 +1,25 @@
package com.zombie.game.model;
import lombok.Getter;
/**
* 玩家大厅信息
*
* 仅用于房间大厅阶段的玩家信息,不包含游戏内状态。
*/
@Getter
public class PlayerInfo {
private final String id;
private final String name;
private boolean ready;
public PlayerInfo(String id, String name) {
this.id = id;
this.name = name;
this.ready = false;
}
public void setReady(boolean ready) {
this.ready = ready;
}
}

View File

@@ -17,7 +17,7 @@ import static com.zombie.game.model.Constants.*;
public class Room { public class Room {
private String id; private String id;
private String hostId; private String hostId;
private Map<String, Player> players; private Map<String, PlayerInfo> players;
private boolean gameStarted; private boolean gameStarted;
private final int maxPlayers = 4; private final int maxPlayers = 4;
@@ -27,15 +27,13 @@ public class Room {
this.id = id; this.id = id;
this.hostId = hostId; this.hostId = hostId;
this.players = new LinkedHashMap<>(); this.players = new LinkedHashMap<>();
Player host = new Player(hostId, hostName, 0, 0); players.put(hostId, new PlayerInfo(hostId, hostName));
players.put(hostId, host);
} }
public boolean addPlayer(String playerId, String playerName) { public boolean addPlayer(String playerId, String playerName) {
if (players.size() >= maxPlayers) return false; if (players.size() >= maxPlayers) return false;
if (players.containsKey(playerId)) return false; if (players.containsKey(playerId)) return false;
Player player = new Player(playerId, playerName, 0, 0); players.put(playerId, new PlayerInfo(playerId, playerName));
players.put(playerId, player);
return true; return true;
} }
@@ -43,11 +41,11 @@ public class Room {
players.remove(playerId); players.remove(playerId);
} }
public Player getPlayer(String playerId) { public PlayerInfo getPlayer(String playerId) {
return players.get(playerId); return players.get(playerId);
} }
public Collection<Player> getPlayers() { public Collection<PlayerInfo> getPlayers() {
return players.values(); return players.values();
} }
@@ -60,18 +58,12 @@ public class Room {
} }
public boolean allReady() { public boolean allReady() {
for (Player p : players.values()) { for (PlayerInfo p : players.values()) {
if (!p.getId().equals(hostId) && !p.isReady()) return false; if (!p.getId().equals(hostId) && !p.isReady()) return false;
} }
return true; return true;
} }
/**
* 将房间状态转换为Map格式用于网络传输
*
* @param playerId 目标玩家ID
* @return 包含房间状态的Map
*/
public Map<String, Object> toStateMap(String playerId) { public Map<String, Object> toStateMap(String playerId) {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("roomId", id); map.put("roomId", id);
@@ -81,7 +73,7 @@ public class Room {
List<Map<String, Object>> playerList = new ArrayList<>(); List<Map<String, Object>> playerList = new ArrayList<>();
int index = 0; int index = 0;
for (Player p : players.values()) { for (PlayerInfo p : players.values()) {
Map<String, Object> pm = new LinkedHashMap<>(); Map<String, Object> pm = new LinkedHashMap<>();
pm.put("id", p.getId()); pm.put("id", p.getId());
pm.put("name", p.getName()); pm.put("name", p.getName());
@@ -93,15 +85,11 @@ public class Room {
return map; return map;
} }
/**
* 将房间信息转换为房间列表格式,用于大厅显示
*
* @return 包含房间信息的Map
*/
public Map<String, Object> toRoomListMap() { public Map<String, Object> toRoomListMap() {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id); map.put("id", id);
map.put("hostName", players.get(hostId) != null ? players.get(hostId).getName() : "Unknown"); PlayerInfo host = players.get(hostId);
map.put("hostName", host != null ? host.getName() : "Unknown");
map.put("playerCount", players.size()); map.put("playerCount", players.size());
return map; return map;
} }

View File

@@ -1,371 +0,0 @@
package com.zombie.game.model;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 僵尸类
*
* 管理僵尸状态和行为,包括:
* - 位置、朝向、生命值
* - 移动和寻路(基于流场导航)
* - 近战和远程攻击
* - 基于模板配置的属性
*/
@Getter
public class Zombie {
private int id;
private float x, y;
private float angle;
private float health;
private float maxHealth;
private float speed;
private long lastAttackTime;
private long lastRangedAttackTime;
private float targetX, targetY;
private boolean hasTarget;
private int reservedGridX, reservedGridY;
private boolean reservation;
private int attackingWallGridX = -1;
private int attackingWallGridY = -1;
private boolean attackingWall;
private final ZombieTemplate template;
public Zombie(int id, float x, float y, String templateId) {
this.template = TemplateManager.getInstance().getZombieTemplate(templateId);
if (this.template == null) {
throw new IllegalArgumentException("Unknown zombie template: " + templateId);
}
this.id = id;
this.x = x;
this.y = y;
this.angle = 0;
this.health = template.getBaseHealth();
this.maxHealth = template.getBaseHealth();
this.speed = template.getBaseSpeed();
this.lastAttackTime = 0;
this.lastRangedAttackTime = 0;
this.targetX = 0;
this.targetY = 0;
this.hasTarget = false;
this.reservedGridX = -1;
this.reservedGridY = -1;
this.reservation = false;
this.attackingWallGridX = -1;
this.attackingWallGridY = -1;
this.attackingWall = false;
}
public boolean isElite() { return template.isCanRangedAttack(); }
public boolean isSplitter() { return template.isCanSplit(); }
public void takeDamage(float damage) {
this.health -= damage;
if (this.health < 0) this.health = 0;
}
public boolean isAlive() {
return health > 0;
}
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 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.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);
nextGridY = currentGridY;
} else if (!map.isWall(currentGridX, currentGridY + (int) Math.signum(dirY))) {
nextGridX = currentGridX;
nextGridY = currentGridY + (int) Math.signum(dirY);
} else {
reservation = false;
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 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;
}
}
float dx = targetX - x;
float dy = targetY - y;
float dist = (float) Math.sqrt(dx * dx + dy * dy);
if (dist < 0.01f) {
hasTarget = false;
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, ZOMBIE_SIZE);
boolean canMoveY = map.isWalkable(x, newY, ZOMBIE_SIZE);
boolean canMoveDiagonal = map.isWalkable(newX, newY, 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, ZOMBIE_SIZE);
} else if (!wallInY && canMoveY) {
y = newY;
canMoveX = map.isWalkable(newX, y, ZOMBIE_SIZE);
}
}
}
if (!blockedByCorner && canMoveDiagonal) {
x = newX;
y = newY;
} else if (!blockedByCorner) {
if (canMoveX) x = newX;
if (canMoveY) y = newY;
}
} else {
if (canMoveX) x = newX;
if (canMoveY) y = newY;
}
float minSeparationDist = 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, ZOMBIE_SIZE)) {
x = pushedX;
y = pushedY;
} else {
if (map.isWalkable(x + pushX, y, ZOMBIE_SIZE)) {
x = x + pushX;
}
if (map.isWalkable(x, y + pushY, ZOMBIE_SIZE)) {
y = y + pushY;
}
}
}
}
if (dirX != 0 || dirY != 0) {
angle = (float) Math.atan2(dirX, dirY);
}
return null;
}
private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> otherZombies) {
for (Zombie other : otherZombies) {
if (other.getId() == this.id) continue;
if (!other.isAlive()) continue;
int otherGridX = (int) Math.floor(other.getX());
int otherGridY = (int) Math.floor(other.getY());
if (otherGridX == gridX && otherGridY == gridY) {
return true;
}
if (other.isReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
return true;
}
}
return false;
}
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
GameMap map, Collection<Zombie> otherZombies) {
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
java.util.List<int[]> candidates = new java.util.ArrayList<>();
for (int[] dir : allDirs) {
int nx = currentGridX + dir[0];
int ny = currentGridY + dir[1];
if (map.isWall(nx, ny)) continue;
if (dir[0] != 0 && dir[1] != 0) {
if (map.isWall(currentGridX + dir[0], currentGridY) ||
map.isWall(currentGridX, currentGridY + dir[1])) {
continue;
}
}
if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue;
float dotProduct = dir[0] * dirX + dir[1] * dirY;
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
}
if (candidates.isEmpty()) return null;
candidates.sort((a, b) -> b[2] - a[2]);
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
}
public boolean canAttack(long now) {
return now - lastAttackTime >= template.getAttackRate() * 1000;
}
public void attack(long now) {
lastAttackTime = now;
}
public boolean canRangedAttack(long now) {
if (!template.isCanRangedAttack()) return false;
return now - lastRangedAttackTime >= template.getAttackRate() * 1000;
}
public void rangedAttack(long now) {
lastRangedAttackTime = now;
}
public float distanceTo(float px, float py) {
float dx = px - x;
float dy = py - y;
return (float) Math.sqrt(dx * dx + dy * dy);
}
public Map<String, Object> toStateMap() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id);
map.put("x", x);
map.put("y", y);
map.put("angle", angle);
map.put("health", health);
map.put("isElite", isElite());
map.put("isSplitter", isSplitter());
return map;
}
}

View File

@@ -1,6 +1,7 @@
package com.zombie.game.server; package com.zombie.game.server;
import com.zombie.game.model.GameWorld; import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.systems.StateSyncSystem;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -10,37 +11,24 @@ import java.util.concurrent.TimeUnit;
* 游戏循环类 * 游戏循环类
* *
* 管理游戏主循环,以固定帧率更新游戏世界状态。 * 管理游戏主循环,以固定帧率更新游戏世界状态。
* 使用 ScheduledExecutorService 实现精确的定时调度,避免忙等待。
*/ */
public class GameLoop { public class GameLoop {
/** 房间ID */
private String roomId; private String roomId;
/** 游戏世界实例 */ private ECSWorld world;
private GameWorld world;
/** 游戏状态广播回调 */
private GameService.GameStateBroadcast broadcaster; private GameService.GameStateBroadcast broadcaster;
/** 循环运行标志 */ private StateSyncSystem stateSyncSystem;
private volatile boolean running; private volatile boolean running;
/** 逻辑帧率(每秒 tick 数) */
private static final int TICK_RATE = 30; private static final int TICK_RATE = 30;
/** 每次 tick 的时间间隔(毫秒) */
private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE; private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE;
/** 每次 tick 的时间间隔(秒,用于游戏逻辑计算) */
private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE; private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE;
/** 定时任务执行器 */
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
/** public GameLoop(String roomId, ECSWorld world, GameService.GameStateBroadcast broadcaster,
* 构造函数 StateSyncSystem stateSyncSystem) {
*
* @param roomId 房间ID
* @param world 游戏世界实例
* @param broadcaster 游戏状态广播回调
*/
public GameLoop(String roomId, GameWorld world, GameService.GameStateBroadcast broadcaster) {
this.roomId = roomId; this.roomId = roomId;
this.world = world; this.world = world;
this.broadcaster = broadcaster; this.broadcaster = broadcaster;
this.stateSyncSystem = stateSyncSystem;
this.running = true; this.running = true;
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "GameLoop-" + roomId); Thread t = new Thread(r, "GameLoop-" + roomId);
@@ -49,29 +37,18 @@ public class GameLoop {
}); });
} }
/**
* 启动游戏循环
*/
public void start() { public void start() {
scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS); scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS);
} }
/**
* 单次游戏逻辑更新
*/
private void tick() { private void tick() {
if (!running) { if (!running) return;
return;
}
synchronized (world.getLock()) { synchronized (world.getLock()) {
world.update(TICK_INTERVAL_SEC); world.update(TICK_INTERVAL_SEC);
} }
broadcaster.broadcast(roomId, world); broadcaster.broadcast(roomId, world);
} }
/**
* 停止游戏循环
*/
public void stop() { public void stop() {
running = false; running = false;
if (scheduler != null && !scheduler.isShutdown()) { if (scheduler != null && !scheduler.isShutdown()) {

View File

@@ -1,6 +1,9 @@
package com.zombie.game.server; package com.zombie.game.server;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.*; import com.zombie.game.model.*;
import com.zombie.game.systems.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -12,83 +15,87 @@ import java.util.concurrent.ConcurrentHashMap;
* *
* 负责游戏生命周期管理,包括: * 负责游戏生命周期管理,包括:
* - 游戏启动和停止 * - 游戏启动和停止
* - 游戏世界状态管理 * - ECS 游戏世界状态管理
* - 玩家输入处理 * - 玩家输入处理
* - 游戏状态构建 * - 游戏状态构建
*
* 将游戏逻辑从 WebSocket 层解耦,便于独立测试和维护。
*/ */
public class GameService { public class GameService {
private static final Logger logger = LoggerFactory.getLogger(GameService.class); private static final Logger logger = LoggerFactory.getLogger(GameService.class);
/** 活跃游戏世界集合roomId -> GameWorld */ private final Map<String, ECSWorld> activeGames = new ConcurrentHashMap<>();
private final Map<String, GameWorld> activeGames = new ConcurrentHashMap<>();
/** 游戏循环集合roomId -> GameLoop */
private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>(); private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>();
/** 游戏状态广播回调 */
private final GameStateBroadcast broadcaster; private final GameStateBroadcast broadcaster;
/**
* 游戏状态广播接口
*/
public interface GameStateBroadcast { public interface GameStateBroadcast {
void broadcast(String roomId, GameWorld world); void broadcast(String roomId, ECSWorld world);
} }
/**
* 构造函数
*
* @param broadcaster 状态广播回调
*/
public GameService(GameStateBroadcast broadcaster) { public GameService(GameStateBroadcast broadcaster) {
this.broadcaster = broadcaster; this.broadcaster = broadcaster;
} }
/**
* 启动游戏
*
* @param room 游戏房间
* @return 游戏初始化数据按玩家ID分组
*/
public Map<String, Map<String, Object>> startGame(Room room) { public Map<String, Map<String, Object>> startGame(Room room) {
GameWorld world = new GameWorld(); ECSWorld world = new ECSWorld();
// Register systems
StateSyncSystem stateSyncSystem = new StateSyncSystem();
world.addSystem(new DifficultySystem());
world.addSystem(new PlayerInputSystem());
world.addSystem(new ZombieMovementSystem());
world.addSystem(new WeaponFiringSystem());
world.addSystem(new BulletMovementSystem());
world.addSystem(new TurretSystem());
world.addSystem(new CollisionSystem());
world.addSystem(new DamageSystem());
world.addSystem(new ZombieDeathSystem());
world.addSystem(new FireZoneSystem());
world.addSystem(new LootSystem());
world.addSystem(new RespawnSystem());
// Create player entities
int index = 0; int index = 0;
List<int[]> spawnPoints = world.getMap().getSpawnPoints(); List<int[]> spawnPoints = world.getMap().getSpawnPoints();
for (Player player : room.getPlayers()) { for (PlayerInfo playerInfo : room.getPlayers()) {
int[] sp = spawnPoints.get(index % spawnPoints.size()); int[] sp = spawnPoints.get(index % spawnPoints.size());
float wx = sp[0] + 0.5f; float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f; float wy = sp[1] + 0.5f;
player.setPosition(wx, wy); world.createPlayerEntity(playerInfo.getId(), playerInfo.getName(), wx, wy);
world.addPlayer(player);
index++; index++;
} }
activeGames.put(room.getId(), world); activeGames.put(room.getId(), world);
// Build init data
Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>(); Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>();
for (Player player : room.getPlayers()) { for (PlayerInfo playerInfo : room.getPlayers()) {
Map<String, Object> data = new LinkedHashMap<>(); Map<String, Object> data = new LinkedHashMap<>();
data.put("playerId", player.getId()); data.put("playerId", playerInfo.getId());
data.put("mapData", serializeMapData(world.getMapData())); data.put("mapData", serializeMapData(world.getMapData()));
List<Map<String, Object>> playerList = new ArrayList<>(); List<Map<String, Object>> playerList = new ArrayList<>();
int idx = 0; int idx = 0;
for (Player p : room.getPlayers()) { for (PlayerInfo p : room.getPlayers()) {
Integer entityId = world.getPlayerEntity(p.getId());
Map<String, Object> pm = new LinkedHashMap<>(); Map<String, Object> pm = new LinkedHashMap<>();
pm.put("id", p.getId()); pm.put("id", p.getId());
pm.put("name", p.getName()); pm.put("name", p.getName());
pm.put("x", p.getX()); if (entityId != null) {
pm.put("y", p.getY()); Position pos = world.getPositions().get(entityId);
if (pos != null) {
pm.put("x", pos.getX());
pm.put("y", pos.getY());
}
}
pm.put("index", idx++); pm.put("index", idx++);
playerList.add(pm); playerList.add(pm);
} }
data.put("players", playerList); data.put("players", playerList);
playerInitData.put(player.getId(), data); playerInitData.put(playerInfo.getId(), data);
} }
GameLoop loop = new GameLoop(room.getId(), world, broadcaster); GameLoop loop = new GameLoop(room.getId(), world, broadcaster, stateSyncSystem);
gameLoops.put(room.getId(), loop); gameLoops.put(room.getId(), loop);
loop.start(); loop.start();
@@ -96,11 +103,6 @@ public class GameService {
return playerInitData; return playerInitData;
} }
/**
* 停止游戏
*
* @param roomId 房间ID
*/
public void stopGame(String roomId) { public void stopGame(String roomId) {
GameLoop loop = gameLoops.remove(roomId); GameLoop loop = gameLoops.remove(roomId);
if (loop != null) { if (loop != null) {
@@ -110,61 +112,41 @@ public class GameService {
logger.info("Game stopped for room: {}", roomId); logger.info("Game stopped for room: {}", roomId);
} }
/**
* 处理玩家输入
*
* @param roomId 房间ID
* @param playerId 玩家ID
* @param dx X方向移动量
* @param dy Y方向移动量
* @param aimX 瞄准X坐标
* @param aimY 瞄准Y坐标
* @param firing 是否开火
* @param weaponIndex 武器索引
* @param seq 输入序列号
* @param grenadeCharge 手榴弹蓄力值
* @param grenadeReleased 手榴弹是否释放
*/
public void processPlayerInput(String roomId, String playerId, public void processPlayerInput(String roomId, String playerId,
float dx, float dy, float aimX, float aimY, float dx, float dy, float aimX, float aimY,
boolean firing, int weaponIndex, int seq, boolean firing, int weaponIndex, int seq,
float grenadeCharge, boolean grenadeReleased) { float grenadeCharge, boolean grenadeReleased) {
GameWorld world = activeGames.get(roomId); ECSWorld world = activeGames.get(roomId);
if (world == null) return; if (world == null) return;
synchronized (world.getLock()) { synchronized (world.getLock()) {
Player player = world.getPlayer(playerId); Integer entityId = world.getPlayerEntity(playerId);
if (player == null || !player.isAlive()) return; if (entityId == null) return;
if (weaponIndex >= 0 && weaponIndex <= 3) { Health health = world.getHealths().get(entityId);
player.setWeaponIndex(weaponIndex); if (health == null || !health.isAlive()) return;
}
PlayerInput input = world.getPlayerInputs().get(entityId);
player.applyMovement(dx, dy, world.getMap()); if (input == null) return;
player.setAngle(aimX, aimY);
player.setLastProcessedSeq(seq); input.setDx(dx);
input.setDy(dy);
if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) { input.setAimX(aimX);
world.fireWeapon(player, aimX, aimY, grenadeCharge); input.setAimY(aimY);
} else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) { input.setFiring(firing);
world.fireWeapon(player, aimX, aimY); if (weaponIndex >= 0 && weaponIndex <= 6) {
input.setWeaponIndex(weaponIndex);
} }
input.setSeq(seq);
input.setGrenadeCharge(grenadeCharge);
input.setGrenadeReleased(grenadeReleased);
} }
} }
/**
* 检查房间是否有活跃游戏
*
* @param roomId 房间ID
* @return true 表示有活跃游戏
*/
public boolean hasActiveGame(String roomId) { public boolean hasActiveGame(String roomId) {
return activeGames.containsKey(roomId); return activeGames.containsKey(roomId);
} }
/**
* 序列化地图数据为二维列表
*/
private List<List<Integer>> serializeMapData(int[][] cells) { private List<List<Integer>> serializeMapData(int[][] cells) {
List<List<Integer>> result = new ArrayList<>(); List<List<Integer>> result = new ArrayList<>();
for (int[] row : cells) { for (int[] row : cells) {

View File

@@ -2,8 +2,10 @@ 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.ecs.ECSWorld;
import com.zombie.game.model.*; import com.zombie.game.model.*;
import com.zombie.game.model.Constants; import com.zombie.game.model.Constants;
import com.zombie.game.systems.StateSyncSystem;
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;
@@ -18,32 +20,17 @@ import java.util.concurrent.ConcurrentHashMap;
* 游戏 WebSocket 服务器 * 游戏 WebSocket 服务器
* *
* 处理客户端连接和消息,协调房间管理和游戏实例。 * 处理客户端连接和消息,协调房间管理和游戏实例。
* 主要功能:
* - 房间创建、加入、离开
* - 玩家准备和游戏开始
* - 玩家输入处理
* - 游戏状态广播
*/ */
public class GameWebSocketServer extends WebSocketServer { public class GameWebSocketServer extends WebSocketServer {
private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class); private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class);
/** JSON 序列化工具 */
private Gson gson; private Gson gson;
/** 房间管理器 */
private RoomManager roomManager; private RoomManager roomManager;
/** 游戏服务 */
private GameService gameService; private GameService gameService;
/** WebSocket 连接到玩家ID的映射 */
private Map<WebSocket, String> connectionToPlayer; private Map<WebSocket, String> connectionToPlayer;
/** 玩家ID到WebSocket连接的映射 */
private Map<String, WebSocket> playerToConnection; private Map<String, WebSocket> playerToConnection;
/** 房间列表广播定时器 */
private Timer roomListTimer; private Timer roomListTimer;
private Map<String, StateSyncSystem> stateSyncSystems = new ConcurrentHashMap<>();
/**
* 构造函数
*
* @param port 监听端口号
*/
public GameWebSocketServer(int port) { public GameWebSocketServer(int port) {
super(new InetSocketAddress(port)); super(new InetSocketAddress(port));
this.gson = new Gson(); this.gson = new Gson();
@@ -53,19 +40,11 @@ public class GameWebSocketServer extends WebSocketServer {
this.gameService = new GameService(this::broadcastGameState); this.gameService = new GameService(this::broadcastGameState);
} }
/**
* 新连接建立时的回调
*/
@Override @Override
public void onOpen(WebSocket conn, ClientHandshake handshake) { public void onOpen(WebSocket conn, ClientHandshake handshake) {
logger.info("New connection: {}", conn.getRemoteSocketAddress()); logger.info("New connection: {}", conn.getRemoteSocketAddress());
} }
/**
* 连接关闭时的回调
*
* 清理玩家数据并处理离开房间
*/
@Override @Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) { public void onClose(WebSocket conn, int code, String reason, boolean remote) {
logger.info("Connection closed: {}", conn.getRemoteSocketAddress()); logger.info("Connection closed: {}", conn.getRemoteSocketAddress());
@@ -76,11 +55,6 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 收到消息时的回调
*
* 根据消息类型分发到对应的处理方法
*/
@Override @Override
public void onMessage(WebSocket conn, String message) { public void onMessage(WebSocket conn, String message) {
try { try {
@@ -117,19 +91,11 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 发生错误时的回调
*/
@Override @Override
public void onError(WebSocket conn, Exception ex) { public void onError(WebSocket conn, Exception ex) {
logger.error("WebSocket error on connection {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex); logger.error("WebSocket error on connection {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex);
} }
/**
* 服务器启动时的回调
*
* 启动房间列表广播定时器
*/
@Override @Override
public void onStart() { public void onStart() {
logger.info("Game WebSocket Server started on port {}", getPort()); logger.info("Game WebSocket Server started on port {}", getPort());
@@ -142,9 +108,6 @@ public class GameWebSocketServer extends WebSocketServer {
}, 0, 2000); }, 0, 2000);
} }
/**
* 处理创建房间请求
*/
private void handleCreateRoom(WebSocket conn, JsonObject data) { private void handleCreateRoom(WebSocket conn, JsonObject data) {
if (!MessageUtils.hasRequired(data, "playerName")) { if (!MessageUtils.hasRequired(data, "playerName")) {
sendError(conn, "Missing playerName"); sendError(conn, "Missing playerName");
@@ -164,9 +127,6 @@ public class GameWebSocketServer extends WebSocketServer {
logger.info("Room created: {} by {}", roomId, playerName); logger.info("Room created: {} by {}", roomId, playerName);
} }
/**
* 处理加入房间请求
*/
private void handleJoinRoom(WebSocket conn, JsonObject data) { private void handleJoinRoom(WebSocket conn, JsonObject data) {
if (!MessageUtils.hasRequired(data, "roomId", "playerName")) { if (!MessageUtils.hasRequired(data, "roomId", "playerName")) {
sendError(conn, "Missing roomId or playerName"); sendError(conn, "Missing roomId or playerName");
@@ -197,35 +157,25 @@ public class GameWebSocketServer extends WebSocketServer {
logger.info("Player {} joined room {}", playerName, roomId); logger.info("Player {} joined room {}", playerName, roomId);
} }
/**
* 通过连接处理离开房间
*/
private void handleLeaveRoomByConn(WebSocket conn) { private void handleLeaveRoomByConn(WebSocket conn) {
String playerId = connectionToPlayer.get(conn); String playerId = connectionToPlayer.get(conn);
if (playerId == null) return; if (playerId == null) return;
handleLeaveRoomByPlayerId(playerId); handleLeaveRoomByPlayerId(playerId);
} }
/**
* 通过玩家ID处理离开房间
*/
private void handleLeaveRoomByPlayerId(String playerId) { private void handleLeaveRoomByPlayerId(String playerId) {
Room room = roomManager.leaveRoom(playerId); Room room = roomManager.leaveRoom(playerId);
if (room == null) { if (room == null) return;
return;
}
Room updatedRoom = roomManager.getRoom(room.getId()); Room updatedRoom = roomManager.getRoom(room.getId());
if (updatedRoom == null) { if (updatedRoom == null) {
gameService.stopGame(room.getId()); gameService.stopGame(room.getId());
stateSyncSystems.remove(room.getId());
} else { } else {
broadcastRoomState(updatedRoom); broadcastRoomState(updatedRoom);
} }
} }
/**
* 处理获取房间列表请求
*/
private void handleRoomList(WebSocket conn) { private void handleRoomList(WebSocket conn) {
List<Map<String, Object>> roomList = new ArrayList<>(); List<Map<String, Object>> roomList = new ArrayList<>();
for (Room room : roomManager.getAvailableRooms()) { for (Room room : roomManager.getAvailableRooms()) {
@@ -236,9 +186,6 @@ public class GameWebSocketServer extends WebSocketServer {
sendToConnection(conn, Constants.MSG_ROOM_LIST, data); sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
} }
/**
* 处理玩家准备请求
*/
private void handleReady(WebSocket conn) { private void handleReady(WebSocket conn) {
String playerId = connectionToPlayer.get(conn); String playerId = connectionToPlayer.get(conn);
if (playerId == null) return; if (playerId == null) return;
@@ -246,16 +193,13 @@ public class GameWebSocketServer extends WebSocketServer {
Room room = roomManager.getRoomByPlayerId(playerId); Room room = roomManager.getRoomByPlayerId(playerId);
if (room == null) return; if (room == null) return;
Player player = room.getPlayer(playerId); PlayerInfo player = room.getPlayer(playerId);
if (player != null) { if (player != null) {
player.setReady(!player.isReady()); player.setReady(!player.isReady());
broadcastRoomState(room); broadcastRoomState(room);
} }
} }
/**
* 处理开始游戏请求
*/
private void handleStartGame(WebSocket conn) { private void handleStartGame(WebSocket conn) {
String playerId = connectionToPlayer.get(conn); String playerId = connectionToPlayer.get(conn);
if (playerId == null) return; if (playerId == null) return;
@@ -269,11 +213,6 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 启动游戏
*
* @param room 游戏房间
*/
private void startGame(Room room) { private void startGame(Room room) {
Map<String, Map<String, Object>> playerInitData = gameService.startGame(room); Map<String, Map<String, Object>> playerInitData = gameService.startGame(room);
@@ -285,11 +224,6 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 处理玩家输入
*
* 接收并处理玩家的移动、射击、武器切换等输入
*/
private void handlePlayerInput(WebSocket conn, JsonObject data) { private void handlePlayerInput(WebSocket conn, JsonObject data) {
String playerId = connectionToPlayer.get(conn); String playerId = connectionToPlayer.get(conn);
if (playerId == null) return; if (playerId == null) return;
@@ -311,21 +245,16 @@ public class GameWebSocketServer extends WebSocketServer {
firing, weaponIndex, seq, grenadeCharge, grenadeReleased); firing, weaponIndex, seq, grenadeCharge, grenadeReleased);
} }
/** private void broadcastGameState(String roomId, ECSWorld world) {
* 广播游戏状态给房间内所有玩家
*
* @param roomId 房间ID
* @param world 游戏世界
*/
private void broadcastGameState(String roomId, GameWorld world) {
Room room = roomManager.getRoom(roomId); Room room = roomManager.getRoom(roomId);
if (room == null) return; if (room == null) return;
Map<String, Object> state = null; StateSyncSystem syncSystem = stateSyncSystems.computeIfAbsent(roomId, id -> new StateSyncSystem());
synchronized (world.getLock()) { synchronized (world.getLock()) {
for (Player player : room.getPlayers()) { for (PlayerInfo playerInfo : room.getPlayers()) {
state = world.buildGameState(player.getId()); Map<String, Object> state = syncSystem.buildGameState(world, playerInfo.getId());
WebSocket pConn = playerToConnection.get(player.getId()); WebSocket pConn = playerToConnection.get(playerInfo.getId());
if (pConn != null && pConn.isOpen()) { if (pConn != null && pConn.isOpen()) {
sendToConnection(pConn, Constants.MSG_GAME_STATE, state); sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
} }
@@ -333,11 +262,8 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 广播房间状态给房间内所有玩家
*/
private void broadcastRoomState(Room room) { private void broadcastRoomState(Room room) {
for (Player player : room.getPlayers()) { for (PlayerInfo player : room.getPlayers()) {
WebSocket pConn = playerToConnection.get(player.getId()); WebSocket pConn = playerToConnection.get(player.getId());
if (pConn != null && pConn.isOpen()) { if (pConn != null && pConn.isOpen()) {
sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId())); sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId()));
@@ -345,9 +271,6 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 广播房间列表给所有未加入房间的连接
*/
private void broadcastRoomList() { private void broadcastRoomList() {
List<Map<String, Object>> roomList = new ArrayList<>(); List<Map<String, Object>> roomList = new ArrayList<>();
for (Room room : roomManager.getAvailableRooms()) { for (Room room : roomManager.getAvailableRooms()) {
@@ -363,13 +286,6 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 发送消息给指定连接
*
* @param conn WebSocket 连接
* @param type 消息类型
* @param data 消息数据
*/
private void sendToConnection(WebSocket conn, String type, Object data) { private void sendToConnection(WebSocket conn, String type, Object data) {
if (conn != null && conn.isOpen()) { if (conn != null && conn.isOpen()) {
Map<String, Object> msg = new LinkedHashMap<>(); Map<String, Object> msg = new LinkedHashMap<>();
@@ -379,12 +295,6 @@ public class GameWebSocketServer extends WebSocketServer {
} }
} }
/**
* 发送错误消息
*
* @param conn WebSocket 连接
* @param message 错误消息
*/
private void sendError(WebSocket conn, String message) { private void sendError(WebSocket conn, String message) {
Map<String, Object> data = new LinkedHashMap<>(); Map<String, Object> data = new LinkedHashMap<>();
data.put("message", message); data.put("message", message);

View File

@@ -1,6 +1,6 @@
package com.zombie.game.server; package com.zombie.game.server;
import com.zombie.game.model.Player; import com.zombie.game.model.PlayerInfo;
import com.zombie.game.model.Room; import com.zombie.game.model.Room;
import java.util.ArrayList; import java.util.ArrayList;
@@ -29,7 +29,7 @@ public class RoomManager {
*/ */
public void addRoom(String roomId, Room room) { public void addRoom(String roomId, Room room) {
rooms.put(roomId, room); rooms.put(roomId, room);
for (Player player : room.getPlayers()) { for (PlayerInfo player : room.getPlayers()) {
playerToRoom.put(player.getId(), roomId); playerToRoom.put(player.getId(), roomId);
} }
} }
@@ -129,7 +129,7 @@ public class RoomManager {
public void removeRoom(String roomId) { public void removeRoom(String roomId) {
Room room = rooms.remove(roomId); Room room = rooms.remove(roomId);
if (room != null) { if (room != null) {
for (Player player : room.getPlayers()) { for (PlayerInfo player : room.getPlayers()) {
playerToRoom.remove(player.getId()); playerToRoom.remove(player.getId());
} }
} }

View File

@@ -0,0 +1,108 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.*;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 子弹移动系统
*
* 处理所有子弹/投掷物的移动:
* - 标准子弹:直线飞行
* - 手榴弹/燃烧瓶:抛物线飞行
* - 燃烧瓶落地时创建 FireZone
*/
public class BulletMovementSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
GameMap map = world.getMap();
// 更新玩家子弹
updateBullets(dt, world, world.getPlayerBullets(), map, false);
// 更新僵尸子弹
updateBullets(dt, world, world.getZombieBullets(), map, true);
}
private void updateBullets(float dt, ECSWorld world, Set<Integer> bulletSet, GameMap map, boolean isZombieBullet) {
List<Integer> toRemove = new ArrayList<>();
for (int entityId : new ArrayList<>(bulletSet)) {
Position pos = world.getPositions().get(entityId);
Velocity vel = world.getVelocities().get(entityId);
BulletData data = world.getBulletDatas().get(entityId);
if (pos == null || vel == null || data == null) {
toRemove.add(entityId);
continue;
}
if (data.isGrenade() || data.isMolotov()) {
// 抛物线飞行
data.setFlightTime(data.getFlightTime() + dt);
pos.setX(pos.getX() + vel.getVx() * dt);
pos.setY(pos.getY() + vel.getVy() * dt);
float progress = data.getFlightTime() / data.getMaxFlightTime();
float z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
if (data.getFlightTime() >= data.getMaxFlightTime() || (z <= 0.5f && progress > 0.5f)) {
// 落地
if (data.isMolotov()) {
// 燃烧瓶落地后创建火焰区域
FireZone fz = world.getFireZonesData().get(entityId);
if (fz != null) {
world.createFireZoneEntity(pos.getX(), pos.getY(),
fz.getRadius(), fz.getDamagePerTick(), fz.getDuration(), fz.getOwnerId());
}
}
// 触发爆炸(由 CollisionSystem 中的伤害处理)
toRemove.add(entityId);
continue;
}
if (pos.getX() < 0 || pos.getX() >= GRID_SIZE || pos.getY() < 0 || pos.getY() >= GRID_SIZE) {
toRemove.add(entityId);
}
} else {
// 标准直线子弹
float moveX = vel.getVx() * dt;
float moveY = vel.getVy() * dt;
pos.setX(pos.getX() + moveX);
pos.setY(pos.getY() + moveY);
data.setDistanceTraveled(data.getDistanceTraveled() + (float) Math.sqrt(moveX * moveX + moveY * moveY));
boolean remove = false;
if (data.getDistanceTraveled() >= data.getRange()) remove = true;
if (pos.getX() < 0 || pos.getX() >= GRID_SIZE || pos.getY() < 0 || pos.getY() >= GRID_SIZE) remove = true;
if (!remove) {
int gx = (int) Math.floor(pos.getX());
int gy = (int) Math.floor(pos.getY());
Wall wall = map.getWall(gx, gy);
if (wall instanceof StaticWall) remove = true;
}
if (remove) {
toRemove.add(entityId);
}
}
}
for (int id : toRemove) {
if (isZombieBullet) {
world.getRemovedZombieBullets().add(id);
world.getZombieBullets().remove(id);
} else {
world.getRemovedBullets().add(id);
world.getPlayerBullets().remove(id);
}
world.destroyEntity(id);
}
}
}

View File

@@ -0,0 +1,181 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.*;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 碰撞检测系统
*
* 处理子弹与实体、子弹与墙体的碰撞。
*/
public class CollisionSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
checkPlayerBulletCollisions(world);
checkZombieBulletCollisions(world);
}
/**
* 检测玩家子弹与僵尸、坚果墙、机枪塔的碰撞
*/
private void checkPlayerBulletCollisions(ECSWorld world) {
List<Integer> bulletsToRemove = new ArrayList<>();
for (int bulletId : new ArrayList<>(world.getPlayerBullets())) {
BulletData data = world.getBulletDatas().get(bulletId);
Position bulletPos = world.getPositions().get(bulletId);
if (data == null || bulletPos == null) continue;
// 跳过手榴弹和燃烧瓶(落地爆炸,由其他系统处理)
if (data.isGrenade() || data.isMolotov()) continue;
boolean hit = false;
// 检测与僵尸的碰撞
for (int zombieId : new ArrayList<>(world.getZombies())) {
Health zombieHealth = world.getHealths().get(zombieId);
Position zombiePos = world.getPositions().get(zombieId);
if (zombieHealth == null || !zombieHealth.isAlive() || zombiePos == null) continue;
if (hitsEntity(bulletPos, zombiePos, ZOMBIE_SIZE)) {
zombieHealth.takeDamage(data.getDamage());
hit = true;
break;
}
}
// 检测与坚果墙的碰撞
if (!hit) {
int gx = (int) Math.floor(bulletPos.getX());
int gy = (int) Math.floor(bulletPos.getY());
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
hit = true;
if (wall.isDestroyed()) {
world.getMap().removeWall(gx, gy);
}
}
}
// 检测与机枪塔的碰撞
if (!hit) {
for (int turretId : new ArrayList<>(world.getTurrets())) {
WallEntity we = world.getWallEntityDatas().get(turretId);
Health turretHealth = world.getHealths().get(turretId);
Position turretPos = world.getPositions().get(turretId);
if (we == null || turretHealth == null || !turretHealth.isAlive() || turretPos == null) continue;
if (hitsEntity(bulletPos, turretPos, ZOMBIE_SIZE)) {
turretHealth.takeDamage(data.getDamage());
hit = true;
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(turretId);
}
break;
}
}
}
if (hit) {
bulletsToRemove.add(bulletId);
}
}
for (int id : bulletsToRemove) {
world.getPlayerBullets().remove(id);
world.getRemovedBullets().add(id);
world.destroyEntity(id);
}
}
/**
* 检测僵尸子弹与玩家、坚果墙、机枪塔的碰撞
*/
private void checkZombieBulletCollisions(ECSWorld world) {
List<Integer> bulletsToRemove = new ArrayList<>();
for (int bulletId : new ArrayList<>(world.getZombieBullets())) {
Position bulletPos = world.getPositions().get(bulletId);
BulletData data = world.getBulletDatas().get(bulletId);
if (bulletPos == null || data == null) continue;
boolean hit = false;
// 检测与玩家的碰撞
for (int playerId : world.getPlayers()) {
Health playerHealth = world.getHealths().get(playerId);
Position playerPos = world.getPositions().get(playerId);
if (playerHealth == null || !playerHealth.isAlive() || playerPos == null) continue;
if (hitsEntity(bulletPos, playerPos, PLAYER_SIZE)) {
playerHealth.takeDamage(data.getDamage());
hit = true;
break;
}
}
// 检测与坚果墙的碰撞
if (!hit) {
int gx = (int) Math.floor(bulletPos.getX());
int gy = (int) Math.floor(bulletPos.getY());
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
hit = true;
if (wall.isDestroyed()) {
world.getMap().removeWall(gx, gy);
}
}
}
// 检测与机枪塔的碰撞
if (!hit) {
for (int turretId : new ArrayList<>(world.getTurrets())) {
WallEntity we = world.getWallEntityDatas().get(turretId);
Health turretHealth = world.getHealths().get(turretId);
Position turretPos = world.getPositions().get(turretId);
if (we == null || turretHealth == null || !turretHealth.isAlive() || turretPos == null) continue;
if (hitsEntity(bulletPos, turretPos, ZOMBIE_SIZE)) {
turretHealth.takeDamage(data.getDamage());
hit = true;
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(turretId);
}
break;
}
}
}
if (hit) {
bulletsToRemove.add(bulletId);
}
}
for (int id : bulletsToRemove) {
world.getZombieBullets().remove(id);
world.getRemovedZombieBullets().add(id);
world.destroyEntity(id);
}
}
/**
* 判断子弹是否命中实体(基于距离检测)
*/
private boolean hitsEntity(Position bulletPos, Position entityPos, float entitySize) {
float dx = bulletPos.getX() - entityPos.getX();
float dy = bulletPos.getY() - entityPos.getY();
float dist = (float) Math.sqrt(dx * dx + dy * dy);
return dist < entitySize / 2 + 0.1f;
}
}

View File

@@ -0,0 +1,93 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.template.ZombieTemplate;
/**
* 伤害系统
*
* 处理僵尸近战攻击玩家。
*/
public class DamageSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
long now = java.lang.System.currentTimeMillis();
for (int zombieId : world.getZombies()) {
Health zombieHealth = world.getHealths().get(zombieId);
Position zombiePos = world.getPositions().get(zombieId);
ZombieAI ai = world.getZombieAIs().get(zombieId);
if (zombieHealth == null || !zombieHealth.isAlive() || zombiePos == null || ai == null) continue;
ZombieTemplate template = world.getZombieTemplate(ai.getTemplateId());
for (int playerId : world.getPlayers()) {
Health playerHealth = world.getHealths().get(playerId);
Position playerPos = world.getPositions().get(playerId);
if (playerHealth == null || !playerHealth.isAlive() || playerPos == null) continue;
float dist = zombiePos.distanceTo(playerPos.getX(), playerPos.getY());
if (dist < 1.0f && now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) {
playerHealth.takeDamage(template.getDamage());
ai.setLastAttackTime(now);
// 玩家死亡时启动重生计时器
if (!playerHealth.isAlive()) {
RespawnState respawn = world.getRespawnStates().get(playerId);
if (respawn != null) {
respawn.startRespawnTimer(
com.zombie.game.template.TemplateManager.getInstance().getPlayerTemplate().getRespawnTime()
);
}
}
}
}
}
// 精英僵尸远程攻击
for (int zombieId : world.getZombies()) {
Health zombieHealth = world.getHealths().get(zombieId);
Position zombiePos = world.getPositions().get(zombieId);
ZombieAI ai = world.getZombieAIs().get(zombieId);
if (zombieHealth == null || !zombieHealth.isAlive() || zombiePos == null || ai == null) continue;
ZombieTemplate template = world.getZombieTemplate(ai.getTemplateId());
if (!template.isCanRangedAttack()) continue;
// 查找最近的玩家
int nearestPlayerId = -1;
float minDist = Float.MAX_VALUE;
for (int playerId : world.getPlayers()) {
Health playerHealth = world.getHealths().get(playerId);
Position playerPos = world.getPositions().get(playerId);
if (playerHealth == null || !playerHealth.isAlive() || playerPos == null) continue;
float dist = zombiePos.distanceTo(playerPos.getX(), playerPos.getY());
if (dist < minDist) {
minDist = dist;
nearestPlayerId = playerId;
}
}
if (nearestPlayerId >= 0 && minDist <= template.getRangedRange() &&
now - ai.getLastRangedAttackTime() >= template.getAttackRate() * 1000) {
Position targetPos = world.getPositions().get(nearestPlayerId);
float dx = targetPos.getX() - zombiePos.getX();
float dy = targetPos.getY() - zombiePos.getY();
float angle = (float) Math.atan2(dx, dy);
float startX = zombiePos.getX() + (float) Math.sin(angle) * 0.5f;
float startY = zombiePos.getY() + (float) Math.cos(angle) * 0.5f;
world.createBulletEntity(startX, startY, angle, template.getRangedBulletSpeed(),
template.getRangedDamage(), "zombie_" + zombieId, -1, 15, true);
ai.setLastRangedAttackTime(now);
}
}
}
}

View File

@@ -0,0 +1,86 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 难度递增 + 僵尸生成系统
*/
public class DifficultySystem implements System {
private static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
private static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.2f;
private static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
private float spawnTimer;
private float difficultyTimer;
private float spawnInterval;
private final Random random = new Random();
public DifficultySystem() {
this.spawnTimer = 0;
this.difficultyTimer = 0;
this.spawnInterval = ZOMBIE_SPAWN_INTERVAL_BASE;
}
@Override
public void update(float dt, ECSWorld world) {
// 检查是否有存活的玩家
boolean hasAlivePlayer = false;
for (int playerId : world.getPlayers()) {
com.zombie.game.ecs.components.Health h = world.getHealths().get(playerId);
if (h != null && h.isAlive()) {
hasAlivePlayer = true;
break;
}
}
if (!hasAlivePlayer) return;
difficultyTimer += dt;
spawnTimer += dt;
if (difficultyTimer >= DIFFICULTY_INCREASE_INTERVAL) {
difficultyTimer -= DIFFICULTY_INCREASE_INTERVAL;
world.setWaveNumber(world.getWaveNumber() + 1);
spawnInterval = Math.max(ZOMBIE_SPAWN_INTERVAL_MIN, spawnInterval - 0.3f);
}
if (spawnTimer >= spawnInterval) {
spawnTimer -= spawnInterval;
spawnZombie(world);
}
}
/**
* 在随机出生点生成僵尸,根据权重选择类型(普通/精英/分裂)
*/
private void spawnZombie(ECSWorld world) {
List<int[]> spawnPoints = world.getMap().getZombieSpawnPoints();
if (spawnPoints.isEmpty()) return;
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
ZombieTemplate elite = TemplateManager.getInstance().getZombieTemplate("elite");
ZombieTemplate splitter = TemplateManager.getInstance().getZombieTemplate("splitter");
float eliteChance = elite.getSpawnWeight();
float splitterChance = splitter.getSpawnWeight();
float roll = random.nextFloat();
String templateId;
if (roll < eliteChance) {
templateId = "elite";
} else if (roll < eliteChance + splitterChance) {
templateId = "splitter";
} else {
templateId = "normal";
}
world.createZombieEntity(wx, wy, templateId);
}
}

View File

@@ -0,0 +1,49 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import java.util.*;
/**
* 燃烧区域系统
*
* 处理燃烧瓶留下的火焰区域:持续伤害 + 生命周期。
*/
public class FireZoneSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
List<Integer> toRemove = new ArrayList<>();
for (int entityId : world.getFireZones()) {
FireZone fz = world.getFireZonesData().get(entityId);
Position fzPos = world.getPositions().get(entityId);
if (fz == null || fzPos == null) continue;
fz.update(dt);
// 对范围内的僵尸造成持续伤害
for (int zombieId : world.getZombies()) {
Health zombieHealth = world.getHealths().get(zombieId);
Position zombiePos = world.getPositions().get(zombieId);
if (zombieHealth == null || !zombieHealth.isAlive() || zombiePos == null) continue;
float dist = fzPos.distanceTo(zombiePos.getX(), zombiePos.getY());
if (dist < fz.getRadius()) {
zombieHealth.takeDamage(fz.getDamagePerTick());
}
}
if (fz.isExpired()) {
toRemove.add(entityId);
}
}
for (int id : toRemove) {
world.getFireZones().remove(id);
world.destroyEntity(id);
}
}
}

View File

@@ -0,0 +1,54 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 掉落物系统
*
* 处理掉落物的收集逻辑。
*/
public class LootSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
List<Integer> toRemove = new ArrayList<>();
for (int lootId : world.getLoots()) {
Position lootPos = world.getPositions().get(lootId);
LootData lootData = world.getLootDatas().get(lootId);
if (lootPos == null || lootData == null) continue;
for (int playerId : world.getPlayers()) {
Health playerHealth = world.getHealths().get(playerId);
Position playerPos = world.getPositions().get(playerId);
if (playerHealth == null || !playerHealth.isAlive() || playerPos == null) continue;
float dx = playerPos.getX() - lootPos.getX();
float dy = playerPos.getY() - lootPos.getY();
if (Math.sqrt(dx * dx + dy * dy) < 0.8f) {
if (LOOT_TYPE_HEALTH.equals(lootData.getType())) {
playerHealth.heal(LOOT_HEALTH_AMOUNT);
} else {
WeaponState weapon = world.getWeaponStates().get(playerId);
if (weapon != null) {
weapon.refillRandomWeapon();
}
}
toRemove.add(lootId);
break;
}
}
}
for (int id : toRemove) {
world.getLoots().remove(id);
world.destroyEntity(id);
}
}
}

View File

@@ -0,0 +1,52 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.template.TemplateManager;
import static com.zombie.game.model.Constants.*;
/**
* 玩家输入处理系统
*
* 读取 PlayerInput 组件,应用到 Position 和 WeaponState。
*/
public class PlayerInputSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
for (int entityId : world.getPlayers()) {
PlayerInput input = world.getPlayerInputs().get(entityId);
Position pos = world.getPositions().get(entityId);
Health health = world.getHealths().get(entityId);
WeaponState weapon = world.getWeaponStates().get(entityId);
if (input == null || pos == null || health == null || !health.isAlive()) continue;
if (input.getWeaponIndex() >= 0 && input.getWeaponIndex() < weapon.getWeaponCount()) {
weapon.setWeaponIndex(input.getWeaponIndex());
}
// 应用移动
float speed = TemplateManager.getInstance().getPlayerTemplate().getSpeed() * TICK_INTERVAL;
float newX = pos.getX() + input.getDx() * speed;
float newY = pos.getY() + input.getDy() * speed;
// 碰撞检测后更新位置,限制在地图边界内
if (world.getMap().isWalkable(newX, pos.getY(),
TemplateManager.getInstance().getPlayerTemplate().getSize())) {
pos.setX(Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newX)));
}
if (world.getMap().isWalkable(pos.getX(), newY,
TemplateManager.getInstance().getPlayerTemplate().getSize())) {
pos.setY(Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newY)));
}
// 更新朝向角度(面向鼠标方向)
pos.setAngle((float) Math.atan2(input.getAimX() - pos.getX(), input.getAimY() - pos.getY()));
weapon.setLastProcessedSeq(input.getSeq());
}
}
}

View File

@@ -0,0 +1,52 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import java.util.*;
/**
* 玩家重生系统
*
* 处理死亡玩家的重生逻辑。
*/
public class RespawnSystem implements System {
private final Random random = new Random();
@Override
public void update(float dt, ECSWorld world) {
boolean hasAlivePlayer = false;
for (int playerId : world.getPlayers()) {
Health health = world.getHealths().get(playerId);
if (health != null && health.isAlive()) {
hasAlivePlayer = true;
break;
}
}
for (int playerId : world.getPlayers()) {
RespawnState respawn = world.getRespawnStates().get(playerId);
if (respawn == null || !respawn.isWaitingForRespawn()) continue;
respawn.updateTimer(dt);
if (hasAlivePlayer && respawn.canRespawn()) {
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
if (spawnPoints.isEmpty()) continue;
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
Position pos = world.getPositions().get(playerId);
Health health = world.getHealths().get(playerId);
if (pos != null && health != null) {
pos.setX(wx);
pos.setY(wy);
health.reset();
respawn.reset();
}
}
}
}
}

View File

@@ -0,0 +1,204 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.template.TemplateManager;
import java.util.*;
/**
* 状态同步系统
*
* 构建游戏状态用于网络传输。不修改任何组件状态。
*/
public class StateSyncSystem implements System {
/**
* 构建指定玩家视角的游戏状态,用于网络同步
* @param world ECS世界
* @param forPlayerId 请求玩家ID用于获取弹药信息
* @return 游戏状态Map将被序列化为JSON发送给客户端
*/
public Map<String, Object> buildGameState(ECSWorld world, String forPlayerId) {
Map<String, Object> state = new LinkedHashMap<>();
// 玩家状态
List<Map<String, Object>> playerStates = new ArrayList<>();
for (int entityId : world.getPlayers()) {
playerStates.add(buildPlayerState(world, entityId));
}
state.put("players", playerStates);
// 僵尸状态
List<Map<String, Object>> zombieStates = new ArrayList<>();
for (int entityId : world.getZombies()) {
zombieStates.add(buildZombieState(world, entityId));
}
state.put("zombies", zombieStates);
// 玩家子弹状态
List<Map<String, Object>> bulletStates = new ArrayList<>();
for (int entityId : world.getPlayerBullets()) {
bulletStates.add(buildBulletState(world, entityId));
}
state.put("bullets", bulletStates);
// 僵尸子弹状态
List<Map<String, Object>> zombieBulletStates = new ArrayList<>();
for (int entityId : world.getZombieBullets()) {
zombieBulletStates.add(buildBulletState(world, entityId));
}
state.put("zombieBullets", zombieBulletStates);
// 掉落物状态
List<Map<String, Object>> lootStates = new ArrayList<>();
for (int entityId : world.getLoots()) {
lootStates.add(buildLootState(world, entityId));
}
state.put("loots", lootStates);
// 爆炸效果、移除子弹、坚果墙状态、游戏信息
state.put("explosions", new ArrayList<>(world.getExplosions()));
state.put("removedBullets", new ArrayList<>(world.getRemovedBullets()));
state.put("nutWalls", world.getMap().getNutWallStates());
state.put("gameTime", world.getGameTime());
state.put("waveNumber", world.getWaveNumber());
state.put("score", world.getScore());
// 请求玩家的弹药信息
Integer playerEntityId = world.getPlayerEntity(forPlayerId);
if (playerEntityId != null) {
WeaponState weapon = world.getWeaponStates().get(playerEntityId);
if (weapon != null) {
Map<String, Object> ammoMap = new LinkedHashMap<>();
int weaponCount = weapon.getWeaponCount();
for (int i = 0; i < weaponCount; i++) {
String weaponId = TemplateManager.getInstance().getWeaponId(i);
float ammo = weapon.getAmmo()[i];
ammoMap.put(weaponId, ammo == Integer.MAX_VALUE ? -1 : (int) ammo);
}
state.put("ammo", ammoMap);
}
}
return state;
}
/** 构建单个玩家的状态数据 */
private Map<String, Object> buildPlayerState(ECSWorld world, int entityId) {
Map<String, Object> map = new LinkedHashMap<>();
Position pos = world.getPositions().get(entityId);
Health health = world.getHealths().get(entityId);
WeaponState weapon = world.getWeaponStates().get(entityId);
RespawnState respawn = world.getRespawnStates().get(entityId);
// 查找玩家ID
String playerId = null;
for (var entry : world.getPlayerIdToEntity().entrySet()) {
if (entry.getValue() == entityId) {
playerId = entry.getKey();
break;
}
}
map.put("id", playerId);
if (pos != null) {
map.put("x", pos.getX());
map.put("y", pos.getY());
map.put("angle", pos.getAngle());
}
if (health != null) {
map.put("health", health.getHealth());
}
if (weapon != null) {
map.put("weaponIndex", weapon.getWeaponIndex());
map.put("lastProcessedSeq", weapon.getLastProcessedSeq());
}
if (respawn != null) {
map.put("waitingForRespawn", respawn.isWaitingForRespawn());
map.put("respawnTimer", respawn.getRespawnTimer());
}
return map;
}
/** 构建单个僵尸的状态数据 */
private Map<String, Object> buildZombieState(ECSWorld world, int entityId) {
Map<String, Object> map = new LinkedHashMap<>();
Position pos = world.getPositions().get(entityId);
Health health = world.getHealths().get(entityId);
ZombieAI ai = world.getZombieAIs().get(entityId);
map.put("id", entityId);
if (pos != null) {
map.put("x", pos.getX());
map.put("y", pos.getY());
map.put("angle", pos.getAngle());
}
if (health != null) {
map.put("health", health.getHealth());
}
if (ai != null) {
var template = world.getZombieTemplate(ai.getTemplateId());
map.put("isElite", template.isCanRangedAttack());
map.put("isSplitter", template.isCanSplit());
}
return map;
}
/** 构建单个子弹的状态数据 */
private Map<String, Object> buildBulletState(ECSWorld world, int entityId) {
Map<String, Object> map = new LinkedHashMap<>();
Position pos = world.getPositions().get(entityId);
Velocity vel = world.getVelocities().get(entityId);
BulletData data = world.getBulletDatas().get(entityId);
map.put("id", entityId);
if (pos != null) {
map.put("x", pos.getX());
map.put("y", pos.getY());
// 计算投掷物的Z轴高度抛物线
if (data != null && (data.isGrenade() || data.isMolotov())) {
float progress = data.getFlightTime() / data.getMaxFlightTime();
float z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
map.put("z", z);
} else {
map.put("z", 0.5f);
}
}
if (vel != null) {
map.put("angle", (float) Math.atan2(vel.getVx(), vel.getVy()));
}
if (data != null) {
map.put("weaponIndex", data.getWeaponIndex());
map.put("ownerId", data.getOwnerId());
if (data.isGrenade() || data.isMolotov()) {
map.put("targetX", data.getTargetX());
map.put("targetY", data.getTargetY());
}
}
return map;
}
/** 构建单个掉落物的状态数据 */
private Map<String, Object> buildLootState(ECSWorld world, int entityId) {
Map<String, Object> map = new LinkedHashMap<>();
Position pos = world.getPositions().get(entityId);
LootData data = world.getLootDatas().get(entityId);
map.put("id", entityId);
if (pos != null) {
map.put("x", pos.getX());
map.put("y", pos.getY());
}
if (data != null) {
map.put("type", data.getType());
}
return map;
}
@Override
public void update(float dt, ECSWorld world) {
// 空实现:本系统通过 buildGameState() 显式调用
}
}

View File

@@ -0,0 +1,60 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
/**
* 自动机枪塔系统
*
* 处理机枪塔的自动射击逻辑。
*/
public class TurretSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
long now = java.lang.System.currentTimeMillis();
for (int turretId : world.getTurrets()) {
TurretState turret = world.getTurretStates().get(turretId);
Health health = world.getHealths().get(turretId);
Position turretPos = world.getPositions().get(turretId);
if (turret == null || health == null || !health.isAlive() || turretPos == null) continue;
// 跳过坚果墙fireRange == 0 表示纯墙体,无射击行为)
if (turret.getFireRange() <= 0) continue;
if (!turret.canFire(now)) continue;
// 查找范围内最近的僵尸
int nearestZombieId = -1;
float minDist = Float.MAX_VALUE;
for (int zombieId : world.getZombies()) {
Health zombieHealth = world.getHealths().get(zombieId);
Position zombiePos = world.getPositions().get(zombieId);
if (zombieHealth == null || !zombieHealth.isAlive() || zombiePos == null) continue;
float dist = turretPos.distanceTo(zombiePos.getX(), zombiePos.getY());
if (dist <= turret.getFireRange() && dist < minDist) {
minDist = dist;
nearestZombieId = zombieId;
}
}
if (nearestZombieId >= 0) {
Position targetPos = world.getPositions().get(nearestZombieId);
float dx = targetPos.getX() - turretPos.getX();
float dy = targetPos.getY() - turretPos.getY();
float angle = (float) Math.atan2(dx, dy);
float startX = turretPos.getX() + (float) Math.sin(angle) * 0.5f;
float startY = turretPos.getY() + (float) Math.cos(angle) * 0.5f;
world.createBulletEntity(startX, startY, angle, turret.getBulletSpeed(),
turret.getDamage(), "turret_" + turretId, -1, turret.getFireRange(), false);
turret.setLastFireTime(now);
}
}
}
}

View File

@@ -0,0 +1,173 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.NutWall;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.WeaponTemplate;
import java.util.Random;
import static com.zombie.game.model.Constants.*;
/**
* 武器开火系统
*
* 处理所有武器类型的开火逻辑:
* - 枪械类:创建子弹
* - 投掷类:手榴弹/燃烧瓶
* - 放置类:坚果墙/机枪塔
*/
public class WeaponFiringSystem implements System {
private final Random random = new Random();
@Override
public void update(float dt, ECSWorld world) {
long now = java.lang.System.currentTimeMillis();
for (int entityId : world.getPlayers()) {
Health health = world.getHealths().get(entityId);
if (health == null || !health.isAlive()) continue;
PlayerInput input = world.getPlayerInputs().get(entityId);
WeaponState weapon = world.getWeaponStates().get(entityId);
Position pos = world.getPositions().get(entityId);
if (input == null || weapon == null || pos == null) continue;
String playerId = getPlayerId(world, entityId);
if (playerId == null) continue;
WeaponTemplate currentWeapon = weapon.getCurrentWeapon();
String category = currentWeapon.getCategory();
if ("thrown".equals(category)) {
handleThrownWeapon(entityId, playerId, input, weapon, pos, currentWeapon, now, world);
} else if ("placed".equals(category)) {
handlePlacedWeapon(entityId, playerId, input, weapon, pos, currentWeapon, now, world);
} else {
handleFirearmWeapon(entityId, playerId, input, weapon, pos, currentWeapon, now, world);
}
}
}
/**
* 处理枪械类武器开火(手枪、机枪、散弹枪)
* 创建子弹实体,支持散布和多弹丸
*/
private void handleFirearmWeapon(int entityId, String playerId, PlayerInput input, WeaponState weapon,
Position pos, WeaponTemplate currentWeapon, long now, ECSWorld world) {
if (!input.isFiring() || !weapon.canFire(now) || !weapon.hasAmmo()) return;
pos.setAngle((float) Math.atan2(input.getAimX() - pos.getX(), input.getAimY() - pos.getY()));
weapon.fire(now);
int pellets = currentWeapon.getPelletCount();
float spread = currentWeapon.getSpread();
float range = currentWeapon.getRange();
for (int i = 0; i < pellets; i++) {
float angle = pos.getAngle();
if (spread > 0) {
angle += (random.nextFloat() - 0.5f) * spread * 2;
}
float startX = pos.getX() + (float) Math.sin(angle) * 0.5f;
float startY = pos.getY() + (float) Math.cos(angle) * 0.5f;
world.createBulletEntity(startX, startY, angle, currentWeapon.getBulletSpeed(),
currentWeapon.getDamage(), playerId, weapon.getWeaponIndex(), range, false);
}
}
/**
* 处理投掷类武器(手榴弹、燃烧瓶)
* 蓄力后释放,根据蓄力百分比决定投掷距离
*/
private void handleThrownWeapon(int entityId, String playerId, PlayerInput input, WeaponState weapon,
Position pos, WeaponTemplate currentWeapon, long now, ECSWorld world) {
if (input.isGrenadeReleased() && weapon.hasAmmo()) {
float chargePercent = weapon.getGrenadeChargePercent();
weapon.stopGrenadeCharge();
weapon.fire(now);
float startX = pos.getX();
float startY = pos.getY();
float minDist = 3.0f;
float maxDist = 15.0f;
float dist = minDist + (maxDist - minDist) * chargePercent;
float dx = input.getAimX() - startX;
float dy = input.getAimY() - startY;
float targetDist = (float) Math.sqrt(dx * dx + dy * dy);
float targetX, targetY;
if (targetDist < 0.1f) {
targetX = startX + minDist;
targetY = startY;
} else {
float scale = Math.min(dist, targetDist) / targetDist;
targetX = startX + dx * scale;
targetY = startY + dy * scale;
}
targetX = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetX));
targetY = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetY));
float flightDuration = 0.8f + chargePercent * 0.7f;
if ("molotov".equals(currentWeapon.getId())) {
world.createMolotovEntity(startX, startY, targetX, targetY, flightDuration,
currentWeapon.getDamage(), playerId, currentWeapon.getExplosionRadius(),
currentWeapon.getFireZoneRadius(), currentWeapon.getFireZoneDamage(),
currentWeapon.getFireZoneDuration());
} else {
world.createGrenadeEntity(startX, startY, targetX, targetY, flightDuration,
currentWeapon.getDamage(), playerId, currentWeapon.getExplosionRadius());
}
} else if (input.isFiring() && !weapon.isChargingGrenade()) {
weapon.startGrenadeCharge();
}
}
/**
* 处理放置类武器(坚果墙、机枪塔)
* 在鼠标指向的空地上放置实体
*/
private void handlePlacedWeapon(int entityId, String playerId, PlayerInput input, WeaponState weapon,
Position pos, WeaponTemplate currentWeapon, long now, ECSWorld world) {
if (!input.isFiring() || !weapon.canFire(now) || !weapon.hasAmmo()) return;
// 计算目标网格位置
float aimX = input.getAimX();
float aimY = input.getAimY();
int gridX = (int) Math.floor(aimX);
int gridY = (int) Math.floor(aimY);
// 检查目标格子是否为空地
if (world.getMap().getWall(gridX, gridY) != null) return;
weapon.fire(now);
if ("nut_wall".equals(currentWeapon.getId())) {
world.getMap().addNutWall(gridX, gridY);
// 创建纯墙体实体fireRange=0无机枪塔行为
world.createTurretEntity(gridX + 0.5f, gridY + 0.5f, gridX, gridY,
0, 0, 0, 0, 20);
} else if ("auto_turret".equals(currentWeapon.getId())) {
world.getMap().addNutWall(gridX, gridY);
world.createTurretEntity(gridX + 0.5f, gridY + 0.5f, gridX, gridY,
currentWeapon.getTurretRange(), currentWeapon.getTurretFireRate(),
currentWeapon.getTurretDamage(), currentWeapon.getTurretBulletSpeed(),
currentWeapon.getTurretHealth());
}
}
/** 根据实体ID查找对应的玩家ID */
private String getPlayerId(ECSWorld world, int entityId) {
for (var entry : world.getPlayerIdToEntity().entrySet()) {
if (entry.getValue() == entityId) return entry.getKey();
}
return null;
}
}

View File

@@ -0,0 +1,63 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.template.ZombieTemplate;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 僵尸死亡系统
*
* 处理僵尸死亡:分裂、计分、掉落物。
*/
public class ZombieDeathSystem implements System {
private static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
private final Random random = new Random();
@Override
public void update(float dt, ECSWorld world) {
List<Integer> deadZombies = new ArrayList<>();
for (int zombieId : world.getZombies()) {
Health health = world.getHealths().get(zombieId);
if (health == null || health.isAlive()) continue;
deadZombies.add(zombieId);
}
for (int zombieId : deadZombies) {
Position pos = world.getPositions().get(zombieId);
ZombieAI ai = world.getZombieAIs().get(zombieId);
if (pos == null || ai == null) continue;
ZombieTemplate template = world.getZombieTemplate(ai.getTemplateId());
// 分裂僵尸:生成子僵尸
if (template.isCanSplit()) {
int splitCount = template.getMinSplit() +
random.nextInt(template.getMaxSplit() - template.getMinSplit() + 1);
for (int i = 0; i < splitCount; i++) {
float offsetX = (random.nextFloat() - 0.5f) * 1.0f;
float offsetY = (random.nextFloat() - 0.5f) * 1.0f;
world.createZombieEntity(pos.getX() + offsetX, pos.getY() + offsetY, "normal");
}
world.addScore(20);
} else {
world.addScore(template.isCanRangedAttack() ? 50 : 10);
}
// 掉落物生成
if (random.nextFloat() < ZOMBIE_LOOT_DROP_CHANCE) {
String lootType = random.nextFloat() < 0.5f ? LOOT_TYPE_AMMO : LOOT_TYPE_HEALTH;
world.createLootEntity(pos.getX(), pos.getY(), lootType);
}
// 移除僵尸实体
world.getZombies().remove(zombieId);
world.destroyEntity(zombieId);
}
}
}

View File

@@ -0,0 +1,315 @@
package com.zombie.game.systems;
import com.zombie.game.ecs.ECSWorld;
import com.zombie.game.ecs.System;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.*;
import com.zombie.game.template.ZombieTemplate;
import java.util.*;
import static com.zombie.game.model.Constants.*;
/**
* 僵尸移动系统
*
* 处理僵尸的流场寻路、移动、墙体攻击、僵尸间分离。
* 从原 Zombie.move() 方法提取。
*/
public class ZombieMovementSystem implements System {
@Override
public void update(float dt, ECSWorld world) {
GameMap map = world.getMap();
if (!map.isFlowFieldValid()) return;
long now = java.lang.System.currentTimeMillis();
// 按ID排序僵尸确保确定性行为
List<Integer> sortedZombies = new ArrayList<>(world.getZombies());
Collections.sort(sortedZombies);
for (int entityId : sortedZombies) {
Position pos = world.getPositions().get(entityId);
Health health = world.getHealths().get(entityId);
ZombieAI ai = world.getZombieAIs().get(entityId);
if (pos == null || health == null || !health.isAlive()) continue;
if (ai == null) continue;
ZombieTemplate template = world.getZombieTemplate(ai.getTemplateId());
// 处理墙体攻击状态
if (ai.isAttackingWall() && ai.getAttackingWallGridX() >= 0) {
float wallCenterX = ai.getAttackingWallGridX() + 0.5f;
float wallCenterY = ai.getAttackingWallGridY() + 0.5f;
float distToWall = pos.distanceTo(wallCenterX, wallCenterY);
if (distToWall < 0.8f) {
Wall wall = map.getWall(ai.getAttackingWallGridX(), ai.getAttackingWallGridY());
if (wall != null && !wall.isDestroyed()) {
// 攻击墙体
if (now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) {
wall.takeDamage(1.0f);
ai.setLastAttackTime(now);
if (wall.isDestroyed()) {
map.removeWall(wall.getGridX(), wall.getGridY());
}
}
continue;
}
ai.setAttackingWall(false);
ai.setAttackingWallGridX(-1);
ai.setAttackingWallGridY(-1);
}
}
// 目标选择
float centerDist = Float.MAX_VALUE;
if (ai.isHasTarget()) {
float dx = ai.getTargetX() - pos.getX();
float dy = ai.getTargetY() - pos.getY();
centerDist = (float) Math.sqrt(dx * dx + dy * dy);
}
if (!ai.isHasTarget() || centerDist < 0.15f) {
float[] flowDir = map.getFlowDirection(pos.getX(), pos.getY());
float dirX = flowDir[0];
float dirY = flowDir[1];
if (dirX == 0 && dirY == 0) continue;
float len = (float) Math.sqrt(dirX * dirX + dirY * dirY);
if (len > 0) {
dirX /= len;
dirY /= len;
}
int currentGridX = (int) Math.floor(pos.getX());
int currentGridY = (int) Math.floor(pos.getY());
int nextGridX = currentGridX + (int) Math.round(dirX);
int nextGridY = currentGridY + (int) Math.round(dirY);
if (map.isNutWall(nextGridX, nextGridY)) {
ai.setAttackingWall(true);
ai.setAttackingWallGridX(nextGridX);
ai.setAttackingWallGridY(nextGridY);
ai.setReservedGridX(nextGridX);
ai.setReservedGridY(nextGridY);
ai.setReservation(true);
ai.setTargetX(nextGridX + 0.5f);
ai.setTargetY(nextGridY + 0.5f);
ai.setHasTarget(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);
nextGridY = currentGridY;
} else if (!map.isWall(currentGridX, currentGridY + (int) Math.signum(dirY))) {
nextGridX = currentGridX;
nextGridY = currentGridY + (int) Math.signum(dirY);
} else {
ai.setReservation(false);
continue;
}
}
if (isGridOccupiedOrReserved(nextGridX, nextGridY, world, entityId)) {
int[] alt = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, world, entityId);
if (alt != null) {
nextGridX = alt[0];
nextGridY = alt[1];
} else {
ai.setReservation(false);
continue;
}
}
ai.setReservedGridX(nextGridX);
ai.setReservedGridY(nextGridY);
ai.setReservation(true);
ai.setTargetX(nextGridX + 0.5f);
ai.setTargetY(nextGridY + 0.5f);
ai.setHasTarget(true);
} else {
if (isGridOccupiedOrReserved(nextGridX, nextGridY, world, entityId)) {
int[] alt = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, world, entityId);
if (alt != null) {
nextGridX = alt[0];
nextGridY = alt[1];
} else {
ai.setReservation(false);
continue;
}
}
ai.setReservedGridX(nextGridX);
ai.setReservedGridY(nextGridY);
ai.setReservation(true);
ai.setTargetX(nextGridX + 0.5f);
ai.setTargetY(nextGridY + 0.5f);
ai.setHasTarget(true);
}
}
// 向目标移动
float dx = ai.getTargetX() - pos.getX();
float dy = ai.getTargetY() - pos.getY();
float dist = (float) Math.sqrt(dx * dx + dy * dy);
if (dist < 0.01f) {
ai.setHasTarget(false);
continue;
}
float dirX = dx / dist;
float dirY = dy / dist;
float speed = template.getBaseSpeed();
float moveX = dirX * speed * dt;
float moveY = dirY * speed * dt;
float newX = pos.getX() + moveX;
float newY = pos.getY() + moveY;
boolean canMoveX = map.isWalkable(newX, pos.getY(), ZOMBIE_SIZE);
boolean canMoveY = map.isWalkable(pos.getX(), newY, ZOMBIE_SIZE);
boolean canMoveDiagonal = map.isWalkable(newX, newY, ZOMBIE_SIZE);
if (moveX != 0 && moveY != 0) {
int checkX = (int) Math.floor(newX);
int checkY = (int) Math.floor(newY);
int checkCurrentX = (int) Math.floor(pos.getX());
int checkCurrentY = (int) Math.floor(pos.getY());
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) {
pos.setX(newX);
canMoveY = map.isWalkable(pos.getX(), newY, ZOMBIE_SIZE);
} else if (!wallInY && canMoveY) {
pos.setY(newY);
canMoveX = map.isWalkable(newX, pos.getY(), ZOMBIE_SIZE);
}
}
}
if (!blockedByCorner && canMoveDiagonal) {
pos.setX(newX);
pos.setY(newY);
} else if (!blockedByCorner) {
if (canMoveX) pos.setX(newX);
if (canMoveY) pos.setY(newY);
}
} else {
if (canMoveX) pos.setX(newX);
if (canMoveY) pos.setY(newY);
}
// 僵尸间分离(防止重叠)
for (int otherId : world.getZombies()) {
if (otherId == entityId) continue;
Health otherHealth = world.getHealths().get(otherId);
if (otherHealth == null || !otherHealth.isAlive()) continue;
Position otherPos = world.getPositions().get(otherId);
if (otherPos == null) continue;
float sepDx = pos.getX() - otherPos.getX();
float sepDy = pos.getY() - otherPos.getY();
float sepDist = (float) Math.sqrt(sepDx * sepDx + sepDy * sepDy);
if (sepDist < ZOMBIE_SIZE && sepDist > 0.01f) {
float overlap = ZOMBIE_SIZE - sepDist;
float pushX = (sepDx / sepDist) * overlap * 0.5f;
float pushY = (sepDy / sepDist) * overlap * 0.5f;
float pushedX = pos.getX() + pushX;
float pushedY = pos.getY() + pushY;
if (map.isWalkable(pushedX, pushedY, ZOMBIE_SIZE)) {
pos.setX(pushedX);
pos.setY(pushedY);
} else {
if (map.isWalkable(pos.getX() + pushX, pos.getY(), ZOMBIE_SIZE)) {
pos.setX(pos.getX() + pushX);
}
if (map.isWalkable(pos.getX(), pos.getY() + pushY, ZOMBIE_SIZE)) {
pos.setY(pos.getY() + pushY);
}
}
}
}
// 更新朝向角度
if (dirX != 0 || dirY != 0) {
pos.setAngle((float) Math.atan2(dirX, dirY));
}
}
}
/**
* 检查网格是否被其他僵尸占据或预留
*/
private boolean isGridOccupiedOrReserved(int gridX, int gridY, ECSWorld world, int selfId) {
for (int otherId : world.getZombies()) {
if (otherId == selfId) continue;
Health otherHealth = world.getHealths().get(otherId);
if (otherHealth == null || !otherHealth.isAlive()) continue;
Position otherPos = world.getPositions().get(otherId);
if (otherPos == null) continue;
int otherGridX = (int) Math.floor(otherPos.getX());
int otherGridY = (int) Math.floor(otherPos.getY());
if (otherGridX == gridX && otherGridY == gridY) return true;
ZombieAI otherAi = world.getZombieAIs().get(otherId);
if (otherAi != null && otherAi.isReservation() &&
otherAi.getReservedGridX() == gridX && otherAi.getReservedGridY() == gridY) {
return true;
}
}
return false;
}
/**
* 查找替代移动方向(当前方向被阻挡时)
* 优先选择与原方向点积最大的可用方向
*/
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
GameMap map, ECSWorld world, int selfId) {
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
List<int[]> candidates = new ArrayList<>();
for (int[] dir : allDirs) {
int nx = currentGridX + dir[0];
int ny = currentGridY + dir[1];
if (map.isWall(nx, ny)) continue;
if (dir[0] != 0 && dir[1] != 0) {
if (map.isWall(currentGridX + dir[0], currentGridY) ||
map.isWall(currentGridX, currentGridY + dir[1])) {
continue;
}
}
if (isGridOccupiedOrReserved(nx, ny, world, selfId)) continue;
float dotProduct = dir[0] * dirX + dir[1] * dirY;
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
}
if (candidates.isEmpty()) return null;
candidates.sort((a, b) -> b[2] - a[2]);
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
}
}

View File

@@ -33,9 +33,6 @@ public class TemplateManager {
return instance; return instance;
} }
/**
* 加载所有模板
*/
private void loadAll() { private void loadAll() {
loadZombies(); loadZombies();
loadWeapons(); loadWeapons();
@@ -82,6 +79,7 @@ public class TemplateManager {
WeaponTemplate t = new WeaponTemplate( WeaponTemplate t = new WeaponTemplate(
node.get("id").asText(), node.get("id").asText(),
node.get("name").asText(), node.get("name").asText(),
node.has("category") ? node.get("category").asText() : "firearm",
node.get("damage").asInt(), node.get("damage").asInt(),
node.get("fireRate").asLong(), node.get("fireRate").asLong(),
node.get("pelletCount").asInt(), node.get("pelletCount").asInt(),
@@ -91,7 +89,15 @@ public class TemplateManager {
node.get("maxAmmo").asInt(), node.get("maxAmmo").asInt(),
node.get("chargeable").asBoolean(), node.get("chargeable").asBoolean(),
node.get("explosive").asBoolean(), node.get("explosive").asBoolean(),
(float) node.get("explosionRadius").asDouble() (float) node.get("explosionRadius").asDouble(),
node.has("fireZoneRadius") ? (float) node.get("fireZoneRadius").asDouble() : 0,
node.has("fireZoneDamage") ? (float) node.get("fireZoneDamage").asDouble() : 0,
node.has("fireZoneDuration") ? (float) node.get("fireZoneDuration").asDouble() : 0,
node.has("turretHealth") ? (float) node.get("turretHealth").asDouble() : 0,
node.has("turretRange") ? (float) node.get("turretRange").asDouble() : 0,
node.has("turretFireRate") ? node.get("turretFireRate").asLong() : 0,
node.has("turretDamage") ? node.get("turretDamage").asInt() : 0,
node.has("turretBulletSpeed") ? (float) node.get("turretBulletSpeed").asDouble() : 0
); );
weaponTemplates.put(t.getId(), t); weaponTemplates.put(t.getId(), t);
} }
@@ -137,9 +143,6 @@ public class TemplateManager {
return playerTemplate; return playerTemplate;
} }
/**
* 获取武器列表的索引位置
*/
public int getWeaponIndex(String id) { public int getWeaponIndex(String id) {
int idx = 0; int idx = 0;
for (String key : weaponTemplates.keySet()) { for (String key : weaponTemplates.keySet()) {
@@ -149,9 +152,6 @@ public class TemplateManager {
return 0; return 0;
} }
/**
* 根据索引获取武器ID
*/
public String getWeaponId(int index) { public String getWeaponId(int index) {
int idx = 0; int idx = 0;
for (String key : weaponTemplates.keySet()) { for (String key : weaponTemplates.keySet()) {

View File

@@ -6,11 +6,13 @@ import lombok.Getter;
* 武器类型模板 * 武器类型模板
* *
* 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。 * 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。
* 支持三种类别firearm枪械、thrown投掷、placed放置
*/ */
@Getter @Getter
public class WeaponTemplate { public class WeaponTemplate {
private final String id; private final String id;
private final String name; private final String name;
private final String category; // firearm, thrown, placed
private final int damage; private final int damage;
private final long fireRate; private final long fireRate;
private final int pelletCount; private final int pelletCount;
@@ -22,12 +24,28 @@ public class WeaponTemplate {
private final boolean explosive; private final boolean explosive;
private final float explosionRadius; private final float explosionRadius;
public WeaponTemplate(String id, String name, int damage, long fireRate, // Thrown weapon (molotov) fields
private final float fireZoneRadius;
private final float fireZoneDamage;
private final float fireZoneDuration;
// Placed weapon (turret) fields
private final float turretHealth;
private final float turretRange;
private final long turretFireRate;
private final int turretDamage;
private final float turretBulletSpeed;
public WeaponTemplate(String id, String name, String category, int damage, long fireRate,
int pelletCount, float spread, float bulletSpeed, int pelletCount, float spread, float bulletSpeed,
float range, int maxAmmo, boolean chargeable, float range, int maxAmmo, boolean chargeable,
boolean explosive, float explosionRadius) { boolean explosive, float explosionRadius,
float fireZoneRadius, float fireZoneDamage, float fireZoneDuration,
float turretHealth, float turretRange, long turretFireRate,
int turretDamage, float turretBulletSpeed) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.category = category;
this.damage = damage; this.damage = damage;
this.fireRate = fireRate; this.fireRate = fireRate;
this.pelletCount = pelletCount; this.pelletCount = pelletCount;
@@ -38,5 +56,13 @@ public class WeaponTemplate {
this.chargeable = chargeable; this.chargeable = chargeable;
this.explosive = explosive; this.explosive = explosive;
this.explosionRadius = explosionRadius; this.explosionRadius = explosionRadius;
this.fireZoneRadius = fireZoneRadius;
this.fireZoneDamage = fireZoneDamage;
this.fireZoneDuration = fireZoneDuration;
this.turretHealth = turretHealth;
this.turretRange = turretRange;
this.turretFireRate = turretFireRate;
this.turretDamage = turretDamage;
this.turretBulletSpeed = turretBulletSpeed;
} }
} }

View File

@@ -3,6 +3,7 @@
{ {
"id": "pistol", "id": "pistol",
"name": "手枪", "name": "手枪",
"category": "firearm",
"damage": 50, "damage": 50,
"fireRate": 400, "fireRate": 400,
"pelletCount": 1, "pelletCount": 1,
@@ -12,11 +13,20 @@
"maxAmmo": 2147483647, "maxAmmo": 2147483647,
"chargeable": false, "chargeable": false,
"explosive": false, "explosive": false,
"explosionRadius": 0 "explosionRadius": 0,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 0,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
"turretBulletSpeed": 0
}, },
{ {
"id": "machine_gun", "id": "machine_gun",
"name": "机枪", "name": "机枪",
"category": "firearm",
"damage": 50, "damage": 50,
"fireRate": 100, "fireRate": 100,
"pelletCount": 1, "pelletCount": 1,
@@ -26,11 +36,20 @@
"maxAmmo": 100, "maxAmmo": 100,
"chargeable": false, "chargeable": false,
"explosive": false, "explosive": false,
"explosionRadius": 0 "explosionRadius": 0,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 0,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
"turretBulletSpeed": 0
}, },
{ {
"id": "shotgun", "id": "shotgun",
"name": "弹枪", "name": "弹枪",
"category": "firearm",
"damage": 50, "damage": 50,
"fireRate": 800, "fireRate": 800,
"pelletCount": 10, "pelletCount": 10,
@@ -40,11 +59,20 @@
"maxAmmo": 20, "maxAmmo": 20,
"chargeable": false, "chargeable": false,
"explosive": false, "explosive": false,
"explosionRadius": 0 "explosionRadius": 0,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 0,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
"turretBulletSpeed": 0
}, },
{ {
"id": "grenade", "id": "grenade",
"name": "手榴弹", "name": "手榴弹",
"category": "thrown",
"damage": 120, "damage": 120,
"fireRate": 1500, "fireRate": 1500,
"pelletCount": 1, "pelletCount": 1,
@@ -54,7 +82,84 @@
"maxAmmo": 10, "maxAmmo": 10,
"chargeable": true, "chargeable": true,
"explosive": true, "explosive": true,
"explosionRadius": 3 "explosionRadius": 3,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 0,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
"turretBulletSpeed": 0
},
{
"id": "molotov",
"name": "燃烧瓶",
"category": "thrown",
"damage": 80,
"fireRate": 2000,
"pelletCount": 1,
"spread": 0,
"bulletSpeed": 12,
"range": 12,
"maxAmmo": 5,
"chargeable": true,
"explosive": true,
"explosionRadius": 2.0,
"fireZoneRadius": 2.5,
"fireZoneDamage": 10,
"fireZoneDuration": 3.0,
"turretHealth": 0,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
"turretBulletSpeed": 0
},
{
"id": "nut_wall",
"name": "坚果墙体",
"category": "placed",
"damage": 0,
"fireRate": 1000,
"pelletCount": 0,
"spread": 0,
"bulletSpeed": 0,
"range": 0,
"maxAmmo": 3,
"chargeable": false,
"explosive": false,
"explosionRadius": 0,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 20,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
"turretBulletSpeed": 0
},
{
"id": "auto_turret",
"name": "自动机枪塔",
"category": "placed",
"damage": 0,
"fireRate": 2000,
"pelletCount": 0,
"spread": 0,
"bulletSpeed": 0,
"range": 0,
"maxAmmo": 2,
"chargeable": false,
"explosive": false,
"explosionRadius": 0,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 100,
"turretRange": 6.0,
"turretFireRate": 300,
"turretDamage": 30,
"turretBulletSpeed": 20
} }
] ]
} }

View File

@@ -54,7 +54,10 @@ export class GameEngine {
[WEAPONS.PISTOL]: Infinity, // 手枪无限弹药 [WEAPONS.PISTOL]: Infinity, // 手枪无限弹药
[WEAPONS.MACHINE_GUN]: 100, [WEAPONS.MACHINE_GUN]: 100,
[WEAPONS.SHOTGUN]: 20, [WEAPONS.SHOTGUN]: 20,
[WEAPONS.GRENADE]: 10 [WEAPONS.GRENADE]: 10,
[WEAPONS.MOLOTOV]: 5,
[WEAPONS.NUT_WALL]: 3,
[WEAPONS.AUTO_TURRET]: 2
} }
// 手雷蓄力相关 // 手雷蓄力相关
@@ -217,8 +220,8 @@ export class GameEngine {
const localPlayer = this.players.get(this.localPlayerId) const localPlayer = this.players.get(this.localPlayerId)
if (localPlayer) { if (localPlayer) {
this.scene.updateCamera(localPlayer.x, localPlayer.y) this.scene.updateCamera(localPlayer.x, localPlayer.y)
// 手雷瞄准指示器 // 投掷武器瞄准指示器(手雷/燃烧瓶)
if (this.isChargingGrenade && this.currentWeaponIndex === 3) { if (this.isChargingGrenade && (this.currentWeaponIndex === 3 || this.currentWeaponIndex === 4)) {
this.scene.showGrenadeTarget(localPlayer.x, localPlayer.y, this.scene.showGrenadeTarget(localPlayer.x, localPlayer.y,
this.input.mouse.groundX || 0, this.input.mouse.groundY || 0, this.input.mouse.groundX || 0, this.input.mouse.groundY || 0,
this.grenadeChargePercent) this.grenadeChargePercent)
@@ -260,11 +263,16 @@ export class GameEngine {
// 应用本地预测(客户端预测) // 应用本地预测(客户端预测)
this._applyLocalPrediction(inputState) this._applyLocalPrediction(inputState)
// 添加手雷相关数据到输入状态 // 添加蓄力武器相关数据到输入状态(手雷/燃烧瓶)
inputState.grenadeCharge = this.grenadeChargePercent inputState.grenadeCharge = this.grenadeChargePercent
inputState.grenadeReleased = this.grenadeReleased inputState.grenadeReleased = this.grenadeReleased
// 手雷释放前不发火 // 蓄力武器释放前不发火
inputState.firing = this.currentWeaponIndex === 3 ? false : inputState.firing const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
const currentWeaponId = weaponList7[this.currentWeaponIndex]
const currentConfig = WEAPON_CONFIG[currentWeaponId]
if (currentConfig && currentConfig.chargeable) {
inputState.firing = false
}
// 保存输入以便进行客户端预测校正 // 保存输入以便进行客户端预测校正
this.pendingInputs.push(inputState) this.pendingInputs.push(inputState)
@@ -287,10 +295,11 @@ export class GameEngine {
* 手雷需要长按鼠标蓄力,松开释放 * 手雷需要长按鼠标蓄力,松开释放
*/ */
_handleGrenadeCharge(inputState) { _handleGrenadeCharge(inputState) {
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE] const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
const currentWeapon = weaponList[this.currentWeaponIndex] const currentWeapon = weaponList7[this.currentWeaponIndex]
const config = WEAPON_CONFIG[currentWeapon]
if (currentWeapon === WEAPONS.GRENADE && WEAPON_CONFIG[WEAPONS.GRENADE].chargeable) { if (config && config.chargeable) {
// 开始蓄力 // 开始蓄力
if (inputState.firing && !this.isChargingGrenade) { if (inputState.firing && !this.isChargingGrenade) {
this.isChargingGrenade = true this.isChargingGrenade = true
@@ -298,13 +307,13 @@ export class GameEngine {
} else if (inputState.firing && this.isChargingGrenade) { } else if (inputState.firing && this.isChargingGrenade) {
// 蓄力中,计算蓄力百分比 // 蓄力中,计算蓄力百分比
const elapsed = Date.now() - this.grenadeChargeStart const elapsed = Date.now() - this.grenadeChargeStart
this.grenadeChargePercent = Math.min(1, elapsed / WEAPON_CONFIG[WEAPONS.GRENADE].maxCharge) this.grenadeChargePercent = Math.min(1, elapsed / config.maxCharge)
} else if (!inputState.firing && this.isChargingGrenade) { } else if (!inputState.firing && this.isChargingGrenade) {
// 释放手雷 // 释放
this.grenadeReleased = true this.grenadeReleased = true
this.isChargingGrenade = false this.isChargingGrenade = false
const elapsed = Date.now() - this.grenadeChargeStart const elapsed = Date.now() - this.grenadeChargeStart
this.grenadeChargePercent = Math.min(1, elapsed / WEAPON_CONFIG[WEAPONS.GRENADE].maxCharge) this.grenadeChargePercent = Math.min(1, elapsed / config.maxCharge)
} }
} else { } else {
this.isChargingGrenade = false this.isChargingGrenade = false
@@ -417,9 +426,9 @@ export class GameEngine {
// 新增子弹 // 新增子弹
this.bullets.set(bs.id, { ...bs }) this.bullets.set(bs.id, { ...bs })
this.scene.addBullet(bs) this.scene.addBullet(bs)
// 枪口火焰特效(手雷除外) // 枪口火焰特效(投掷类武器除外)
const player = this.players.get(bs.ownerId) const player = this.players.get(bs.ownerId)
if (player && bs.weapon !== WEAPONS.GRENADE) { if (player && bs.weaponIndex !== 3 && bs.weaponIndex !== 4) {
this.scene.addMuzzleFlash(player.x, player.y, bs.angle || 0) this.scene.addMuzzleFlash(player.x, player.y, bs.angle || 0)
} }
} else { } else {

View File

@@ -556,22 +556,30 @@ export class GameScene {
*/ */
addBullet(bullet) { addBullet(bullet) {
const group = new THREE.Group() const group = new THREE.Group()
const isGrenade = bullet.weapon === WEAPONS.GRENADE // weaponIndex: 0=pistol, 1=machinegun, 2=shotgun, 3=grenade, 4=molotov
const wIdx = bullet.weaponIndex
const isGrenade = wIdx === 3
const isMolotov = wIdx === 4
const isThrown = isGrenade || isMolotov
const z = bullet.z || 0.5 const z = bullet.z || 0.5
if (isGrenade) { if (isThrown) {
// 手雷:球体 + 发光 + 拖尾 // 投掷物:球体 + 发光 + 拖尾
const bodyColor = isMolotov ? 0xff6600 : 0x44ff44
const glowColor = isMolotov ? 0xff4400 : 0x22cc22
const trailColor = isMolotov ? 0xff8844 : 0x88ff88
const geo = new THREE.SphereGeometry(0.12, 8, 8) const geo = new THREE.SphereGeometry(0.12, 8, 8)
const mat = new THREE.MeshBasicMaterial({ color: 0x44ff44 }) const mat = new THREE.MeshBasicMaterial({ color: bodyColor })
const mesh = new THREE.Mesh(geo, mat) const mesh = new THREE.Mesh(geo, mat)
mesh.position.set(bullet.x, z, bullet.y) mesh.position.set(bullet.x, z, bullet.y)
group.add(mesh) group.add(mesh)
const glowGeo = new THREE.SphereGeometry(0.18, 8, 8) const glowGeo = new THREE.SphereGeometry(0.18, 8, 8)
const glowMat = new THREE.MeshBasicMaterial({ const glowMat = new THREE.MeshBasicMaterial({
color: 0x22cc22, color: glowColor,
transparent: true, transparent: true,
opacity: 0.4 opacity: 0.5
}) })
const glow = new THREE.Mesh(glowGeo, glowMat) const glow = new THREE.Mesh(glowGeo, glowMat)
glow.position.set(bullet.x, z, bullet.y) glow.position.set(bullet.x, z, bullet.y)
@@ -580,7 +588,7 @@ export class GameScene {
// 拖尾线 // 拖尾线
const trailGeo = new THREE.BufferGeometry() const trailGeo = new THREE.BufferGeometry()
const trailMat = new THREE.LineBasicMaterial({ const trailMat = new THREE.LineBasicMaterial({
color: 0x88ff88, color: trailColor,
transparent: true, transparent: true,
opacity: 0.6 opacity: 0.6
}) })
@@ -598,14 +606,8 @@ export class GameScene {
const angle = bullet.angle || 0 const angle = bullet.angle || 0
// 根据武器类型调整大小 // 根据武器类型调整大小
switch (bullet.weapon) { if (wIdx === 1) bulletSize = 0.05 // 机枪
case WEAPONS.MACHINE_GUN: else if (wIdx === 2) bulletSize = 0.04 // 霰弹枪
bulletSize = 0.05
break
case WEAPONS.SHOTGUN:
bulletSize = 0.04
break
}
// 拖尾 // 拖尾
const trailLength = 2.5 const trailLength = 2.5
@@ -651,9 +653,10 @@ export class GameScene {
x: bullet.x, x: bullet.x,
y: bullet.y, y: bullet.y,
z: z, z: z,
weapon: bullet.weapon, weaponIndex: wIdx,
angle: bullet.angle || 0, angle: bullet.angle || 0,
isGrenade isGrenade,
isMolotov
}) })
} }
@@ -695,7 +698,7 @@ export class GameScene {
child.position.z = y child.position.z = y
} }
// 更新拖尾位置 // 更新拖尾位置
if (child.isLine && !bullet.isGrenade) { if (child.isLine && !bullet.isGrenade && !bullet.isMolotov) {
const trailLength = 2.5 const trailLength = 2.5
const positions = child.geometry.attributes.position.array const positions = child.geometry.attributes.position.array
positions[0] = x - Math.sin(angle) * trailLength positions[0] = x - Math.sin(angle) * trailLength
@@ -1083,7 +1086,7 @@ export class GameScene {
// 更新子弹拖尾 // 更新子弹拖尾
for (const bullet of this.bullets) { for (const bullet of this.bullets) {
if (!bullet.isGrenade && bullet.trail) { if (!bullet.isGrenade && !bullet.isMolotov && bullet.trail) {
const positions = bullet.trail.geometry.attributes.position.array const positions = bullet.trail.geometry.attributes.position.array
positions[0] = bullet.x - Math.sin(bullet.angle) * 1.5 positions[0] = bullet.x - Math.sin(bullet.angle) * 1.5
positions[1] = 0.5 positions[1] = 0.5

View File

@@ -28,7 +28,7 @@ export class HUD {
this.weaponPanel.className = 'hud-weapon-panel' this.weaponPanel.className = 'hud-weapon-panel'
// 武器列表 // 武器列表
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE] const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
this.weaponSlots = [] this.weaponSlots = []
for (let i = 0; i < weaponList.length; i++) { for (let i = 0; i < weaponList.length; i++) {
const slot = document.createElement('div') const slot = document.createElement('div')
@@ -135,7 +135,7 @@ export class HUD {
* @param {Object} ammo 各武器弹药数 * @param {Object} ammo 各武器弹药数
*/ */
updateWeapons(currentIndex, ammo) { updateWeapons(currentIndex, ammo) {
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE] const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
for (let i = 0; i < this.weaponSlots.length; i++) { for (let i = 0; i < this.weaponSlots.length; i++) {
const slot = this.weaponSlots[i] const slot = this.weaponSlots[i]
// 高亮当前武器 // 高亮当前武器

View File

@@ -17,7 +17,10 @@ export class SettingsUI {
weapon1: 'Digit1', weapon1: 'Digit1',
weapon2: 'Digit2', weapon2: 'Digit2',
weapon3: 'Digit3', weapon3: 'Digit3',
weapon4: 'Digit4' weapon4: 'Digit4',
weapon5: 'Digit5',
weapon6: 'Digit6',
weapon7: 'Digit7'
} }
this.bindings = { ...this.defaultBindings } this.bindings = { ...this.defaultBindings }
} }
@@ -68,7 +71,10 @@ export class SettingsUI {
weapon1: 'Weapon 1 (Pistol)', weapon1: 'Weapon 1 (Pistol)',
weapon2: 'Weapon 2 (MG)', weapon2: 'Weapon 2 (MG)',
weapon3: 'Weapon 3 (Shotgun)', weapon3: 'Weapon 3 (Shotgun)',
weapon4: 'Weapon 4 (Grenade)' weapon4: 'Weapon 4 (Grenade)',
weapon5: 'Weapon 5 (Molotov)',
weapon6: 'Weapon 6 (Wall)',
weapon7: 'Weapon 7 (Turret)'
} }
// 生成每种操作的按键配置行 // 生成每种操作的按键配置行

View File

@@ -23,7 +23,10 @@ export const WEAPONS = {
PISTOL: 'pistol', // 手枪 PISTOL: 'pistol', // 手枪
MACHINE_GUN: 'machine_gun', // 机枪 MACHINE_GUN: 'machine_gun', // 机枪
SHOTGUN: 'shotgun', // 霰弹枪 SHOTGUN: 'shotgun', // 霰弹枪
GRENADE: 'grenade' // 手雷 GRENADE: 'grenade', // 手雷
MOLOTOV: 'molotov', // 燃烧瓶
NUT_WALL: 'nut_wall', // 坚果墙体
AUTO_TURRET: 'auto_turret' // 自动机枪塔
} }
// ========== 武器配置 ========== // ========== 武器配置 ==========
@@ -84,7 +87,51 @@ export const WEAPON_CONFIG = {
auto: false, auto: false,
chargeable: true, // 可蓄力 chargeable: true, // 可蓄力
maxCharge: 2000, // 最大蓄力时间(毫秒) maxCharge: 2000, // 最大蓄力时间(毫秒)
explosionRadius: 3 // 爆炸半径 explosionRadius: 3 // 爆炸半径
},
// 燃烧瓶配置
[WEAPONS.MOLOTOV]: {
name: 'Molotov',
damage: 80,
fireRate: 2000,
ammo: 5,
maxAmmo: 5,
speed: 12,
spread: 0,
pellets: 1,
range: 12,
auto: false,
chargeable: true, // 可蓄力
maxCharge: 2000,
explosionRadius: 2
},
// 坚果墙体配置
[WEAPONS.NUT_WALL]: {
name: 'Wall',
damage: 0,
fireRate: 1000,
ammo: 3,
maxAmmo: 3,
speed: 0,
spread: 0,
pellets: 0,
range: 0,
auto: false,
chargeable: false
},
// 自动机枪塔配置
[WEAPONS.AUTO_TURRET]: {
name: 'Turret',
damage: 0,
fireRate: 2000,
ammo: 2,
maxAmmo: 2,
speed: 0,
spread: 0,
pellets: 0,
range: 0,
auto: false,
chargeable: false
} }
} }

View File

@@ -18,7 +18,10 @@ export class InputManager {
weapon1: 'Digit1', weapon1: 'Digit1',
weapon2: 'Digit2', weapon2: 'Digit2',
weapon3: 'Digit3', weapon3: 'Digit3',
weapon4: 'Digit4' weapon4: 'Digit4',
weapon5: 'Digit5',
weapon6: 'Digit6',
weapon7: 'Digit7'
} }
// 待处理的输入序列(用于客户端预测) // 待处理的输入序列(用于客户端预测)
@@ -100,13 +103,16 @@ export class InputManager {
/** /**
* 获取当前选择的武器 * 获取当前选择的武器
* @returns {number} 武器索引0-3),无选择返回-1 * @returns {number} 武器索引0-6),无选择返回-1
*/ */
getSelectedWeapon() { getSelectedWeapon() {
if (this.keys[this.keyBindings.weapon1]) return 0 if (this.keys[this.keyBindings.weapon1]) return 0
if (this.keys[this.keyBindings.weapon2]) return 1 if (this.keys[this.keyBindings.weapon2]) return 1
if (this.keys[this.keyBindings.weapon3]) return 2 if (this.keys[this.keyBindings.weapon3]) return 2
if (this.keys[this.keyBindings.weapon4]) return 3 if (this.keys[this.keyBindings.weapon4]) return 3
if (this.keys[this.keyBindings.weapon5]) return 4
if (this.keys[this.keyBindings.weapon6]) return 5
if (this.keys[this.keyBindings.weapon7]) return 6
return -1 return -1
} }

View File

@@ -1 +1,671 @@
{"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}]} {
"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
}
]
}