迁移证esc
This commit is contained in:
429
backend/src/main/java/com/zombie/game/ecs/ECSWorld.java
Normal file
429
backend/src/main/java/com/zombie/game/ecs/ECSWorld.java
Normal 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 模板ID(normal/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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/src/main/java/com/zombie/game/ecs/System.java
Normal file
10
backend/src/main/java/com/zombie/game/ecs/System.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.zombie.game.ecs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ECS 系统接口
|
||||||
|
*
|
||||||
|
* 所有系统实现此接口,在每帧 update 中处理具有特定组件组合的实体。
|
||||||
|
*/
|
||||||
|
public interface System {
|
||||||
|
void update(float dt, ECSWorld world);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.zombie.game.ecs.components;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 僵尸AI组件
|
||||||
|
* 存储僵尸的寻路目标、格子预留、墙体攻击等AI状态。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ZombieAI {
|
||||||
|
/** 僵尸模板ID(normal/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
backend/src/main/java/com/zombie/game/model/PlayerInfo.java
Normal file
25
backend/src/main/java/com/zombie/game/model/PlayerInfo.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import static com.zombie.game.model.Constants.*;
|
|||||||
public class Room {
|
public class Room {
|
||||||
private String id;
|
private String id;
|
||||||
private String hostId;
|
private String hostId;
|
||||||
private Map<String, Player> players;
|
private Map<String, PlayerInfo> players;
|
||||||
private boolean gameStarted;
|
private boolean gameStarted;
|
||||||
private final int maxPlayers = 4;
|
private final int maxPlayers = 4;
|
||||||
|
|
||||||
@@ -27,15 +27,13 @@ public class Room {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
this.hostId = hostId;
|
this.hostId = hostId;
|
||||||
this.players = new LinkedHashMap<>();
|
this.players = new LinkedHashMap<>();
|
||||||
Player host = new Player(hostId, hostName, 0, 0);
|
players.put(hostId, new PlayerInfo(hostId, hostName));
|
||||||
players.put(hostId, host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addPlayer(String playerId, String playerName) {
|
public boolean addPlayer(String playerId, String playerName) {
|
||||||
if (players.size() >= maxPlayers) return false;
|
if (players.size() >= maxPlayers) return false;
|
||||||
if (players.containsKey(playerId)) return false;
|
if (players.containsKey(playerId)) return false;
|
||||||
Player player = new Player(playerId, playerName, 0, 0);
|
players.put(playerId, new PlayerInfo(playerId, playerName));
|
||||||
players.put(playerId, player);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +41,11 @@ public class Room {
|
|||||||
players.remove(playerId);
|
players.remove(playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Player getPlayer(String playerId) {
|
public PlayerInfo getPlayer(String playerId) {
|
||||||
return players.get(playerId);
|
return players.get(playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Player> getPlayers() {
|
public Collection<PlayerInfo> getPlayers() {
|
||||||
return players.values();
|
return players.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,18 +58,12 @@ public class Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean allReady() {
|
public boolean allReady() {
|
||||||
for (Player p : players.values()) {
|
for (PlayerInfo p : players.values()) {
|
||||||
if (!p.getId().equals(hostId) && !p.isReady()) return false;
|
if (!p.getId().equals(hostId) && !p.isReady()) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将房间状态转换为Map格式,用于网络传输
|
|
||||||
*
|
|
||||||
* @param playerId 目标玩家ID
|
|
||||||
* @return 包含房间状态的Map
|
|
||||||
*/
|
|
||||||
public Map<String, Object> toStateMap(String playerId) {
|
public Map<String, Object> toStateMap(String playerId) {
|
||||||
Map<String, Object> map = new LinkedHashMap<>();
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
map.put("roomId", id);
|
map.put("roomId", id);
|
||||||
@@ -81,7 +73,7 @@ public class Room {
|
|||||||
|
|
||||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (Player p : players.values()) {
|
for (PlayerInfo p : players.values()) {
|
||||||
Map<String, Object> pm = new LinkedHashMap<>();
|
Map<String, Object> pm = new LinkedHashMap<>();
|
||||||
pm.put("id", p.getId());
|
pm.put("id", p.getId());
|
||||||
pm.put("name", p.getName());
|
pm.put("name", p.getName());
|
||||||
@@ -93,15 +85,11 @@ public class Room {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将房间信息转换为房间列表格式,用于大厅显示
|
|
||||||
*
|
|
||||||
* @return 包含房间信息的Map
|
|
||||||
*/
|
|
||||||
public Map<String, Object> toRoomListMap() {
|
public Map<String, Object> toRoomListMap() {
|
||||||
Map<String, Object> map = new LinkedHashMap<>();
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
map.put("id", id);
|
map.put("id", id);
|
||||||
map.put("hostName", players.get(hostId) != null ? players.get(hostId).getName() : "Unknown");
|
PlayerInfo host = players.get(hostId);
|
||||||
|
map.put("hostName", host != null ? host.getName() : "Unknown");
|
||||||
map.put("playerCount", players.size());
|
map.put("playerCount", players.size());
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.zombie.game.server;
|
package com.zombie.game.server;
|
||||||
|
|
||||||
import com.zombie.game.model.GameWorld;
|
import com.zombie.game.ecs.ECSWorld;
|
||||||
|
import com.zombie.game.systems.StateSyncSystem;
|
||||||
|
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
@@ -10,37 +11,24 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* 游戏循环类
|
* 游戏循环类
|
||||||
*
|
*
|
||||||
* 管理游戏主循环,以固定帧率更新游戏世界状态。
|
* 管理游戏主循环,以固定帧率更新游戏世界状态。
|
||||||
* 使用 ScheduledExecutorService 实现精确的定时调度,避免忙等待。
|
|
||||||
*/
|
*/
|
||||||
public class GameLoop {
|
public class GameLoop {
|
||||||
/** 房间ID */
|
|
||||||
private String roomId;
|
private String roomId;
|
||||||
/** 游戏世界实例 */
|
private ECSWorld world;
|
||||||
private GameWorld world;
|
|
||||||
/** 游戏状态广播回调 */
|
|
||||||
private GameService.GameStateBroadcast broadcaster;
|
private GameService.GameStateBroadcast broadcaster;
|
||||||
/** 循环运行标志 */
|
private StateSyncSystem stateSyncSystem;
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
/** 逻辑帧率(每秒 tick 数) */
|
|
||||||
private static final int TICK_RATE = 30;
|
private static final int TICK_RATE = 30;
|
||||||
/** 每次 tick 的时间间隔(毫秒) */
|
|
||||||
private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE;
|
private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE;
|
||||||
/** 每次 tick 的时间间隔(秒,用于游戏逻辑计算) */
|
|
||||||
private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE;
|
private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE;
|
||||||
/** 定时任务执行器 */
|
|
||||||
private ScheduledExecutorService scheduler;
|
private ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
/**
|
public GameLoop(String roomId, ECSWorld world, GameService.GameStateBroadcast broadcaster,
|
||||||
* 构造函数
|
StateSyncSystem stateSyncSystem) {
|
||||||
*
|
|
||||||
* @param roomId 房间ID
|
|
||||||
* @param world 游戏世界实例
|
|
||||||
* @param broadcaster 游戏状态广播回调
|
|
||||||
*/
|
|
||||||
public GameLoop(String roomId, GameWorld world, GameService.GameStateBroadcast broadcaster) {
|
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.world = world;
|
this.world = world;
|
||||||
this.broadcaster = broadcaster;
|
this.broadcaster = broadcaster;
|
||||||
|
this.stateSyncSystem = stateSyncSystem;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
Thread t = new Thread(r, "GameLoop-" + roomId);
|
Thread t = new Thread(r, "GameLoop-" + roomId);
|
||||||
@@ -49,29 +37,18 @@ public class GameLoop {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动游戏循环
|
|
||||||
*/
|
|
||||||
public void start() {
|
public void start() {
|
||||||
scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 单次游戏逻辑更新
|
|
||||||
*/
|
|
||||||
private void tick() {
|
private void tick() {
|
||||||
if (!running) {
|
if (!running) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
synchronized (world.getLock()) {
|
synchronized (world.getLock()) {
|
||||||
world.update(TICK_INTERVAL_SEC);
|
world.update(TICK_INTERVAL_SEC);
|
||||||
}
|
}
|
||||||
broadcaster.broadcast(roomId, world);
|
broadcaster.broadcast(roomId, world);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止游戏循环
|
|
||||||
*/
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
if (scheduler != null && !scheduler.isShutdown()) {
|
if (scheduler != null && !scheduler.isShutdown()) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.zombie.game.server;
|
package com.zombie.game.server;
|
||||||
|
|
||||||
|
import com.zombie.game.ecs.ECSWorld;
|
||||||
|
import com.zombie.game.ecs.components.*;
|
||||||
import com.zombie.game.model.*;
|
import com.zombie.game.model.*;
|
||||||
|
import com.zombie.game.systems.*;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -12,83 +15,87 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
*
|
*
|
||||||
* 负责游戏生命周期管理,包括:
|
* 负责游戏生命周期管理,包括:
|
||||||
* - 游戏启动和停止
|
* - 游戏启动和停止
|
||||||
* - 游戏世界状态管理
|
* - ECS 游戏世界状态管理
|
||||||
* - 玩家输入处理
|
* - 玩家输入处理
|
||||||
* - 游戏状态构建
|
* - 游戏状态构建
|
||||||
*
|
|
||||||
* 将游戏逻辑从 WebSocket 层解耦,便于独立测试和维护。
|
|
||||||
*/
|
*/
|
||||||
public class GameService {
|
public class GameService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GameService.class);
|
private static final Logger logger = LoggerFactory.getLogger(GameService.class);
|
||||||
|
|
||||||
/** 活跃游戏世界集合:roomId -> GameWorld */
|
private final Map<String, ECSWorld> activeGames = new ConcurrentHashMap<>();
|
||||||
private final Map<String, GameWorld> activeGames = new ConcurrentHashMap<>();
|
|
||||||
/** 游戏循环集合:roomId -> GameLoop */
|
|
||||||
private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>();
|
private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>();
|
||||||
/** 游戏状态广播回调 */
|
|
||||||
private final GameStateBroadcast broadcaster;
|
private final GameStateBroadcast broadcaster;
|
||||||
|
|
||||||
/**
|
|
||||||
* 游戏状态广播接口
|
|
||||||
*/
|
|
||||||
public interface GameStateBroadcast {
|
public interface GameStateBroadcast {
|
||||||
void broadcast(String roomId, GameWorld world);
|
void broadcast(String roomId, ECSWorld world);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param broadcaster 状态广播回调
|
|
||||||
*/
|
|
||||||
public GameService(GameStateBroadcast broadcaster) {
|
public GameService(GameStateBroadcast broadcaster) {
|
||||||
this.broadcaster = broadcaster;
|
this.broadcaster = broadcaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动游戏
|
|
||||||
*
|
|
||||||
* @param room 游戏房间
|
|
||||||
* @return 游戏初始化数据,按玩家ID分组
|
|
||||||
*/
|
|
||||||
public Map<String, Map<String, Object>> startGame(Room room) {
|
public Map<String, Map<String, Object>> startGame(Room room) {
|
||||||
GameWorld world = new GameWorld();
|
ECSWorld world = new ECSWorld();
|
||||||
|
|
||||||
|
// Register systems
|
||||||
|
StateSyncSystem stateSyncSystem = new StateSyncSystem();
|
||||||
|
world.addSystem(new DifficultySystem());
|
||||||
|
world.addSystem(new PlayerInputSystem());
|
||||||
|
world.addSystem(new ZombieMovementSystem());
|
||||||
|
world.addSystem(new WeaponFiringSystem());
|
||||||
|
world.addSystem(new BulletMovementSystem());
|
||||||
|
world.addSystem(new TurretSystem());
|
||||||
|
world.addSystem(new CollisionSystem());
|
||||||
|
world.addSystem(new DamageSystem());
|
||||||
|
world.addSystem(new ZombieDeathSystem());
|
||||||
|
world.addSystem(new FireZoneSystem());
|
||||||
|
world.addSystem(new LootSystem());
|
||||||
|
world.addSystem(new RespawnSystem());
|
||||||
|
|
||||||
|
// Create player entities
|
||||||
int index = 0;
|
int index = 0;
|
||||||
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
|
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
|
||||||
|
|
||||||
for (Player player : room.getPlayers()) {
|
for (PlayerInfo playerInfo : room.getPlayers()) {
|
||||||
int[] sp = spawnPoints.get(index % spawnPoints.size());
|
int[] sp = spawnPoints.get(index % spawnPoints.size());
|
||||||
float wx = sp[0] + 0.5f;
|
float wx = sp[0] + 0.5f;
|
||||||
float wy = sp[1] + 0.5f;
|
float wy = sp[1] + 0.5f;
|
||||||
|
|
||||||
player.setPosition(wx, wy);
|
world.createPlayerEntity(playerInfo.getId(), playerInfo.getName(), wx, wy);
|
||||||
world.addPlayer(player);
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeGames.put(room.getId(), world);
|
activeGames.put(room.getId(), world);
|
||||||
|
|
||||||
|
// Build init data
|
||||||
Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>();
|
Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>();
|
||||||
for (Player player : room.getPlayers()) {
|
for (PlayerInfo playerInfo : room.getPlayers()) {
|
||||||
Map<String, Object> data = new LinkedHashMap<>();
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
data.put("playerId", player.getId());
|
data.put("playerId", playerInfo.getId());
|
||||||
data.put("mapData", serializeMapData(world.getMapData()));
|
data.put("mapData", serializeMapData(world.getMapData()));
|
||||||
|
|
||||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (Player p : room.getPlayers()) {
|
for (PlayerInfo p : room.getPlayers()) {
|
||||||
|
Integer entityId = world.getPlayerEntity(p.getId());
|
||||||
Map<String, Object> pm = new LinkedHashMap<>();
|
Map<String, Object> pm = new LinkedHashMap<>();
|
||||||
pm.put("id", p.getId());
|
pm.put("id", p.getId());
|
||||||
pm.put("name", p.getName());
|
pm.put("name", p.getName());
|
||||||
pm.put("x", p.getX());
|
if (entityId != null) {
|
||||||
pm.put("y", p.getY());
|
Position pos = world.getPositions().get(entityId);
|
||||||
|
if (pos != null) {
|
||||||
|
pm.put("x", pos.getX());
|
||||||
|
pm.put("y", pos.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
pm.put("index", idx++);
|
pm.put("index", idx++);
|
||||||
playerList.add(pm);
|
playerList.add(pm);
|
||||||
}
|
}
|
||||||
data.put("players", playerList);
|
data.put("players", playerList);
|
||||||
playerInitData.put(player.getId(), data);
|
playerInitData.put(playerInfo.getId(), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
GameLoop loop = new GameLoop(room.getId(), world, broadcaster);
|
GameLoop loop = new GameLoop(room.getId(), world, broadcaster, stateSyncSystem);
|
||||||
gameLoops.put(room.getId(), loop);
|
gameLoops.put(room.getId(), loop);
|
||||||
loop.start();
|
loop.start();
|
||||||
|
|
||||||
@@ -96,11 +103,6 @@ public class GameService {
|
|||||||
return playerInitData;
|
return playerInitData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止游戏
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID
|
|
||||||
*/
|
|
||||||
public void stopGame(String roomId) {
|
public void stopGame(String roomId) {
|
||||||
GameLoop loop = gameLoops.remove(roomId);
|
GameLoop loop = gameLoops.remove(roomId);
|
||||||
if (loop != null) {
|
if (loop != null) {
|
||||||
@@ -110,61 +112,41 @@ public class GameService {
|
|||||||
logger.info("Game stopped for room: {}", roomId);
|
logger.info("Game stopped for room: {}", roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理玩家输入
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID
|
|
||||||
* @param playerId 玩家ID
|
|
||||||
* @param dx X方向移动量
|
|
||||||
* @param dy Y方向移动量
|
|
||||||
* @param aimX 瞄准X坐标
|
|
||||||
* @param aimY 瞄准Y坐标
|
|
||||||
* @param firing 是否开火
|
|
||||||
* @param weaponIndex 武器索引
|
|
||||||
* @param seq 输入序列号
|
|
||||||
* @param grenadeCharge 手榴弹蓄力值
|
|
||||||
* @param grenadeReleased 手榴弹是否释放
|
|
||||||
*/
|
|
||||||
public void processPlayerInput(String roomId, String playerId,
|
public void processPlayerInput(String roomId, String playerId,
|
||||||
float dx, float dy, float aimX, float aimY,
|
float dx, float dy, float aimX, float aimY,
|
||||||
boolean firing, int weaponIndex, int seq,
|
boolean firing, int weaponIndex, int seq,
|
||||||
float grenadeCharge, boolean grenadeReleased) {
|
float grenadeCharge, boolean grenadeReleased) {
|
||||||
GameWorld world = activeGames.get(roomId);
|
ECSWorld world = activeGames.get(roomId);
|
||||||
if (world == null) return;
|
if (world == null) return;
|
||||||
|
|
||||||
synchronized (world.getLock()) {
|
synchronized (world.getLock()) {
|
||||||
Player player = world.getPlayer(playerId);
|
Integer entityId = world.getPlayerEntity(playerId);
|
||||||
if (player == null || !player.isAlive()) return;
|
if (entityId == null) return;
|
||||||
|
|
||||||
if (weaponIndex >= 0 && weaponIndex <= 3) {
|
Health health = world.getHealths().get(entityId);
|
||||||
player.setWeaponIndex(weaponIndex);
|
if (health == null || !health.isAlive()) return;
|
||||||
}
|
|
||||||
|
PlayerInput input = world.getPlayerInputs().get(entityId);
|
||||||
player.applyMovement(dx, dy, world.getMap());
|
if (input == null) return;
|
||||||
player.setAngle(aimX, aimY);
|
|
||||||
player.setLastProcessedSeq(seq);
|
input.setDx(dx);
|
||||||
|
input.setDy(dy);
|
||||||
if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) {
|
input.setAimX(aimX);
|
||||||
world.fireWeapon(player, aimX, aimY, grenadeCharge);
|
input.setAimY(aimY);
|
||||||
} else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) {
|
input.setFiring(firing);
|
||||||
world.fireWeapon(player, aimX, aimY);
|
if (weaponIndex >= 0 && weaponIndex <= 6) {
|
||||||
|
input.setWeaponIndex(weaponIndex);
|
||||||
}
|
}
|
||||||
|
input.setSeq(seq);
|
||||||
|
input.setGrenadeCharge(grenadeCharge);
|
||||||
|
input.setGrenadeReleased(grenadeReleased);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查房间是否有活跃游戏
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID
|
|
||||||
* @return true 表示有活跃游戏
|
|
||||||
*/
|
|
||||||
public boolean hasActiveGame(String roomId) {
|
public boolean hasActiveGame(String roomId) {
|
||||||
return activeGames.containsKey(roomId);
|
return activeGames.containsKey(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 序列化地图数据为二维列表
|
|
||||||
*/
|
|
||||||
private List<List<Integer>> serializeMapData(int[][] cells) {
|
private List<List<Integer>> serializeMapData(int[][] cells) {
|
||||||
List<List<Integer>> result = new ArrayList<>();
|
List<List<Integer>> result = new ArrayList<>();
|
||||||
for (int[] row : cells) {
|
for (int[] row : cells) {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.zombie.game.server;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import com.zombie.game.ecs.ECSWorld;
|
||||||
import com.zombie.game.model.*;
|
import com.zombie.game.model.*;
|
||||||
import com.zombie.game.model.Constants;
|
import com.zombie.game.model.Constants;
|
||||||
|
import com.zombie.game.systems.StateSyncSystem;
|
||||||
import org.java_websocket.WebSocket;
|
import org.java_websocket.WebSocket;
|
||||||
import org.java_websocket.handshake.ClientHandshake;
|
import org.java_websocket.handshake.ClientHandshake;
|
||||||
import org.java_websocket.server.WebSocketServer;
|
import org.java_websocket.server.WebSocketServer;
|
||||||
@@ -18,32 +20,17 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* 游戏 WebSocket 服务器
|
* 游戏 WebSocket 服务器
|
||||||
*
|
*
|
||||||
* 处理客户端连接和消息,协调房间管理和游戏实例。
|
* 处理客户端连接和消息,协调房间管理和游戏实例。
|
||||||
* 主要功能:
|
|
||||||
* - 房间创建、加入、离开
|
|
||||||
* - 玩家准备和游戏开始
|
|
||||||
* - 玩家输入处理
|
|
||||||
* - 游戏状态广播
|
|
||||||
*/
|
*/
|
||||||
public class GameWebSocketServer extends WebSocketServer {
|
public class GameWebSocketServer extends WebSocketServer {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class);
|
private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class);
|
||||||
/** JSON 序列化工具 */
|
|
||||||
private Gson gson;
|
private Gson gson;
|
||||||
/** 房间管理器 */
|
|
||||||
private RoomManager roomManager;
|
private RoomManager roomManager;
|
||||||
/** 游戏服务 */
|
|
||||||
private GameService gameService;
|
private GameService gameService;
|
||||||
/** WebSocket 连接到玩家ID的映射 */
|
|
||||||
private Map<WebSocket, String> connectionToPlayer;
|
private Map<WebSocket, String> connectionToPlayer;
|
||||||
/** 玩家ID到WebSocket连接的映射 */
|
|
||||||
private Map<String, WebSocket> playerToConnection;
|
private Map<String, WebSocket> playerToConnection;
|
||||||
/** 房间列表广播定时器 */
|
|
||||||
private Timer roomListTimer;
|
private Timer roomListTimer;
|
||||||
|
private Map<String, StateSyncSystem> stateSyncSystems = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param port 监听端口号
|
|
||||||
*/
|
|
||||||
public GameWebSocketServer(int port) {
|
public GameWebSocketServer(int port) {
|
||||||
super(new InetSocketAddress(port));
|
super(new InetSocketAddress(port));
|
||||||
this.gson = new Gson();
|
this.gson = new Gson();
|
||||||
@@ -53,19 +40,11 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
this.gameService = new GameService(this::broadcastGameState);
|
this.gameService = new GameService(this::broadcastGameState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 新连接建立时的回调
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||||
logger.info("New connection: {}", conn.getRemoteSocketAddress());
|
logger.info("New connection: {}", conn.getRemoteSocketAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接关闭时的回调
|
|
||||||
*
|
|
||||||
* 清理玩家数据并处理离开房间
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||||
logger.info("Connection closed: {}", conn.getRemoteSocketAddress());
|
logger.info("Connection closed: {}", conn.getRemoteSocketAddress());
|
||||||
@@ -76,11 +55,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 收到消息时的回调
|
|
||||||
*
|
|
||||||
* 根据消息类型分发到对应的处理方法
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(WebSocket conn, String message) {
|
public void onMessage(WebSocket conn, String message) {
|
||||||
try {
|
try {
|
||||||
@@ -117,19 +91,11 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发生错误时的回调
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(WebSocket conn, Exception ex) {
|
public void onError(WebSocket conn, Exception ex) {
|
||||||
logger.error("WebSocket error on connection {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex);
|
logger.error("WebSocket error on connection {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器启动时的回调
|
|
||||||
*
|
|
||||||
* 启动房间列表广播定时器
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
logger.info("Game WebSocket Server started on port {}", getPort());
|
logger.info("Game WebSocket Server started on port {}", getPort());
|
||||||
@@ -142,9 +108,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}, 0, 2000);
|
}, 0, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理创建房间请求
|
|
||||||
*/
|
|
||||||
private void handleCreateRoom(WebSocket conn, JsonObject data) {
|
private void handleCreateRoom(WebSocket conn, JsonObject data) {
|
||||||
if (!MessageUtils.hasRequired(data, "playerName")) {
|
if (!MessageUtils.hasRequired(data, "playerName")) {
|
||||||
sendError(conn, "Missing playerName");
|
sendError(conn, "Missing playerName");
|
||||||
@@ -164,9 +127,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
logger.info("Room created: {} by {}", roomId, playerName);
|
logger.info("Room created: {} by {}", roomId, playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理加入房间请求
|
|
||||||
*/
|
|
||||||
private void handleJoinRoom(WebSocket conn, JsonObject data) {
|
private void handleJoinRoom(WebSocket conn, JsonObject data) {
|
||||||
if (!MessageUtils.hasRequired(data, "roomId", "playerName")) {
|
if (!MessageUtils.hasRequired(data, "roomId", "playerName")) {
|
||||||
sendError(conn, "Missing roomId or playerName");
|
sendError(conn, "Missing roomId or playerName");
|
||||||
@@ -197,35 +157,25 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
logger.info("Player {} joined room {}", playerName, roomId);
|
logger.info("Player {} joined room {}", playerName, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过连接处理离开房间
|
|
||||||
*/
|
|
||||||
private void handleLeaveRoomByConn(WebSocket conn) {
|
private void handleLeaveRoomByConn(WebSocket conn) {
|
||||||
String playerId = connectionToPlayer.get(conn);
|
String playerId = connectionToPlayer.get(conn);
|
||||||
if (playerId == null) return;
|
if (playerId == null) return;
|
||||||
handleLeaveRoomByPlayerId(playerId);
|
handleLeaveRoomByPlayerId(playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过玩家ID处理离开房间
|
|
||||||
*/
|
|
||||||
private void handleLeaveRoomByPlayerId(String playerId) {
|
private void handleLeaveRoomByPlayerId(String playerId) {
|
||||||
Room room = roomManager.leaveRoom(playerId);
|
Room room = roomManager.leaveRoom(playerId);
|
||||||
if (room == null) {
|
if (room == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Room updatedRoom = roomManager.getRoom(room.getId());
|
Room updatedRoom = roomManager.getRoom(room.getId());
|
||||||
if (updatedRoom == null) {
|
if (updatedRoom == null) {
|
||||||
gameService.stopGame(room.getId());
|
gameService.stopGame(room.getId());
|
||||||
|
stateSyncSystems.remove(room.getId());
|
||||||
} else {
|
} else {
|
||||||
broadcastRoomState(updatedRoom);
|
broadcastRoomState(updatedRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理获取房间列表请求
|
|
||||||
*/
|
|
||||||
private void handleRoomList(WebSocket conn) {
|
private void handleRoomList(WebSocket conn) {
|
||||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||||
for (Room room : roomManager.getAvailableRooms()) {
|
for (Room room : roomManager.getAvailableRooms()) {
|
||||||
@@ -236,9 +186,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
|
sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理玩家准备请求
|
|
||||||
*/
|
|
||||||
private void handleReady(WebSocket conn) {
|
private void handleReady(WebSocket conn) {
|
||||||
String playerId = connectionToPlayer.get(conn);
|
String playerId = connectionToPlayer.get(conn);
|
||||||
if (playerId == null) return;
|
if (playerId == null) return;
|
||||||
@@ -246,16 +193,13 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||||
if (room == null) return;
|
if (room == null) return;
|
||||||
|
|
||||||
Player player = room.getPlayer(playerId);
|
PlayerInfo player = room.getPlayer(playerId);
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.setReady(!player.isReady());
|
player.setReady(!player.isReady());
|
||||||
broadcastRoomState(room);
|
broadcastRoomState(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理开始游戏请求
|
|
||||||
*/
|
|
||||||
private void handleStartGame(WebSocket conn) {
|
private void handleStartGame(WebSocket conn) {
|
||||||
String playerId = connectionToPlayer.get(conn);
|
String playerId = connectionToPlayer.get(conn);
|
||||||
if (playerId == null) return;
|
if (playerId == null) return;
|
||||||
@@ -269,11 +213,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动游戏
|
|
||||||
*
|
|
||||||
* @param room 游戏房间
|
|
||||||
*/
|
|
||||||
private void startGame(Room room) {
|
private void startGame(Room room) {
|
||||||
Map<String, Map<String, Object>> playerInitData = gameService.startGame(room);
|
Map<String, Map<String, Object>> playerInitData = gameService.startGame(room);
|
||||||
|
|
||||||
@@ -285,11 +224,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理玩家输入
|
|
||||||
*
|
|
||||||
* 接收并处理玩家的移动、射击、武器切换等输入
|
|
||||||
*/
|
|
||||||
private void handlePlayerInput(WebSocket conn, JsonObject data) {
|
private void handlePlayerInput(WebSocket conn, JsonObject data) {
|
||||||
String playerId = connectionToPlayer.get(conn);
|
String playerId = connectionToPlayer.get(conn);
|
||||||
if (playerId == null) return;
|
if (playerId == null) return;
|
||||||
@@ -311,21 +245,16 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
firing, weaponIndex, seq, grenadeCharge, grenadeReleased);
|
firing, weaponIndex, seq, grenadeCharge, grenadeReleased);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void broadcastGameState(String roomId, ECSWorld world) {
|
||||||
* 广播游戏状态给房间内所有玩家
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID
|
|
||||||
* @param world 游戏世界
|
|
||||||
*/
|
|
||||||
private void broadcastGameState(String roomId, GameWorld world) {
|
|
||||||
Room room = roomManager.getRoom(roomId);
|
Room room = roomManager.getRoom(roomId);
|
||||||
if (room == null) return;
|
if (room == null) return;
|
||||||
|
|
||||||
Map<String, Object> state = null;
|
StateSyncSystem syncSystem = stateSyncSystems.computeIfAbsent(roomId, id -> new StateSyncSystem());
|
||||||
|
|
||||||
synchronized (world.getLock()) {
|
synchronized (world.getLock()) {
|
||||||
for (Player player : room.getPlayers()) {
|
for (PlayerInfo playerInfo : room.getPlayers()) {
|
||||||
state = world.buildGameState(player.getId());
|
Map<String, Object> state = syncSystem.buildGameState(world, playerInfo.getId());
|
||||||
WebSocket pConn = playerToConnection.get(player.getId());
|
WebSocket pConn = playerToConnection.get(playerInfo.getId());
|
||||||
if (pConn != null && pConn.isOpen()) {
|
if (pConn != null && pConn.isOpen()) {
|
||||||
sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
|
sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
|
||||||
}
|
}
|
||||||
@@ -333,11 +262,8 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播房间状态给房间内所有玩家
|
|
||||||
*/
|
|
||||||
private void broadcastRoomState(Room room) {
|
private void broadcastRoomState(Room room) {
|
||||||
for (Player player : room.getPlayers()) {
|
for (PlayerInfo player : room.getPlayers()) {
|
||||||
WebSocket pConn = playerToConnection.get(player.getId());
|
WebSocket pConn = playerToConnection.get(player.getId());
|
||||||
if (pConn != null && pConn.isOpen()) {
|
if (pConn != null && pConn.isOpen()) {
|
||||||
sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId()));
|
sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId()));
|
||||||
@@ -345,9 +271,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播房间列表给所有未加入房间的连接
|
|
||||||
*/
|
|
||||||
private void broadcastRoomList() {
|
private void broadcastRoomList() {
|
||||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||||
for (Room room : roomManager.getAvailableRooms()) {
|
for (Room room : roomManager.getAvailableRooms()) {
|
||||||
@@ -363,13 +286,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息给指定连接
|
|
||||||
*
|
|
||||||
* @param conn WebSocket 连接
|
|
||||||
* @param type 消息类型
|
|
||||||
* @param data 消息数据
|
|
||||||
*/
|
|
||||||
private void sendToConnection(WebSocket conn, String type, Object data) {
|
private void sendToConnection(WebSocket conn, String type, Object data) {
|
||||||
if (conn != null && conn.isOpen()) {
|
if (conn != null && conn.isOpen()) {
|
||||||
Map<String, Object> msg = new LinkedHashMap<>();
|
Map<String, Object> msg = new LinkedHashMap<>();
|
||||||
@@ -379,12 +295,6 @@ public class GameWebSocketServer extends WebSocketServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送错误消息
|
|
||||||
*
|
|
||||||
* @param conn WebSocket 连接
|
|
||||||
* @param message 错误消息
|
|
||||||
*/
|
|
||||||
private void sendError(WebSocket conn, String message) {
|
private void sendError(WebSocket conn, String message) {
|
||||||
Map<String, Object> data = new LinkedHashMap<>();
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
data.put("message", message);
|
data.put("message", message);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.zombie.game.server;
|
package com.zombie.game.server;
|
||||||
|
|
||||||
import com.zombie.game.model.Player;
|
import com.zombie.game.model.PlayerInfo;
|
||||||
import com.zombie.game.model.Room;
|
import com.zombie.game.model.Room;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -29,7 +29,7 @@ public class RoomManager {
|
|||||||
*/
|
*/
|
||||||
public void addRoom(String roomId, Room room) {
|
public void addRoom(String roomId, Room room) {
|
||||||
rooms.put(roomId, room);
|
rooms.put(roomId, room);
|
||||||
for (Player player : room.getPlayers()) {
|
for (PlayerInfo player : room.getPlayers()) {
|
||||||
playerToRoom.put(player.getId(), roomId);
|
playerToRoom.put(player.getId(), roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ public class RoomManager {
|
|||||||
public void removeRoom(String roomId) {
|
public void removeRoom(String roomId) {
|
||||||
Room room = rooms.remove(roomId);
|
Room room = rooms.remove(roomId);
|
||||||
if (room != null) {
|
if (room != null) {
|
||||||
for (Player player : room.getPlayers()) {
|
for (PlayerInfo player : room.getPlayers()) {
|
||||||
playerToRoom.remove(player.getId());
|
playerToRoom.remove(player.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() 显式调用
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,9 +33,6 @@ public class TemplateManager {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载所有模板
|
|
||||||
*/
|
|
||||||
private void loadAll() {
|
private void loadAll() {
|
||||||
loadZombies();
|
loadZombies();
|
||||||
loadWeapons();
|
loadWeapons();
|
||||||
@@ -82,6 +79,7 @@ public class TemplateManager {
|
|||||||
WeaponTemplate t = new WeaponTemplate(
|
WeaponTemplate t = new WeaponTemplate(
|
||||||
node.get("id").asText(),
|
node.get("id").asText(),
|
||||||
node.get("name").asText(),
|
node.get("name").asText(),
|
||||||
|
node.has("category") ? node.get("category").asText() : "firearm",
|
||||||
node.get("damage").asInt(),
|
node.get("damage").asInt(),
|
||||||
node.get("fireRate").asLong(),
|
node.get("fireRate").asLong(),
|
||||||
node.get("pelletCount").asInt(),
|
node.get("pelletCount").asInt(),
|
||||||
@@ -91,7 +89,15 @@ public class TemplateManager {
|
|||||||
node.get("maxAmmo").asInt(),
|
node.get("maxAmmo").asInt(),
|
||||||
node.get("chargeable").asBoolean(),
|
node.get("chargeable").asBoolean(),
|
||||||
node.get("explosive").asBoolean(),
|
node.get("explosive").asBoolean(),
|
||||||
(float) node.get("explosionRadius").asDouble()
|
(float) node.get("explosionRadius").asDouble(),
|
||||||
|
node.has("fireZoneRadius") ? (float) node.get("fireZoneRadius").asDouble() : 0,
|
||||||
|
node.has("fireZoneDamage") ? (float) node.get("fireZoneDamage").asDouble() : 0,
|
||||||
|
node.has("fireZoneDuration") ? (float) node.get("fireZoneDuration").asDouble() : 0,
|
||||||
|
node.has("turretHealth") ? (float) node.get("turretHealth").asDouble() : 0,
|
||||||
|
node.has("turretRange") ? (float) node.get("turretRange").asDouble() : 0,
|
||||||
|
node.has("turretFireRate") ? node.get("turretFireRate").asLong() : 0,
|
||||||
|
node.has("turretDamage") ? node.get("turretDamage").asInt() : 0,
|
||||||
|
node.has("turretBulletSpeed") ? (float) node.get("turretBulletSpeed").asDouble() : 0
|
||||||
);
|
);
|
||||||
weaponTemplates.put(t.getId(), t);
|
weaponTemplates.put(t.getId(), t);
|
||||||
}
|
}
|
||||||
@@ -137,9 +143,6 @@ public class TemplateManager {
|
|||||||
return playerTemplate;
|
return playerTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取武器列表的索引位置
|
|
||||||
*/
|
|
||||||
public int getWeaponIndex(String id) {
|
public int getWeaponIndex(String id) {
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (String key : weaponTemplates.keySet()) {
|
for (String key : weaponTemplates.keySet()) {
|
||||||
@@ -149,9 +152,6 @@ public class TemplateManager {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据索引获取武器ID
|
|
||||||
*/
|
|
||||||
public String getWeaponId(int index) {
|
public String getWeaponId(int index) {
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (String key : weaponTemplates.keySet()) {
|
for (String key : weaponTemplates.keySet()) {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import lombok.Getter;
|
|||||||
* 武器类型模板
|
* 武器类型模板
|
||||||
*
|
*
|
||||||
* 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。
|
* 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。
|
||||||
|
* 支持三种类别:firearm(枪械)、thrown(投掷)、placed(放置)
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class WeaponTemplate {
|
public class WeaponTemplate {
|
||||||
private final String id;
|
private final String id;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final String category; // firearm, thrown, placed
|
||||||
private final int damage;
|
private final int damage;
|
||||||
private final long fireRate;
|
private final long fireRate;
|
||||||
private final int pelletCount;
|
private final int pelletCount;
|
||||||
@@ -22,12 +24,28 @@ public class WeaponTemplate {
|
|||||||
private final boolean explosive;
|
private final boolean explosive;
|
||||||
private final float explosionRadius;
|
private final float explosionRadius;
|
||||||
|
|
||||||
public WeaponTemplate(String id, String name, int damage, long fireRate,
|
// Thrown weapon (molotov) fields
|
||||||
|
private final float fireZoneRadius;
|
||||||
|
private final float fireZoneDamage;
|
||||||
|
private final float fireZoneDuration;
|
||||||
|
|
||||||
|
// Placed weapon (turret) fields
|
||||||
|
private final float turretHealth;
|
||||||
|
private final float turretRange;
|
||||||
|
private final long turretFireRate;
|
||||||
|
private final int turretDamage;
|
||||||
|
private final float turretBulletSpeed;
|
||||||
|
|
||||||
|
public WeaponTemplate(String id, String name, String category, int damage, long fireRate,
|
||||||
int pelletCount, float spread, float bulletSpeed,
|
int pelletCount, float spread, float bulletSpeed,
|
||||||
float range, int maxAmmo, boolean chargeable,
|
float range, int maxAmmo, boolean chargeable,
|
||||||
boolean explosive, float explosionRadius) {
|
boolean explosive, float explosionRadius,
|
||||||
|
float fireZoneRadius, float fireZoneDamage, float fireZoneDuration,
|
||||||
|
float turretHealth, float turretRange, long turretFireRate,
|
||||||
|
int turretDamage, float turretBulletSpeed) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.category = category;
|
||||||
this.damage = damage;
|
this.damage = damage;
|
||||||
this.fireRate = fireRate;
|
this.fireRate = fireRate;
|
||||||
this.pelletCount = pelletCount;
|
this.pelletCount = pelletCount;
|
||||||
@@ -38,5 +56,13 @@ public class WeaponTemplate {
|
|||||||
this.chargeable = chargeable;
|
this.chargeable = chargeable;
|
||||||
this.explosive = explosive;
|
this.explosive = explosive;
|
||||||
this.explosionRadius = explosionRadius;
|
this.explosionRadius = explosionRadius;
|
||||||
|
this.fireZoneRadius = fireZoneRadius;
|
||||||
|
this.fireZoneDamage = fireZoneDamage;
|
||||||
|
this.fireZoneDuration = fireZoneDuration;
|
||||||
|
this.turretHealth = turretHealth;
|
||||||
|
this.turretRange = turretRange;
|
||||||
|
this.turretFireRate = turretFireRate;
|
||||||
|
this.turretDamage = turretDamage;
|
||||||
|
this.turretBulletSpeed = turretBulletSpeed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{
|
{
|
||||||
"id": "pistol",
|
"id": "pistol",
|
||||||
"name": "手枪",
|
"name": "手枪",
|
||||||
|
"category": "firearm",
|
||||||
"damage": 50,
|
"damage": 50,
|
||||||
"fireRate": 400,
|
"fireRate": 400,
|
||||||
"pelletCount": 1,
|
"pelletCount": 1,
|
||||||
@@ -12,11 +13,20 @@
|
|||||||
"maxAmmo": 2147483647,
|
"maxAmmo": 2147483647,
|
||||||
"chargeable": false,
|
"chargeable": false,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosionRadius": 0
|
"explosionRadius": 0,
|
||||||
|
"fireZoneRadius": 0,
|
||||||
|
"fireZoneDamage": 0,
|
||||||
|
"fireZoneDuration": 0,
|
||||||
|
"turretHealth": 0,
|
||||||
|
"turretRange": 0,
|
||||||
|
"turretFireRate": 0,
|
||||||
|
"turretDamage": 0,
|
||||||
|
"turretBulletSpeed": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "machine_gun",
|
"id": "machine_gun",
|
||||||
"name": "机枪",
|
"name": "机关枪",
|
||||||
|
"category": "firearm",
|
||||||
"damage": 50,
|
"damage": 50,
|
||||||
"fireRate": 100,
|
"fireRate": 100,
|
||||||
"pelletCount": 1,
|
"pelletCount": 1,
|
||||||
@@ -26,11 +36,20 @@
|
|||||||
"maxAmmo": 100,
|
"maxAmmo": 100,
|
||||||
"chargeable": false,
|
"chargeable": false,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosionRadius": 0
|
"explosionRadius": 0,
|
||||||
|
"fireZoneRadius": 0,
|
||||||
|
"fireZoneDamage": 0,
|
||||||
|
"fireZoneDuration": 0,
|
||||||
|
"turretHealth": 0,
|
||||||
|
"turretRange": 0,
|
||||||
|
"turretFireRate": 0,
|
||||||
|
"turretDamage": 0,
|
||||||
|
"turretBulletSpeed": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "shotgun",
|
"id": "shotgun",
|
||||||
"name": "霰弹枪",
|
"name": "散弹枪",
|
||||||
|
"category": "firearm",
|
||||||
"damage": 50,
|
"damage": 50,
|
||||||
"fireRate": 800,
|
"fireRate": 800,
|
||||||
"pelletCount": 10,
|
"pelletCount": 10,
|
||||||
@@ -40,11 +59,20 @@
|
|||||||
"maxAmmo": 20,
|
"maxAmmo": 20,
|
||||||
"chargeable": false,
|
"chargeable": false,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosionRadius": 0
|
"explosionRadius": 0,
|
||||||
|
"fireZoneRadius": 0,
|
||||||
|
"fireZoneDamage": 0,
|
||||||
|
"fireZoneDuration": 0,
|
||||||
|
"turretHealth": 0,
|
||||||
|
"turretRange": 0,
|
||||||
|
"turretFireRate": 0,
|
||||||
|
"turretDamage": 0,
|
||||||
|
"turretBulletSpeed": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grenade",
|
"id": "grenade",
|
||||||
"name": "手榴弹",
|
"name": "手榴弹",
|
||||||
|
"category": "thrown",
|
||||||
"damage": 120,
|
"damage": 120,
|
||||||
"fireRate": 1500,
|
"fireRate": 1500,
|
||||||
"pelletCount": 1,
|
"pelletCount": 1,
|
||||||
@@ -54,7 +82,84 @@
|
|||||||
"maxAmmo": 10,
|
"maxAmmo": 10,
|
||||||
"chargeable": true,
|
"chargeable": true,
|
||||||
"explosive": true,
|
"explosive": true,
|
||||||
"explosionRadius": 3
|
"explosionRadius": 3,
|
||||||
|
"fireZoneRadius": 0,
|
||||||
|
"fireZoneDamage": 0,
|
||||||
|
"fireZoneDuration": 0,
|
||||||
|
"turretHealth": 0,
|
||||||
|
"turretRange": 0,
|
||||||
|
"turretFireRate": 0,
|
||||||
|
"turretDamage": 0,
|
||||||
|
"turretBulletSpeed": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "molotov",
|
||||||
|
"name": "燃烧瓶",
|
||||||
|
"category": "thrown",
|
||||||
|
"damage": 80,
|
||||||
|
"fireRate": 2000,
|
||||||
|
"pelletCount": 1,
|
||||||
|
"spread": 0,
|
||||||
|
"bulletSpeed": 12,
|
||||||
|
"range": 12,
|
||||||
|
"maxAmmo": 5,
|
||||||
|
"chargeable": true,
|
||||||
|
"explosive": true,
|
||||||
|
"explosionRadius": 2.0,
|
||||||
|
"fireZoneRadius": 2.5,
|
||||||
|
"fireZoneDamage": 10,
|
||||||
|
"fireZoneDuration": 3.0,
|
||||||
|
"turretHealth": 0,
|
||||||
|
"turretRange": 0,
|
||||||
|
"turretFireRate": 0,
|
||||||
|
"turretDamage": 0,
|
||||||
|
"turretBulletSpeed": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nut_wall",
|
||||||
|
"name": "坚果墙体",
|
||||||
|
"category": "placed",
|
||||||
|
"damage": 0,
|
||||||
|
"fireRate": 1000,
|
||||||
|
"pelletCount": 0,
|
||||||
|
"spread": 0,
|
||||||
|
"bulletSpeed": 0,
|
||||||
|
"range": 0,
|
||||||
|
"maxAmmo": 3,
|
||||||
|
"chargeable": false,
|
||||||
|
"explosive": false,
|
||||||
|
"explosionRadius": 0,
|
||||||
|
"fireZoneRadius": 0,
|
||||||
|
"fireZoneDamage": 0,
|
||||||
|
"fireZoneDuration": 0,
|
||||||
|
"turretHealth": 20,
|
||||||
|
"turretRange": 0,
|
||||||
|
"turretFireRate": 0,
|
||||||
|
"turretDamage": 0,
|
||||||
|
"turretBulletSpeed": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "auto_turret",
|
||||||
|
"name": "自动机枪塔",
|
||||||
|
"category": "placed",
|
||||||
|
"damage": 0,
|
||||||
|
"fireRate": 2000,
|
||||||
|
"pelletCount": 0,
|
||||||
|
"spread": 0,
|
||||||
|
"bulletSpeed": 0,
|
||||||
|
"range": 0,
|
||||||
|
"maxAmmo": 2,
|
||||||
|
"chargeable": false,
|
||||||
|
"explosive": false,
|
||||||
|
"explosionRadius": 0,
|
||||||
|
"fireZoneRadius": 0,
|
||||||
|
"fireZoneDamage": 0,
|
||||||
|
"fireZoneDuration": 0,
|
||||||
|
"turretHealth": 100,
|
||||||
|
"turretRange": 6.0,
|
||||||
|
"turretFireRate": 300,
|
||||||
|
"turretDamage": 30,
|
||||||
|
"turretBulletSpeed": 20
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ export class GameEngine {
|
|||||||
[WEAPONS.PISTOL]: Infinity, // 手枪无限弹药
|
[WEAPONS.PISTOL]: Infinity, // 手枪无限弹药
|
||||||
[WEAPONS.MACHINE_GUN]: 100,
|
[WEAPONS.MACHINE_GUN]: 100,
|
||||||
[WEAPONS.SHOTGUN]: 20,
|
[WEAPONS.SHOTGUN]: 20,
|
||||||
[WEAPONS.GRENADE]: 10
|
[WEAPONS.GRENADE]: 10,
|
||||||
|
[WEAPONS.MOLOTOV]: 5,
|
||||||
|
[WEAPONS.NUT_WALL]: 3,
|
||||||
|
[WEAPONS.AUTO_TURRET]: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手雷蓄力相关
|
// 手雷蓄力相关
|
||||||
@@ -217,8 +220,8 @@ export class GameEngine {
|
|||||||
const localPlayer = this.players.get(this.localPlayerId)
|
const localPlayer = this.players.get(this.localPlayerId)
|
||||||
if (localPlayer) {
|
if (localPlayer) {
|
||||||
this.scene.updateCamera(localPlayer.x, localPlayer.y)
|
this.scene.updateCamera(localPlayer.x, localPlayer.y)
|
||||||
// 手雷瞄准指示器
|
// 投掷武器瞄准指示器(手雷/燃烧瓶)
|
||||||
if (this.isChargingGrenade && this.currentWeaponIndex === 3) {
|
if (this.isChargingGrenade && (this.currentWeaponIndex === 3 || this.currentWeaponIndex === 4)) {
|
||||||
this.scene.showGrenadeTarget(localPlayer.x, localPlayer.y,
|
this.scene.showGrenadeTarget(localPlayer.x, localPlayer.y,
|
||||||
this.input.mouse.groundX || 0, this.input.mouse.groundY || 0,
|
this.input.mouse.groundX || 0, this.input.mouse.groundY || 0,
|
||||||
this.grenadeChargePercent)
|
this.grenadeChargePercent)
|
||||||
@@ -260,11 +263,16 @@ export class GameEngine {
|
|||||||
// 应用本地预测(客户端预测)
|
// 应用本地预测(客户端预测)
|
||||||
this._applyLocalPrediction(inputState)
|
this._applyLocalPrediction(inputState)
|
||||||
|
|
||||||
// 添加手雷相关数据到输入状态
|
// 添加蓄力武器相关数据到输入状态(手雷/燃烧瓶)
|
||||||
inputState.grenadeCharge = this.grenadeChargePercent
|
inputState.grenadeCharge = this.grenadeChargePercent
|
||||||
inputState.grenadeReleased = this.grenadeReleased
|
inputState.grenadeReleased = this.grenadeReleased
|
||||||
// 手雷释放前不发火
|
// 蓄力武器释放前不发火
|
||||||
inputState.firing = this.currentWeaponIndex === 3 ? false : inputState.firing
|
const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
|
||||||
|
const currentWeaponId = weaponList7[this.currentWeaponIndex]
|
||||||
|
const currentConfig = WEAPON_CONFIG[currentWeaponId]
|
||||||
|
if (currentConfig && currentConfig.chargeable) {
|
||||||
|
inputState.firing = false
|
||||||
|
}
|
||||||
|
|
||||||
// 保存输入以便进行客户端预测校正
|
// 保存输入以便进行客户端预测校正
|
||||||
this.pendingInputs.push(inputState)
|
this.pendingInputs.push(inputState)
|
||||||
@@ -287,10 +295,11 @@ export class GameEngine {
|
|||||||
* 手雷需要长按鼠标蓄力,松开释放
|
* 手雷需要长按鼠标蓄力,松开释放
|
||||||
*/
|
*/
|
||||||
_handleGrenadeCharge(inputState) {
|
_handleGrenadeCharge(inputState) {
|
||||||
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE]
|
const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
|
||||||
const currentWeapon = weaponList[this.currentWeaponIndex]
|
const currentWeapon = weaponList7[this.currentWeaponIndex]
|
||||||
|
const config = WEAPON_CONFIG[currentWeapon]
|
||||||
|
|
||||||
if (currentWeapon === WEAPONS.GRENADE && WEAPON_CONFIG[WEAPONS.GRENADE].chargeable) {
|
if (config && config.chargeable) {
|
||||||
// 开始蓄力
|
// 开始蓄力
|
||||||
if (inputState.firing && !this.isChargingGrenade) {
|
if (inputState.firing && !this.isChargingGrenade) {
|
||||||
this.isChargingGrenade = true
|
this.isChargingGrenade = true
|
||||||
@@ -298,13 +307,13 @@ export class GameEngine {
|
|||||||
} else if (inputState.firing && this.isChargingGrenade) {
|
} else if (inputState.firing && this.isChargingGrenade) {
|
||||||
// 蓄力中,计算蓄力百分比
|
// 蓄力中,计算蓄力百分比
|
||||||
const elapsed = Date.now() - this.grenadeChargeStart
|
const elapsed = Date.now() - this.grenadeChargeStart
|
||||||
this.grenadeChargePercent = Math.min(1, elapsed / WEAPON_CONFIG[WEAPONS.GRENADE].maxCharge)
|
this.grenadeChargePercent = Math.min(1, elapsed / config.maxCharge)
|
||||||
} else if (!inputState.firing && this.isChargingGrenade) {
|
} else if (!inputState.firing && this.isChargingGrenade) {
|
||||||
// 释放手雷
|
// 释放
|
||||||
this.grenadeReleased = true
|
this.grenadeReleased = true
|
||||||
this.isChargingGrenade = false
|
this.isChargingGrenade = false
|
||||||
const elapsed = Date.now() - this.grenadeChargeStart
|
const elapsed = Date.now() - this.grenadeChargeStart
|
||||||
this.grenadeChargePercent = Math.min(1, elapsed / WEAPON_CONFIG[WEAPONS.GRENADE].maxCharge)
|
this.grenadeChargePercent = Math.min(1, elapsed / config.maxCharge)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.isChargingGrenade = false
|
this.isChargingGrenade = false
|
||||||
@@ -417,9 +426,9 @@ export class GameEngine {
|
|||||||
// 新增子弹
|
// 新增子弹
|
||||||
this.bullets.set(bs.id, { ...bs })
|
this.bullets.set(bs.id, { ...bs })
|
||||||
this.scene.addBullet(bs)
|
this.scene.addBullet(bs)
|
||||||
// 枪口火焰特效(手雷除外)
|
// 枪口火焰特效(投掷类武器除外)
|
||||||
const player = this.players.get(bs.ownerId)
|
const player = this.players.get(bs.ownerId)
|
||||||
if (player && bs.weapon !== WEAPONS.GRENADE) {
|
if (player && bs.weaponIndex !== 3 && bs.weaponIndex !== 4) {
|
||||||
this.scene.addMuzzleFlash(player.x, player.y, bs.angle || 0)
|
this.scene.addMuzzleFlash(player.x, player.y, bs.angle || 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -556,22 +556,30 @@ export class GameScene {
|
|||||||
*/
|
*/
|
||||||
addBullet(bullet) {
|
addBullet(bullet) {
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
const isGrenade = bullet.weapon === WEAPONS.GRENADE
|
// weaponIndex: 0=pistol, 1=machinegun, 2=shotgun, 3=grenade, 4=molotov
|
||||||
|
const wIdx = bullet.weaponIndex
|
||||||
|
const isGrenade = wIdx === 3
|
||||||
|
const isMolotov = wIdx === 4
|
||||||
|
const isThrown = isGrenade || isMolotov
|
||||||
const z = bullet.z || 0.5
|
const z = bullet.z || 0.5
|
||||||
|
|
||||||
if (isGrenade) {
|
if (isThrown) {
|
||||||
// 手雷:球体 + 发光 + 拖尾
|
// 投掷物:球体 + 发光 + 拖尾
|
||||||
|
const bodyColor = isMolotov ? 0xff6600 : 0x44ff44
|
||||||
|
const glowColor = isMolotov ? 0xff4400 : 0x22cc22
|
||||||
|
const trailColor = isMolotov ? 0xff8844 : 0x88ff88
|
||||||
|
|
||||||
const geo = new THREE.SphereGeometry(0.12, 8, 8)
|
const geo = new THREE.SphereGeometry(0.12, 8, 8)
|
||||||
const mat = new THREE.MeshBasicMaterial({ color: 0x44ff44 })
|
const mat = new THREE.MeshBasicMaterial({ color: bodyColor })
|
||||||
const mesh = new THREE.Mesh(geo, mat)
|
const mesh = new THREE.Mesh(geo, mat)
|
||||||
mesh.position.set(bullet.x, z, bullet.y)
|
mesh.position.set(bullet.x, z, bullet.y)
|
||||||
group.add(mesh)
|
group.add(mesh)
|
||||||
|
|
||||||
const glowGeo = new THREE.SphereGeometry(0.18, 8, 8)
|
const glowGeo = new THREE.SphereGeometry(0.18, 8, 8)
|
||||||
const glowMat = new THREE.MeshBasicMaterial({
|
const glowMat = new THREE.MeshBasicMaterial({
|
||||||
color: 0x22cc22,
|
color: glowColor,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.4
|
opacity: 0.5
|
||||||
})
|
})
|
||||||
const glow = new THREE.Mesh(glowGeo, glowMat)
|
const glow = new THREE.Mesh(glowGeo, glowMat)
|
||||||
glow.position.set(bullet.x, z, bullet.y)
|
glow.position.set(bullet.x, z, bullet.y)
|
||||||
@@ -580,7 +588,7 @@ export class GameScene {
|
|||||||
// 拖尾线
|
// 拖尾线
|
||||||
const trailGeo = new THREE.BufferGeometry()
|
const trailGeo = new THREE.BufferGeometry()
|
||||||
const trailMat = new THREE.LineBasicMaterial({
|
const trailMat = new THREE.LineBasicMaterial({
|
||||||
color: 0x88ff88,
|
color: trailColor,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
})
|
})
|
||||||
@@ -598,14 +606,8 @@ export class GameScene {
|
|||||||
const angle = bullet.angle || 0
|
const angle = bullet.angle || 0
|
||||||
|
|
||||||
// 根据武器类型调整大小
|
// 根据武器类型调整大小
|
||||||
switch (bullet.weapon) {
|
if (wIdx === 1) bulletSize = 0.05 // 机枪
|
||||||
case WEAPONS.MACHINE_GUN:
|
else if (wIdx === 2) bulletSize = 0.04 // 霰弹枪
|
||||||
bulletSize = 0.05
|
|
||||||
break
|
|
||||||
case WEAPONS.SHOTGUN:
|
|
||||||
bulletSize = 0.04
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拖尾
|
// 拖尾
|
||||||
const trailLength = 2.5
|
const trailLength = 2.5
|
||||||
@@ -651,9 +653,10 @@ export class GameScene {
|
|||||||
x: bullet.x,
|
x: bullet.x,
|
||||||
y: bullet.y,
|
y: bullet.y,
|
||||||
z: z,
|
z: z,
|
||||||
weapon: bullet.weapon,
|
weaponIndex: wIdx,
|
||||||
angle: bullet.angle || 0,
|
angle: bullet.angle || 0,
|
||||||
isGrenade
|
isGrenade,
|
||||||
|
isMolotov
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,7 +698,7 @@ export class GameScene {
|
|||||||
child.position.z = y
|
child.position.z = y
|
||||||
}
|
}
|
||||||
// 更新拖尾位置
|
// 更新拖尾位置
|
||||||
if (child.isLine && !bullet.isGrenade) {
|
if (child.isLine && !bullet.isGrenade && !bullet.isMolotov) {
|
||||||
const trailLength = 2.5
|
const trailLength = 2.5
|
||||||
const positions = child.geometry.attributes.position.array
|
const positions = child.geometry.attributes.position.array
|
||||||
positions[0] = x - Math.sin(angle) * trailLength
|
positions[0] = x - Math.sin(angle) * trailLength
|
||||||
@@ -1083,7 +1086,7 @@ export class GameScene {
|
|||||||
|
|
||||||
// 更新子弹拖尾
|
// 更新子弹拖尾
|
||||||
for (const bullet of this.bullets) {
|
for (const bullet of this.bullets) {
|
||||||
if (!bullet.isGrenade && bullet.trail) {
|
if (!bullet.isGrenade && !bullet.isMolotov && bullet.trail) {
|
||||||
const positions = bullet.trail.geometry.attributes.position.array
|
const positions = bullet.trail.geometry.attributes.position.array
|
||||||
positions[0] = bullet.x - Math.sin(bullet.angle) * 1.5
|
positions[0] = bullet.x - Math.sin(bullet.angle) * 1.5
|
||||||
positions[1] = 0.5
|
positions[1] = 0.5
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class HUD {
|
|||||||
this.weaponPanel.className = 'hud-weapon-panel'
|
this.weaponPanel.className = 'hud-weapon-panel'
|
||||||
|
|
||||||
// 武器列表
|
// 武器列表
|
||||||
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE]
|
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
|
||||||
this.weaponSlots = []
|
this.weaponSlots = []
|
||||||
for (let i = 0; i < weaponList.length; i++) {
|
for (let i = 0; i < weaponList.length; i++) {
|
||||||
const slot = document.createElement('div')
|
const slot = document.createElement('div')
|
||||||
@@ -135,7 +135,7 @@ export class HUD {
|
|||||||
* @param {Object} ammo 各武器弹药数
|
* @param {Object} ammo 各武器弹药数
|
||||||
*/
|
*/
|
||||||
updateWeapons(currentIndex, ammo) {
|
updateWeapons(currentIndex, ammo) {
|
||||||
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE]
|
const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET]
|
||||||
for (let i = 0; i < this.weaponSlots.length; i++) {
|
for (let i = 0; i < this.weaponSlots.length; i++) {
|
||||||
const slot = this.weaponSlots[i]
|
const slot = this.weaponSlots[i]
|
||||||
// 高亮当前武器
|
// 高亮当前武器
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export class SettingsUI {
|
|||||||
weapon1: 'Digit1',
|
weapon1: 'Digit1',
|
||||||
weapon2: 'Digit2',
|
weapon2: 'Digit2',
|
||||||
weapon3: 'Digit3',
|
weapon3: 'Digit3',
|
||||||
weapon4: 'Digit4'
|
weapon4: 'Digit4',
|
||||||
|
weapon5: 'Digit5',
|
||||||
|
weapon6: 'Digit6',
|
||||||
|
weapon7: 'Digit7'
|
||||||
}
|
}
|
||||||
this.bindings = { ...this.defaultBindings }
|
this.bindings = { ...this.defaultBindings }
|
||||||
}
|
}
|
||||||
@@ -68,7 +71,10 @@ export class SettingsUI {
|
|||||||
weapon1: 'Weapon 1 (Pistol)',
|
weapon1: 'Weapon 1 (Pistol)',
|
||||||
weapon2: 'Weapon 2 (MG)',
|
weapon2: 'Weapon 2 (MG)',
|
||||||
weapon3: 'Weapon 3 (Shotgun)',
|
weapon3: 'Weapon 3 (Shotgun)',
|
||||||
weapon4: 'Weapon 4 (Grenade)'
|
weapon4: 'Weapon 4 (Grenade)',
|
||||||
|
weapon5: 'Weapon 5 (Molotov)',
|
||||||
|
weapon6: 'Weapon 6 (Wall)',
|
||||||
|
weapon7: 'Weapon 7 (Turret)'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成每种操作的按键配置行
|
// 生成每种操作的按键配置行
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ export const WEAPONS = {
|
|||||||
PISTOL: 'pistol', // 手枪
|
PISTOL: 'pistol', // 手枪
|
||||||
MACHINE_GUN: 'machine_gun', // 机枪
|
MACHINE_GUN: 'machine_gun', // 机枪
|
||||||
SHOTGUN: 'shotgun', // 霰弹枪
|
SHOTGUN: 'shotgun', // 霰弹枪
|
||||||
GRENADE: 'grenade' // 手雷
|
GRENADE: 'grenade', // 手雷
|
||||||
|
MOLOTOV: 'molotov', // 燃烧瓶
|
||||||
|
NUT_WALL: 'nut_wall', // 坚果墙体
|
||||||
|
AUTO_TURRET: 'auto_turret' // 自动机枪塔
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 武器配置 ==========
|
// ========== 武器配置 ==========
|
||||||
@@ -84,7 +87,51 @@ export const WEAPON_CONFIG = {
|
|||||||
auto: false,
|
auto: false,
|
||||||
chargeable: true, // 可蓄力
|
chargeable: true, // 可蓄力
|
||||||
maxCharge: 2000, // 最大蓄力时间(毫秒)
|
maxCharge: 2000, // 最大蓄力时间(毫秒)
|
||||||
explosionRadius: 3 // 爆炸半径
|
explosionRadius: 3 // 爆炸半径
|
||||||
|
},
|
||||||
|
// 燃烧瓶配置
|
||||||
|
[WEAPONS.MOLOTOV]: {
|
||||||
|
name: 'Molotov',
|
||||||
|
damage: 80,
|
||||||
|
fireRate: 2000,
|
||||||
|
ammo: 5,
|
||||||
|
maxAmmo: 5,
|
||||||
|
speed: 12,
|
||||||
|
spread: 0,
|
||||||
|
pellets: 1,
|
||||||
|
range: 12,
|
||||||
|
auto: false,
|
||||||
|
chargeable: true, // 可蓄力
|
||||||
|
maxCharge: 2000,
|
||||||
|
explosionRadius: 2
|
||||||
|
},
|
||||||
|
// 坚果墙体配置
|
||||||
|
[WEAPONS.NUT_WALL]: {
|
||||||
|
name: 'Wall',
|
||||||
|
damage: 0,
|
||||||
|
fireRate: 1000,
|
||||||
|
ammo: 3,
|
||||||
|
maxAmmo: 3,
|
||||||
|
speed: 0,
|
||||||
|
spread: 0,
|
||||||
|
pellets: 0,
|
||||||
|
range: 0,
|
||||||
|
auto: false,
|
||||||
|
chargeable: false
|
||||||
|
},
|
||||||
|
// 自动机枪塔配置
|
||||||
|
[WEAPONS.AUTO_TURRET]: {
|
||||||
|
name: 'Turret',
|
||||||
|
damage: 0,
|
||||||
|
fireRate: 2000,
|
||||||
|
ammo: 2,
|
||||||
|
maxAmmo: 2,
|
||||||
|
speed: 0,
|
||||||
|
spread: 0,
|
||||||
|
pellets: 0,
|
||||||
|
range: 0,
|
||||||
|
auto: false,
|
||||||
|
chargeable: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export class InputManager {
|
|||||||
weapon1: 'Digit1',
|
weapon1: 'Digit1',
|
||||||
weapon2: 'Digit2',
|
weapon2: 'Digit2',
|
||||||
weapon3: 'Digit3',
|
weapon3: 'Digit3',
|
||||||
weapon4: 'Digit4'
|
weapon4: 'Digit4',
|
||||||
|
weapon5: 'Digit5',
|
||||||
|
weapon6: 'Digit6',
|
||||||
|
weapon7: 'Digit7'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 待处理的输入序列(用于客户端预测)
|
// 待处理的输入序列(用于客户端预测)
|
||||||
@@ -100,13 +103,16 @@ export class InputManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前选择的武器
|
* 获取当前选择的武器
|
||||||
* @returns {number} 武器索引(0-3),无选择返回-1
|
* @returns {number} 武器索引(0-6),无选择返回-1
|
||||||
*/
|
*/
|
||||||
getSelectedWeapon() {
|
getSelectedWeapon() {
|
||||||
if (this.keys[this.keyBindings.weapon1]) return 0
|
if (this.keys[this.keyBindings.weapon1]) return 0
|
||||||
if (this.keys[this.keyBindings.weapon2]) return 1
|
if (this.keys[this.keyBindings.weapon2]) return 1
|
||||||
if (this.keys[this.keyBindings.weapon3]) return 2
|
if (this.keys[this.keyBindings.weapon3]) return 2
|
||||||
if (this.keys[this.keyBindings.weapon4]) return 3
|
if (this.keys[this.keyBindings.weapon4]) return 3
|
||||||
|
if (this.keys[this.keyBindings.weapon5]) return 4
|
||||||
|
if (this.keys[this.keyBindings.weapon6]) return 5
|
||||||
|
if (this.keys[this.keyBindings.weapon7]) return 6
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user