迁移证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 {
private String id;
private String hostId;
private Map<String, Player> players;
private Map<String, PlayerInfo> players;
private boolean gameStarted;
private final int maxPlayers = 4;
@@ -27,15 +27,13 @@ public class Room {
this.id = id;
this.hostId = hostId;
this.players = new LinkedHashMap<>();
Player host = new Player(hostId, hostName, 0, 0);
players.put(hostId, host);
players.put(hostId, new PlayerInfo(hostId, hostName));
}
public boolean addPlayer(String playerId, String playerName) {
if (players.size() >= maxPlayers) return false;
if (players.containsKey(playerId)) return false;
Player player = new Player(playerId, playerName, 0, 0);
players.put(playerId, player);
players.put(playerId, new PlayerInfo(playerId, playerName));
return true;
}
@@ -43,11 +41,11 @@ public class Room {
players.remove(playerId);
}
public Player getPlayer(String playerId) {
public PlayerInfo getPlayer(String playerId) {
return players.get(playerId);
}
public Collection<Player> getPlayers() {
public Collection<PlayerInfo> getPlayers() {
return players.values();
}
@@ -60,18 +58,12 @@ public class Room {
}
public boolean allReady() {
for (Player p : players.values()) {
for (PlayerInfo p : players.values()) {
if (!p.getId().equals(hostId) && !p.isReady()) return false;
}
return true;
}
/**
* 将房间状态转换为Map格式用于网络传输
*
* @param playerId 目标玩家ID
* @return 包含房间状态的Map
*/
public Map<String, Object> toStateMap(String playerId) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("roomId", id);
@@ -81,7 +73,7 @@ public class Room {
List<Map<String, Object>> playerList = new ArrayList<>();
int index = 0;
for (Player p : players.values()) {
for (PlayerInfo p : players.values()) {
Map<String, Object> pm = new LinkedHashMap<>();
pm.put("id", p.getId());
pm.put("name", p.getName());
@@ -93,15 +85,11 @@ public class Room {
return map;
}
/**
* 将房间信息转换为房间列表格式,用于大厅显示
*
* @return 包含房间信息的Map
*/
public Map<String, Object> toRoomListMap() {
Map<String, Object> map = new LinkedHashMap<>();
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());
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;
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.ScheduledExecutorService;
@@ -10,37 +11,24 @@ import java.util.concurrent.TimeUnit;
* 游戏循环类
*
* 管理游戏主循环,以固定帧率更新游戏世界状态。
* 使用 ScheduledExecutorService 实现精确的定时调度,避免忙等待。
*/
public class GameLoop {
/** 房间ID */
private String roomId;
/** 游戏世界实例 */
private GameWorld world;
/** 游戏状态广播回调 */
private ECSWorld world;
private GameService.GameStateBroadcast broadcaster;
/** 循环运行标志 */
private StateSyncSystem stateSyncSystem;
private volatile boolean running;
/** 逻辑帧率(每秒 tick 数) */
private static final int TICK_RATE = 30;
/** 每次 tick 的时间间隔(毫秒) */
private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE;
/** 每次 tick 的时间间隔(秒,用于游戏逻辑计算) */
private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE;
/** 定时任务执行器 */
private ScheduledExecutorService scheduler;
/**
* 构造函数
*
* @param roomId 房间ID
* @param world 游戏世界实例
* @param broadcaster 游戏状态广播回调
*/
public GameLoop(String roomId, GameWorld world, GameService.GameStateBroadcast broadcaster) {
public GameLoop(String roomId, ECSWorld world, GameService.GameStateBroadcast broadcaster,
StateSyncSystem stateSyncSystem) {
this.roomId = roomId;
this.world = world;
this.broadcaster = broadcaster;
this.stateSyncSystem = stateSyncSystem;
this.running = true;
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "GameLoop-" + roomId);
@@ -49,29 +37,18 @@ public class GameLoop {
});
}
/**
* 启动游戏循环
*/
public void start() {
scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS);
}
/**
* 单次游戏逻辑更新
*/
private void tick() {
if (!running) {
return;
}
if (!running) return;
synchronized (world.getLock()) {
world.update(TICK_INTERVAL_SEC);
}
broadcaster.broadcast(roomId, world);
}
/**
* 停止游戏循环
*/
public void stop() {
running = false;
if (scheduler != null && !scheduler.isShutdown()) {

View File

@@ -1,6 +1,9 @@
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.systems.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,83 +15,87 @@ import java.util.concurrent.ConcurrentHashMap;
*
* 负责游戏生命周期管理,包括:
* - 游戏启动和停止
* - 游戏世界状态管理
* - ECS 游戏世界状态管理
* - 玩家输入处理
* - 游戏状态构建
*
* 将游戏逻辑从 WebSocket 层解耦,便于独立测试和维护。
*/
public class GameService {
private static final Logger logger = LoggerFactory.getLogger(GameService.class);
/** 活跃游戏世界集合roomId -> GameWorld */
private final Map<String, GameWorld> activeGames = new ConcurrentHashMap<>();
/** 游戏循环集合roomId -> GameLoop */
private final Map<String, ECSWorld> activeGames = new ConcurrentHashMap<>();
private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>();
/** 游戏状态广播回调 */
private final GameStateBroadcast broadcaster;
/**
* 游戏状态广播接口
*/
public interface GameStateBroadcast {
void broadcast(String roomId, GameWorld world);
void broadcast(String roomId, ECSWorld world);
}
/**
* 构造函数
*
* @param broadcaster 状态广播回调
*/
public GameService(GameStateBroadcast broadcaster) {
this.broadcaster = broadcaster;
}
/**
* 启动游戏
*
* @param room 游戏房间
* @return 游戏初始化数据按玩家ID分组
*/
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;
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
for (Player player : room.getPlayers()) {
for (PlayerInfo playerInfo : room.getPlayers()) {
int[] sp = spawnPoints.get(index % spawnPoints.size());
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
player.setPosition(wx, wy);
world.addPlayer(player);
world.createPlayerEntity(playerInfo.getId(), playerInfo.getName(), wx, wy);
index++;
}
activeGames.put(room.getId(), world);
// Build init data
Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>();
for (Player player : room.getPlayers()) {
for (PlayerInfo playerInfo : room.getPlayers()) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("playerId", player.getId());
data.put("playerId", playerInfo.getId());
data.put("mapData", serializeMapData(world.getMapData()));
List<Map<String, Object>> playerList = new ArrayList<>();
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<>();
pm.put("id", p.getId());
pm.put("name", p.getName());
pm.put("x", p.getX());
pm.put("y", p.getY());
if (entityId != null) {
Position pos = world.getPositions().get(entityId);
if (pos != null) {
pm.put("x", pos.getX());
pm.put("y", pos.getY());
}
}
pm.put("index", idx++);
playerList.add(pm);
}
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);
loop.start();
@@ -96,11 +103,6 @@ public class GameService {
return playerInitData;
}
/**
* 停止游戏
*
* @param roomId 房间ID
*/
public void stopGame(String roomId) {
GameLoop loop = gameLoops.remove(roomId);
if (loop != null) {
@@ -110,61 +112,41 @@ public class GameService {
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,
float dx, float dy, float aimX, float aimY,
boolean firing, int weaponIndex, int seq,
float grenadeCharge, boolean grenadeReleased) {
GameWorld world = activeGames.get(roomId);
ECSWorld world = activeGames.get(roomId);
if (world == null) return;
synchronized (world.getLock()) {
Player player = world.getPlayer(playerId);
if (player == null || !player.isAlive()) return;
Integer entityId = world.getPlayerEntity(playerId);
if (entityId == null) return;
if (weaponIndex >= 0 && weaponIndex <= 3) {
player.setWeaponIndex(weaponIndex);
}
player.applyMovement(dx, dy, world.getMap());
player.setAngle(aimX, aimY);
player.setLastProcessedSeq(seq);
if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) {
world.fireWeapon(player, aimX, aimY, grenadeCharge);
} else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) {
world.fireWeapon(player, aimX, aimY);
Health health = world.getHealths().get(entityId);
if (health == null || !health.isAlive()) return;
PlayerInput input = world.getPlayerInputs().get(entityId);
if (input == null) return;
input.setDx(dx);
input.setDy(dy);
input.setAimX(aimX);
input.setAimY(aimY);
input.setFiring(firing);
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) {
return activeGames.containsKey(roomId);
}
/**
* 序列化地图数据为二维列表
*/
private List<List<Integer>> serializeMapData(int[][] cells) {
List<List<Integer>> result = new ArrayList<>();
for (int[] row : cells) {

View File

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

View File

@@ -1,6 +1,6 @@
package com.zombie.game.server;
import com.zombie.game.model.Player;
import com.zombie.game.model.PlayerInfo;
import com.zombie.game.model.Room;
import java.util.ArrayList;
@@ -29,7 +29,7 @@ public class RoomManager {
*/
public void addRoom(String roomId, Room room) {
rooms.put(roomId, room);
for (Player player : room.getPlayers()) {
for (PlayerInfo player : room.getPlayers()) {
playerToRoom.put(player.getId(), roomId);
}
}
@@ -129,7 +129,7 @@ public class RoomManager {
public void removeRoom(String roomId) {
Room room = rooms.remove(roomId);
if (room != null) {
for (Player player : room.getPlayers()) {
for (PlayerInfo player : room.getPlayers()) {
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;
}
/**
* 加载所有模板
*/
private void loadAll() {
loadZombies();
loadWeapons();
@@ -82,6 +79,7 @@ public class TemplateManager {
WeaponTemplate t = new WeaponTemplate(
node.get("id").asText(),
node.get("name").asText(),
node.has("category") ? node.get("category").asText() : "firearm",
node.get("damage").asInt(),
node.get("fireRate").asLong(),
node.get("pelletCount").asInt(),
@@ -91,7 +89,15 @@ public class TemplateManager {
node.get("maxAmmo").asInt(),
node.get("chargeable").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);
}
@@ -137,9 +143,6 @@ public class TemplateManager {
return playerTemplate;
}
/**
* 获取武器列表的索引位置
*/
public int getWeaponIndex(String id) {
int idx = 0;
for (String key : weaponTemplates.keySet()) {
@@ -149,9 +152,6 @@ public class TemplateManager {
return 0;
}
/**
* 根据索引获取武器ID
*/
public String getWeaponId(int index) {
int idx = 0;
for (String key : weaponTemplates.keySet()) {

View File

@@ -6,11 +6,13 @@ import lombok.Getter;
* 武器类型模板
*
* 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。
* 支持三种类别firearm枪械、thrown投掷、placed放置
*/
@Getter
public class WeaponTemplate {
private final String id;
private final String name;
private final String category; // firearm, thrown, placed
private final int damage;
private final long fireRate;
private final int pelletCount;
@@ -22,12 +24,28 @@ public class WeaponTemplate {
private final boolean explosive;
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,
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.name = name;
this.category = category;
this.damage = damage;
this.fireRate = fireRate;
this.pelletCount = pelletCount;
@@ -38,5 +56,13 @@ public class WeaponTemplate {
this.chargeable = chargeable;
this.explosive = explosive;
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",
"name": "手枪",
"category": "firearm",
"damage": 50,
"fireRate": 400,
"pelletCount": 1,
@@ -12,11 +13,20 @@
"maxAmmo": 2147483647,
"chargeable": 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",
"name": "机枪",
"name": "机枪",
"category": "firearm",
"damage": 50,
"fireRate": 100,
"pelletCount": 1,
@@ -26,11 +36,20 @@
"maxAmmo": 100,
"chargeable": 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",
"name": "弹枪",
"name": "弹枪",
"category": "firearm",
"damage": 50,
"fireRate": 800,
"pelletCount": 10,
@@ -40,11 +59,20 @@
"maxAmmo": 20,
"chargeable": 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",
"name": "手榴弹",
"category": "thrown",
"damage": 120,
"fireRate": 1500,
"pelletCount": 1,
@@ -54,7 +82,84 @@
"maxAmmo": 10,
"chargeable": 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.MACHINE_GUN]: 100,
[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)
if (localPlayer) {
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.input.mouse.groundX || 0, this.input.mouse.groundY || 0,
this.grenadeChargePercent)
@@ -260,11 +263,16 @@ export class GameEngine {
// 应用本地预测(客户端预测)
this._applyLocalPrediction(inputState)
// 添加手雷相关数据到输入状态
// 添加蓄力武器相关数据到输入状态(手雷/燃烧瓶)
inputState.grenadeCharge = this.grenadeChargePercent
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)
@@ -287,10 +295,11 @@ export class GameEngine {
* 手雷需要长按鼠标蓄力,松开释放
*/
_handleGrenadeCharge(inputState) {
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE]
const currentWeapon = weaponList[this.currentWeaponIndex]
const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
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) {
this.isChargingGrenade = true
@@ -298,13 +307,13 @@ export class GameEngine {
} else if (inputState.firing && this.isChargingGrenade) {
// 蓄力中,计算蓄力百分比
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) {
// 释放手雷
// 释放
this.grenadeReleased = true
this.isChargingGrenade = false
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 {
this.isChargingGrenade = false
@@ -417,9 +426,9 @@ export class GameEngine {
// 新增子弹
this.bullets.set(bs.id, { ...bs })
this.scene.addBullet(bs)
// 枪口火焰特效(手雷除外)
// 枪口火焰特效(投掷类武器除外)
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)
}
} else {

View File

@@ -556,22 +556,30 @@ export class GameScene {
*/
addBullet(bullet) {
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
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 mat = new THREE.MeshBasicMaterial({ color: 0x44ff44 })
const mat = new THREE.MeshBasicMaterial({ color: bodyColor })
const mesh = new THREE.Mesh(geo, mat)
mesh.position.set(bullet.x, z, bullet.y)
group.add(mesh)
const glowGeo = new THREE.SphereGeometry(0.18, 8, 8)
const glowMat = new THREE.MeshBasicMaterial({
color: 0x22cc22,
color: glowColor,
transparent: true,
opacity: 0.4
opacity: 0.5
})
const glow = new THREE.Mesh(glowGeo, glowMat)
glow.position.set(bullet.x, z, bullet.y)
@@ -580,7 +588,7 @@ export class GameScene {
// 拖尾线
const trailGeo = new THREE.BufferGeometry()
const trailMat = new THREE.LineBasicMaterial({
color: 0x88ff88,
color: trailColor,
transparent: true,
opacity: 0.6
})
@@ -598,14 +606,8 @@ export class GameScene {
const angle = bullet.angle || 0
// 根据武器类型调整大小
switch (bullet.weapon) {
case WEAPONS.MACHINE_GUN:
bulletSize = 0.05
break
case WEAPONS.SHOTGUN:
bulletSize = 0.04
break
}
if (wIdx === 1) bulletSize = 0.05 // 机枪
else if (wIdx === 2) bulletSize = 0.04 // 霰弹枪
// 拖尾
const trailLength = 2.5
@@ -651,9 +653,10 @@ export class GameScene {
x: bullet.x,
y: bullet.y,
z: z,
weapon: bullet.weapon,
weaponIndex: wIdx,
angle: bullet.angle || 0,
isGrenade
isGrenade,
isMolotov
})
}
@@ -695,7 +698,7 @@ export class GameScene {
child.position.z = y
}
// 更新拖尾位置
if (child.isLine && !bullet.isGrenade) {
if (child.isLine && !bullet.isGrenade && !bullet.isMolotov) {
const trailLength = 2.5
const positions = child.geometry.attributes.position.array
positions[0] = x - Math.sin(angle) * trailLength
@@ -1083,7 +1086,7 @@ export class GameScene {
// 更新子弹拖尾
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
positions[0] = bullet.x - Math.sin(bullet.angle) * 1.5
positions[1] = 0.5

View File

@@ -28,7 +28,7 @@ export class HUD {
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 = []
for (let i = 0; i < weaponList.length; i++) {
const slot = document.createElement('div')
@@ -135,7 +135,7 @@ export class HUD {
* @param {Object} 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++) {
const slot = this.weaponSlots[i]
// 高亮当前武器

View File

@@ -17,7 +17,10 @@ export class SettingsUI {
weapon1: 'Digit1',
weapon2: 'Digit2',
weapon3: 'Digit3',
weapon4: 'Digit4'
weapon4: 'Digit4',
weapon5: 'Digit5',
weapon6: 'Digit6',
weapon7: 'Digit7'
}
this.bindings = { ...this.defaultBindings }
}
@@ -68,7 +71,10 @@ export class SettingsUI {
weapon1: 'Weapon 1 (Pistol)',
weapon2: 'Weapon 2 (MG)',
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', // 手枪
MACHINE_GUN: 'machine_gun', // 机枪
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,
chargeable: true, // 可蓄力
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',
weapon2: 'Digit2',
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() {
if (this.keys[this.keyBindings.weapon1]) return 0
if (this.keys[this.keyBindings.weapon2]) return 1
if (this.keys[this.keyBindings.weapon3]) return 2
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
}

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
}
]
}