From 9fd572c8c41283cd0563785cb0292ff9f4beed6f Mon Sep 17 00:00:00 2001 From: wfz <1040079213@qq.com> Date: Sat, 2 May 2026 18:07:11 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=AF=81esc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/zombie/game/ecs/ECSWorld.java | 429 +++++++++++ .../main/java/com/zombie/game/ecs/System.java | 10 + .../game/ecs/components/BulletData.java | 84 +++ .../zombie/game/ecs/components/Collision.java | 17 + .../zombie/game/ecs/components/Explosive.java | 20 + .../zombie/game/ecs/components/FireZone.java | 42 ++ .../zombie/game/ecs/components/Health.java | 65 ++ .../zombie/game/ecs/components/LootData.java | 17 + .../game/ecs/components/PlayerInput.java | 41 ++ .../zombie/game/ecs/components/Position.java | 41 ++ .../game/ecs/components/RenderInfo.java | 42 ++ .../game/ecs/components/RespawnState.java | 50 ++ .../game/ecs/components/TurretState.java | 37 + .../zombie/game/ecs/components/Velocity.java | 27 + .../game/ecs/components/WallEntity.java | 28 + .../game/ecs/components/WeaponState.java | 124 ++++ .../zombie/game/ecs/components/ZombieAI.java | 46 ++ .../java/com/zombie/game/model/Bullet.java | 173 ----- .../java/com/zombie/game/model/GameWorld.java | 693 ------------------ .../main/java/com/zombie/game/model/Loot.java | 46 -- .../java/com/zombie/game/model/Player.java | 242 ------ .../com/zombie/game/model/PlayerInfo.java | 25 + .../main/java/com/zombie/game/model/Room.java | 30 +- .../java/com/zombie/game/model/Zombie.java | 371 ---------- .../java/com/zombie/game/server/GameLoop.java | 39 +- .../com/zombie/game/server/GameService.java | 136 ++-- .../game/server/GameWebSocketServer.java | 116 +-- .../com/zombie/game/server/RoomManager.java | 6 +- .../game/systems/BulletMovementSystem.java | 108 +++ .../zombie/game/systems/CollisionSystem.java | 181 +++++ .../com/zombie/game/systems/DamageSystem.java | 93 +++ .../zombie/game/systems/DifficultySystem.java | 86 +++ .../zombie/game/systems/FireZoneSystem.java | 49 ++ .../com/zombie/game/systems/LootSystem.java | 54 ++ .../game/systems/PlayerInputSystem.java | 52 ++ .../zombie/game/systems/RespawnSystem.java | 52 ++ .../zombie/game/systems/StateSyncSystem.java | 204 ++++++ .../com/zombie/game/systems/TurretSystem.java | 60 ++ .../game/systems/WeaponFiringSystem.java | 173 +++++ .../game/systems/ZombieDeathSystem.java | 63 ++ .../game/systems/ZombieMovementSystem.java | 315 ++++++++ .../zombie/game/template/TemplateManager.java | 20 +- .../zombie/game/template/WeaponTemplate.java | 30 +- backend/src/main/resources/data/weapons.json | 117 ++- frontend/src/game/engine.js | 37 +- frontend/src/game/scene.js | 41 +- frontend/src/ui/hud.js | 4 +- frontend/src/ui/settings.js | 10 +- frontend/src/utils/constants.js | 51 +- frontend/src/utils/input.js | 10 +- maps/d540209a.json | 672 ++++++++++++++++- 51 files changed, 3659 insertions(+), 1820 deletions(-) create mode 100644 backend/src/main/java/com/zombie/game/ecs/ECSWorld.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/System.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/BulletData.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/Collision.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/Explosive.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/FireZone.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/Health.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/LootData.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/PlayerInput.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/Position.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/RenderInfo.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/RespawnState.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/TurretState.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/Velocity.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/WallEntity.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/WeaponState.java create mode 100644 backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java delete mode 100644 backend/src/main/java/com/zombie/game/model/Bullet.java delete mode 100644 backend/src/main/java/com/zombie/game/model/GameWorld.java delete mode 100644 backend/src/main/java/com/zombie/game/model/Loot.java delete mode 100644 backend/src/main/java/com/zombie/game/model/Player.java create mode 100644 backend/src/main/java/com/zombie/game/model/PlayerInfo.java delete mode 100644 backend/src/main/java/com/zombie/game/model/Zombie.java create mode 100644 backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/CollisionSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/DamageSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/DifficultySystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/FireZoneSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/LootSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/PlayerInputSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/RespawnSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/TurretSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/ZombieDeathSystem.java create mode 100644 backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java diff --git a/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java b/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java new file mode 100644 index 0000000..df40eaf --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java @@ -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 entities = new LinkedHashSet<>(); + /** 玩家实体集合 */ + private final Set players = new LinkedHashSet<>(); + /** 僵尸实体集合 */ + private final Set zombies = new LinkedHashSet<>(); + /** 玩家子弹实体集合 */ + private final Set playerBullets = new LinkedHashSet<>(); + /** 僵尸子弹实体集合 */ + private final Set zombieBullets = new LinkedHashSet<>(); + /** 掉落物实体集合 */ + private final Set loots = new LinkedHashSet<>(); + /** 燃烧区域实体集合 */ + private final Set fireZones = new LinkedHashSet<>(); + /** 机枪塔实体集合 */ + private final Set turrets = new LinkedHashSet<>(); + /** 墙体实体集合(坚果墙、机枪塔) */ + private final Set wallEntities = new LinkedHashSet<>(); + + // ==================== 组件存储 ==================== + /** 实体名称 */ + private final Map entityNames = new HashMap<>(); + /** 位置组件 */ + private final Map positions = new HashMap<>(); + /** 生命值组件 */ + private final Map healths = new HashMap<>(); + /** 碰撞组件 */ + private final Map collisions = new HashMap<>(); + /** 渲染信息组件 */ + private final Map renderInfos = new HashMap<>(); + /** 玩家输入组件 */ + private final Map playerInputs = new HashMap<>(); + /** 武器状态组件 */ + private final Map weaponStates = new HashMap<>(); + /** 僵尸AI组件 */ + private final Map zombieAIs = new HashMap<>(); + /** 速度组件 */ + private final Map velocities = new HashMap<>(); + /** 子弹数据组件 */ + private final Map bulletDatas = new HashMap<>(); + /** 爆炸组件 */ + private final Map explosives = new HashMap<>(); + /** 掉落物数据组件 */ + private final Map lootDatas = new HashMap<>(); + /** 重生状态组件 */ + private final Map respawnStates = new HashMap<>(); + /** 燃烧区域组件 */ + private final Map fireZonesData = new HashMap<>(); + /** 机枪塔状态组件 */ + private final Map turretStates = new HashMap<>(); + /** 墙体实体组件 */ + private final Map wallEntityDatas = new HashMap<>(); + + // ==================== 系统列表 ==================== + private final List 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> explosions = new ArrayList<>(); + /** 本帧移除的玩家子弹ID */ + private final List removedBullets = new ArrayList<>(); + /** 本帧移除的僵尸子弹ID */ + private final List removedZombieBullets = new ArrayList<>(); + + /** 玩家ID到实体ID的映射 */ + private final Map playerIdToEntity = new HashMap<>(); + + /** 僵尸模板缓存 */ + private final Map 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 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 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); + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/System.java b/backend/src/main/java/com/zombie/game/ecs/System.java new file mode 100644 index 0000000..b954fdd --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/System.java @@ -0,0 +1,10 @@ +package com.zombie.game.ecs; + +/** + * ECS 系统接口 + * + * 所有系统实现此接口,在每帧 update 中处理具有特定组件组合的实体。 + */ +public interface System { + void update(float dt, ECSWorld world); +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/BulletData.java b/backend/src/main/java/com/zombie/game/ecs/components/BulletData.java new file mode 100644 index 0000000..dbed5a8 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/BulletData.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/Collision.java b/backend/src/main/java/com/zombie/game/ecs/components/Collision.java new file mode 100644 index 0000000..cb0996a --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/Collision.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/Explosive.java b/backend/src/main/java/com/zombie/game/ecs/components/Explosive.java new file mode 100644 index 0000000..2f2d8ec --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/Explosive.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/FireZone.java b/backend/src/main/java/com/zombie/game/ecs/components/FireZone.java new file mode 100644 index 0000000..83b3473 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/FireZone.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/Health.java b/backend/src/main/java/com/zombie/game/ecs/components/Health.java new file mode 100644 index 0000000..b272af0 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/Health.java @@ -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(); + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/LootData.java b/backend/src/main/java/com/zombie/game/ecs/components/LootData.java new file mode 100644 index 0000000..5920b13 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/LootData.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/PlayerInput.java b/backend/src/main/java/com/zombie/game/ecs/components/PlayerInput.java new file mode 100644 index 0000000..baaa503 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/PlayerInput.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/Position.java b/backend/src/main/java/com/zombie/game/ecs/components/Position.java new file mode 100644 index 0000000..2d2ee11 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/Position.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/RenderInfo.java b/backend/src/main/java/com/zombie/game/ecs/components/RenderInfo.java new file mode 100644 index 0000000..8bd29c1 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/RenderInfo.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/RespawnState.java b/backend/src/main/java/com/zombie/game/ecs/components/RespawnState.java new file mode 100644 index 0000000..c20e1d3 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/RespawnState.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/TurretState.java b/backend/src/main/java/com/zombie/game/ecs/components/TurretState.java new file mode 100644 index 0000000..86b535d --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/TurretState.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/Velocity.java b/backend/src/main/java/com/zombie/game/ecs/components/Velocity.java new file mode 100644 index 0000000..79f412a --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/Velocity.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/WallEntity.java b/backend/src/main/java/com/zombie/game/ecs/components/WallEntity.java new file mode 100644 index 0000000..2250d2c --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/WallEntity.java @@ -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); + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/WeaponState.java b/backend/src/main/java/com/zombie/game/ecs/components/WeaponState.java new file mode 100644 index 0000000..859fce1 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/WeaponState.java @@ -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 WEAPON_TEMPLATES; + static { + List 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; + } +} diff --git a/backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java b/backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java new file mode 100644 index 0000000..2d91ac9 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/model/Bullet.java b/backend/src/main/java/com/zombie/game/model/Bullet.java deleted file mode 100644 index 1d6ff26..0000000 --- a/backend/src/main/java/com/zombie/game/model/Bullet.java +++ /dev/null @@ -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 toStateMap() { - Map 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; - } -} diff --git a/backend/src/main/java/com/zombie/game/model/GameWorld.java b/backend/src/main/java/com/zombie/game/model/GameWorld.java deleted file mode 100644 index 4c87db1..0000000 --- a/backend/src/main/java/com/zombie/game/model/GameWorld.java +++ /dev/null @@ -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 players; - private Map zombies; - private Map bullets; - private Map zombieBullets; - private Map 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> explosions; - private List removedBullets; - private List 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 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 getPlayers() { return players.values(); } - /** 获取所有僵尸 */ - public Collection getZombies() { return zombies.values(); } - /** 获取所有玩家子弹 */ - public Collection getBullets() { return bullets.values(); } - /** 获取所有掉落物 */ - public Collection 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 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 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 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 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 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 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 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 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 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 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 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 fireWeapon(Player player, float aimX, float aimY, float chargePercent) { - List 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 buildGameState(String forPlayerId) { - Map state = new LinkedHashMap<>(); - - List> playerStates = new ArrayList<>(); - for (Player p : players.values()) { - playerStates.add(p.toStateMap()); - } - state.put("players", playerStates); - - List> zombieStates = new ArrayList<>(); - for (Zombie z : zombies.values()) { - zombieStates.add(z.toStateMap()); - } - state.put("zombies", zombieStates); - - List> bulletStates = new ArrayList<>(); - for (Bullet b : bullets.values()) { - bulletStates.add(b.toStateMap()); - } - state.put("bullets", bulletStates); - - List> zombieBulletStates = new ArrayList<>(); - for (Bullet b : zombieBullets.values()) { - zombieBulletStates.add(b.toStateMap()); - } - state.put("zombieBullets", zombieBulletStates); - - List> 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 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; - } -} diff --git a/backend/src/main/java/com/zombie/game/model/Loot.java b/backend/src/main/java/com/zombie/game/model/Loot.java deleted file mode 100644 index 33a7e53..0000000 --- a/backend/src/main/java/com/zombie/game/model/Loot.java +++ /dev/null @@ -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 toStateMap() { - Map map = new LinkedHashMap<>(); - map.put("id", id); - map.put("x", x); - map.put("y", y); - map.put("type", type); - return map; - } -} diff --git a/backend/src/main/java/com/zombie/game/model/Player.java b/backend/src/main/java/com/zombie/game/model/Player.java deleted file mode 100644 index 34ef2a2..0000000 --- a/backend/src/main/java/com/zombie/game/model/Player.java +++ /dev/null @@ -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 WEAPON_TEMPLATES; - static { - List 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 toStateMap() { - Map 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; - } -} diff --git a/backend/src/main/java/com/zombie/game/model/PlayerInfo.java b/backend/src/main/java/com/zombie/game/model/PlayerInfo.java new file mode 100644 index 0000000..4ff4fb6 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/model/PlayerInfo.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/model/Room.java b/backend/src/main/java/com/zombie/game/model/Room.java index ac98b63..a25169b 100644 --- a/backend/src/main/java/com/zombie/game/model/Room.java +++ b/backend/src/main/java/com/zombie/game/model/Room.java @@ -17,7 +17,7 @@ import static com.zombie.game.model.Constants.*; public class Room { private String id; private String hostId; - private Map players; + private Map players; private boolean gameStarted; private final int maxPlayers = 4; @@ -27,15 +27,13 @@ public class Room { this.id = id; this.hostId = hostId; this.players = new LinkedHashMap<>(); - Player host = new Player(hostId, hostName, 0, 0); - players.put(hostId, host); + players.put(hostId, new PlayerInfo(hostId, hostName)); } public boolean addPlayer(String playerId, String playerName) { if (players.size() >= maxPlayers) return false; if (players.containsKey(playerId)) return false; - Player player = new Player(playerId, playerName, 0, 0); - players.put(playerId, player); + players.put(playerId, new PlayerInfo(playerId, playerName)); return true; } @@ -43,11 +41,11 @@ public class Room { players.remove(playerId); } - public Player getPlayer(String playerId) { + public PlayerInfo getPlayer(String playerId) { return players.get(playerId); } - public Collection getPlayers() { + public Collection getPlayers() { return players.values(); } @@ -60,18 +58,12 @@ public class Room { } public boolean allReady() { - for (Player p : players.values()) { + for (PlayerInfo p : players.values()) { if (!p.getId().equals(hostId) && !p.isReady()) return false; } return true; } - /** - * 将房间状态转换为Map格式,用于网络传输 - * - * @param playerId 目标玩家ID - * @return 包含房间状态的Map - */ public Map toStateMap(String playerId) { Map map = new LinkedHashMap<>(); map.put("roomId", id); @@ -81,7 +73,7 @@ public class Room { List> playerList = new ArrayList<>(); int index = 0; - for (Player p : players.values()) { + for (PlayerInfo p : players.values()) { Map pm = new LinkedHashMap<>(); pm.put("id", p.getId()); pm.put("name", p.getName()); @@ -93,15 +85,11 @@ public class Room { return map; } - /** - * 将房间信息转换为房间列表格式,用于大厅显示 - * - * @return 包含房间信息的Map - */ public Map toRoomListMap() { Map map = new LinkedHashMap<>(); map.put("id", id); - map.put("hostName", players.get(hostId) != null ? players.get(hostId).getName() : "Unknown"); + PlayerInfo host = players.get(hostId); + map.put("hostName", host != null ? host.getName() : "Unknown"); map.put("playerCount", players.size()); return map; } diff --git a/backend/src/main/java/com/zombie/game/model/Zombie.java b/backend/src/main/java/com/zombie/game/model/Zombie.java deleted file mode 100644 index 7790e97..0000000 --- a/backend/src/main/java/com/zombie/game/model/Zombie.java +++ /dev/null @@ -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 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 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 otherZombies) { - int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; - - java.util.List 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 toStateMap() { - Map 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; - } -} diff --git a/backend/src/main/java/com/zombie/game/server/GameLoop.java b/backend/src/main/java/com/zombie/game/server/GameLoop.java index 7e1febd..cbcd91b 100644 --- a/backend/src/main/java/com/zombie/game/server/GameLoop.java +++ b/backend/src/main/java/com/zombie/game/server/GameLoop.java @@ -1,6 +1,7 @@ package com.zombie.game.server; -import com.zombie.game.model.GameWorld; +import com.zombie.game.ecs.ECSWorld; +import com.zombie.game.systems.StateSyncSystem; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -10,37 +11,24 @@ import java.util.concurrent.TimeUnit; * 游戏循环类 * * 管理游戏主循环,以固定帧率更新游戏世界状态。 - * 使用 ScheduledExecutorService 实现精确的定时调度,避免忙等待。 */ public class GameLoop { - /** 房间ID */ private String roomId; - /** 游戏世界实例 */ - private GameWorld world; - /** 游戏状态广播回调 */ + private ECSWorld world; private GameService.GameStateBroadcast broadcaster; - /** 循环运行标志 */ + private StateSyncSystem stateSyncSystem; private volatile boolean running; - /** 逻辑帧率(每秒 tick 数) */ private static final int TICK_RATE = 30; - /** 每次 tick 的时间间隔(毫秒) */ private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE; - /** 每次 tick 的时间间隔(秒,用于游戏逻辑计算) */ private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE; - /** 定时任务执行器 */ private ScheduledExecutorService scheduler; - /** - * 构造函数 - * - * @param roomId 房间ID - * @param world 游戏世界实例 - * @param broadcaster 游戏状态广播回调 - */ - public GameLoop(String roomId, GameWorld world, GameService.GameStateBroadcast broadcaster) { + public GameLoop(String roomId, ECSWorld world, GameService.GameStateBroadcast broadcaster, + StateSyncSystem stateSyncSystem) { this.roomId = roomId; this.world = world; this.broadcaster = broadcaster; + this.stateSyncSystem = stateSyncSystem; this.running = true; this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "GameLoop-" + roomId); @@ -49,29 +37,18 @@ public class GameLoop { }); } - /** - * 启动游戏循环 - */ public void start() { scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS); } - /** - * 单次游戏逻辑更新 - */ private void tick() { - if (!running) { - return; - } + if (!running) return; synchronized (world.getLock()) { world.update(TICK_INTERVAL_SEC); } broadcaster.broadcast(roomId, world); } - /** - * 停止游戏循环 - */ public void stop() { running = false; if (scheduler != null && !scheduler.isShutdown()) { diff --git a/backend/src/main/java/com/zombie/game/server/GameService.java b/backend/src/main/java/com/zombie/game/server/GameService.java index 181a41f..192240c 100644 --- a/backend/src/main/java/com/zombie/game/server/GameService.java +++ b/backend/src/main/java/com/zombie/game/server/GameService.java @@ -1,6 +1,9 @@ package com.zombie.game.server; +import com.zombie.game.ecs.ECSWorld; +import com.zombie.game.ecs.components.*; import com.zombie.game.model.*; +import com.zombie.game.systems.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,83 +15,87 @@ import java.util.concurrent.ConcurrentHashMap; * * 负责游戏生命周期管理,包括: * - 游戏启动和停止 - * - 游戏世界状态管理 + * - ECS 游戏世界状态管理 * - 玩家输入处理 * - 游戏状态构建 - * - * 将游戏逻辑从 WebSocket 层解耦,便于独立测试和维护。 */ public class GameService { private static final Logger logger = LoggerFactory.getLogger(GameService.class); - /** 活跃游戏世界集合:roomId -> GameWorld */ - private final Map activeGames = new ConcurrentHashMap<>(); - /** 游戏循环集合:roomId -> GameLoop */ + private final Map activeGames = new ConcurrentHashMap<>(); private final Map gameLoops = new ConcurrentHashMap<>(); - /** 游戏状态广播回调 */ private final GameStateBroadcast broadcaster; - /** - * 游戏状态广播接口 - */ public interface GameStateBroadcast { - void broadcast(String roomId, GameWorld world); + void broadcast(String roomId, ECSWorld world); } - /** - * 构造函数 - * - * @param broadcaster 状态广播回调 - */ public GameService(GameStateBroadcast broadcaster) { this.broadcaster = broadcaster; } - /** - * 启动游戏 - * - * @param room 游戏房间 - * @return 游戏初始化数据,按玩家ID分组 - */ public Map> startGame(Room room) { - GameWorld world = new GameWorld(); + ECSWorld world = new ECSWorld(); + + // Register systems + StateSyncSystem stateSyncSystem = new StateSyncSystem(); + world.addSystem(new DifficultySystem()); + world.addSystem(new PlayerInputSystem()); + world.addSystem(new ZombieMovementSystem()); + world.addSystem(new WeaponFiringSystem()); + world.addSystem(new BulletMovementSystem()); + world.addSystem(new TurretSystem()); + world.addSystem(new CollisionSystem()); + world.addSystem(new DamageSystem()); + world.addSystem(new ZombieDeathSystem()); + world.addSystem(new FireZoneSystem()); + world.addSystem(new LootSystem()); + world.addSystem(new RespawnSystem()); + + // Create player entities int index = 0; List spawnPoints = world.getMap().getSpawnPoints(); - for (Player player : room.getPlayers()) { + for (PlayerInfo playerInfo : room.getPlayers()) { int[] sp = spawnPoints.get(index % spawnPoints.size()); float wx = sp[0] + 0.5f; float wy = sp[1] + 0.5f; - player.setPosition(wx, wy); - world.addPlayer(player); + world.createPlayerEntity(playerInfo.getId(), playerInfo.getName(), wx, wy); index++; } activeGames.put(room.getId(), world); + // Build init data Map> playerInitData = new LinkedHashMap<>(); - for (Player player : room.getPlayers()) { + for (PlayerInfo playerInfo : room.getPlayers()) { Map data = new LinkedHashMap<>(); - data.put("playerId", player.getId()); + data.put("playerId", playerInfo.getId()); data.put("mapData", serializeMapData(world.getMapData())); List> playerList = new ArrayList<>(); int idx = 0; - for (Player p : room.getPlayers()) { + for (PlayerInfo p : room.getPlayers()) { + Integer entityId = world.getPlayerEntity(p.getId()); Map pm = new LinkedHashMap<>(); pm.put("id", p.getId()); pm.put("name", p.getName()); - pm.put("x", p.getX()); - pm.put("y", p.getY()); + if (entityId != null) { + Position pos = world.getPositions().get(entityId); + if (pos != null) { + pm.put("x", pos.getX()); + pm.put("y", pos.getY()); + } + } pm.put("index", idx++); playerList.add(pm); } data.put("players", playerList); - playerInitData.put(player.getId(), data); + playerInitData.put(playerInfo.getId(), data); } - GameLoop loop = new GameLoop(room.getId(), world, broadcaster); + GameLoop loop = new GameLoop(room.getId(), world, broadcaster, stateSyncSystem); gameLoops.put(room.getId(), loop); loop.start(); @@ -96,11 +103,6 @@ public class GameService { return playerInitData; } - /** - * 停止游戏 - * - * @param roomId 房间ID - */ public void stopGame(String roomId) { GameLoop loop = gameLoops.remove(roomId); if (loop != null) { @@ -110,61 +112,41 @@ public class GameService { logger.info("Game stopped for room: {}", roomId); } - /** - * 处理玩家输入 - * - * @param roomId 房间ID - * @param playerId 玩家ID - * @param dx X方向移动量 - * @param dy Y方向移动量 - * @param aimX 瞄准X坐标 - * @param aimY 瞄准Y坐标 - * @param firing 是否开火 - * @param weaponIndex 武器索引 - * @param seq 输入序列号 - * @param grenadeCharge 手榴弹蓄力值 - * @param grenadeReleased 手榴弹是否释放 - */ public void processPlayerInput(String roomId, String playerId, float dx, float dy, float aimX, float aimY, boolean firing, int weaponIndex, int seq, float grenadeCharge, boolean grenadeReleased) { - GameWorld world = activeGames.get(roomId); + ECSWorld world = activeGames.get(roomId); if (world == null) return; synchronized (world.getLock()) { - Player player = world.getPlayer(playerId); - if (player == null || !player.isAlive()) return; + Integer entityId = world.getPlayerEntity(playerId); + if (entityId == null) return; - if (weaponIndex >= 0 && weaponIndex <= 3) { - player.setWeaponIndex(weaponIndex); - } - - player.applyMovement(dx, dy, world.getMap()); - player.setAngle(aimX, aimY); - player.setLastProcessedSeq(seq); - - if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) { - world.fireWeapon(player, aimX, aimY, grenadeCharge); - } else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) { - world.fireWeapon(player, aimX, aimY); + Health health = world.getHealths().get(entityId); + if (health == null || !health.isAlive()) return; + + PlayerInput input = world.getPlayerInputs().get(entityId); + if (input == null) return; + + input.setDx(dx); + input.setDy(dy); + input.setAimX(aimX); + input.setAimY(aimY); + input.setFiring(firing); + if (weaponIndex >= 0 && weaponIndex <= 6) { + input.setWeaponIndex(weaponIndex); } + input.setSeq(seq); + input.setGrenadeCharge(grenadeCharge); + input.setGrenadeReleased(grenadeReleased); } } - /** - * 检查房间是否有活跃游戏 - * - * @param roomId 房间ID - * @return true 表示有活跃游戏 - */ public boolean hasActiveGame(String roomId) { return activeGames.containsKey(roomId); } - /** - * 序列化地图数据为二维列表 - */ private List> serializeMapData(int[][] cells) { List> result = new ArrayList<>(); for (int[] row : cells) { diff --git a/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java b/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java index 59e1521..32373b4 100644 --- a/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java +++ b/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java @@ -2,8 +2,10 @@ package com.zombie.game.server; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.zombie.game.ecs.ECSWorld; import com.zombie.game.model.*; import com.zombie.game.model.Constants; +import com.zombie.game.systems.StateSyncSystem; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; @@ -18,32 +20,17 @@ import java.util.concurrent.ConcurrentHashMap; * 游戏 WebSocket 服务器 * * 处理客户端连接和消息,协调房间管理和游戏实例。 - * 主要功能: - * - 房间创建、加入、离开 - * - 玩家准备和游戏开始 - * - 玩家输入处理 - * - 游戏状态广播 */ public class GameWebSocketServer extends WebSocketServer { private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class); - /** JSON 序列化工具 */ private Gson gson; - /** 房间管理器 */ private RoomManager roomManager; - /** 游戏服务 */ private GameService gameService; - /** WebSocket 连接到玩家ID的映射 */ private Map connectionToPlayer; - /** 玩家ID到WebSocket连接的映射 */ private Map playerToConnection; - /** 房间列表广播定时器 */ private Timer roomListTimer; + private Map stateSyncSystems = new ConcurrentHashMap<>(); - /** - * 构造函数 - * - * @param port 监听端口号 - */ public GameWebSocketServer(int port) { super(new InetSocketAddress(port)); this.gson = new Gson(); @@ -53,19 +40,11 @@ public class GameWebSocketServer extends WebSocketServer { this.gameService = new GameService(this::broadcastGameState); } - /** - * 新连接建立时的回调 - */ @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { logger.info("New connection: {}", conn.getRemoteSocketAddress()); } - /** - * 连接关闭时的回调 - * - * 清理玩家数据并处理离开房间 - */ @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { logger.info("Connection closed: {}", conn.getRemoteSocketAddress()); @@ -76,11 +55,6 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 收到消息时的回调 - * - * 根据消息类型分发到对应的处理方法 - */ @Override public void onMessage(WebSocket conn, String message) { try { @@ -117,19 +91,11 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 发生错误时的回调 - */ @Override public void onError(WebSocket conn, Exception ex) { logger.error("WebSocket error on connection {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex); } - /** - * 服务器启动时的回调 - * - * 启动房间列表广播定时器 - */ @Override public void onStart() { logger.info("Game WebSocket Server started on port {}", getPort()); @@ -142,9 +108,6 @@ public class GameWebSocketServer extends WebSocketServer { }, 0, 2000); } - /** - * 处理创建房间请求 - */ private void handleCreateRoom(WebSocket conn, JsonObject data) { if (!MessageUtils.hasRequired(data, "playerName")) { sendError(conn, "Missing playerName"); @@ -164,9 +127,6 @@ public class GameWebSocketServer extends WebSocketServer { logger.info("Room created: {} by {}", roomId, playerName); } - /** - * 处理加入房间请求 - */ private void handleJoinRoom(WebSocket conn, JsonObject data) { if (!MessageUtils.hasRequired(data, "roomId", "playerName")) { sendError(conn, "Missing roomId or playerName"); @@ -197,35 +157,25 @@ public class GameWebSocketServer extends WebSocketServer { logger.info("Player {} joined room {}", playerName, roomId); } - /** - * 通过连接处理离开房间 - */ private void handleLeaveRoomByConn(WebSocket conn) { String playerId = connectionToPlayer.get(conn); if (playerId == null) return; handleLeaveRoomByPlayerId(playerId); } - /** - * 通过玩家ID处理离开房间 - */ private void handleLeaveRoomByPlayerId(String playerId) { Room room = roomManager.leaveRoom(playerId); - if (room == null) { - return; - } + if (room == null) return; Room updatedRoom = roomManager.getRoom(room.getId()); if (updatedRoom == null) { gameService.stopGame(room.getId()); + stateSyncSystems.remove(room.getId()); } else { broadcastRoomState(updatedRoom); } } - /** - * 处理获取房间列表请求 - */ private void handleRoomList(WebSocket conn) { List> roomList = new ArrayList<>(); for (Room room : roomManager.getAvailableRooms()) { @@ -236,9 +186,6 @@ public class GameWebSocketServer extends WebSocketServer { sendToConnection(conn, Constants.MSG_ROOM_LIST, data); } - /** - * 处理玩家准备请求 - */ private void handleReady(WebSocket conn) { String playerId = connectionToPlayer.get(conn); if (playerId == null) return; @@ -246,16 +193,13 @@ public class GameWebSocketServer extends WebSocketServer { Room room = roomManager.getRoomByPlayerId(playerId); if (room == null) return; - Player player = room.getPlayer(playerId); + PlayerInfo player = room.getPlayer(playerId); if (player != null) { player.setReady(!player.isReady()); broadcastRoomState(room); } } - /** - * 处理开始游戏请求 - */ private void handleStartGame(WebSocket conn) { String playerId = connectionToPlayer.get(conn); if (playerId == null) return; @@ -269,11 +213,6 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 启动游戏 - * - * @param room 游戏房间 - */ private void startGame(Room room) { Map> playerInitData = gameService.startGame(room); @@ -285,11 +224,6 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 处理玩家输入 - * - * 接收并处理玩家的移动、射击、武器切换等输入 - */ private void handlePlayerInput(WebSocket conn, JsonObject data) { String playerId = connectionToPlayer.get(conn); if (playerId == null) return; @@ -311,21 +245,16 @@ public class GameWebSocketServer extends WebSocketServer { firing, weaponIndex, seq, grenadeCharge, grenadeReleased); } - /** - * 广播游戏状态给房间内所有玩家 - * - * @param roomId 房间ID - * @param world 游戏世界 - */ - private void broadcastGameState(String roomId, GameWorld world) { + private void broadcastGameState(String roomId, ECSWorld world) { Room room = roomManager.getRoom(roomId); if (room == null) return; - Map state = null; + StateSyncSystem syncSystem = stateSyncSystems.computeIfAbsent(roomId, id -> new StateSyncSystem()); + synchronized (world.getLock()) { - for (Player player : room.getPlayers()) { - state = world.buildGameState(player.getId()); - WebSocket pConn = playerToConnection.get(player.getId()); + for (PlayerInfo playerInfo : room.getPlayers()) { + Map state = syncSystem.buildGameState(world, playerInfo.getId()); + WebSocket pConn = playerToConnection.get(playerInfo.getId()); if (pConn != null && pConn.isOpen()) { sendToConnection(pConn, Constants.MSG_GAME_STATE, state); } @@ -333,11 +262,8 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 广播房间状态给房间内所有玩家 - */ private void broadcastRoomState(Room room) { - for (Player player : room.getPlayers()) { + for (PlayerInfo player : room.getPlayers()) { WebSocket pConn = playerToConnection.get(player.getId()); if (pConn != null && pConn.isOpen()) { sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId())); @@ -345,9 +271,6 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 广播房间列表给所有未加入房间的连接 - */ private void broadcastRoomList() { List> roomList = new ArrayList<>(); for (Room room : roomManager.getAvailableRooms()) { @@ -363,13 +286,6 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 发送消息给指定连接 - * - * @param conn WebSocket 连接 - * @param type 消息类型 - * @param data 消息数据 - */ private void sendToConnection(WebSocket conn, String type, Object data) { if (conn != null && conn.isOpen()) { Map msg = new LinkedHashMap<>(); @@ -379,12 +295,6 @@ public class GameWebSocketServer extends WebSocketServer { } } - /** - * 发送错误消息 - * - * @param conn WebSocket 连接 - * @param message 错误消息 - */ private void sendError(WebSocket conn, String message) { Map data = new LinkedHashMap<>(); data.put("message", message); diff --git a/backend/src/main/java/com/zombie/game/server/RoomManager.java b/backend/src/main/java/com/zombie/game/server/RoomManager.java index 15f9b5e..71dbb43 100644 --- a/backend/src/main/java/com/zombie/game/server/RoomManager.java +++ b/backend/src/main/java/com/zombie/game/server/RoomManager.java @@ -1,6 +1,6 @@ package com.zombie.game.server; -import com.zombie.game.model.Player; +import com.zombie.game.model.PlayerInfo; import com.zombie.game.model.Room; import java.util.ArrayList; @@ -29,7 +29,7 @@ public class RoomManager { */ public void addRoom(String roomId, Room room) { rooms.put(roomId, room); - for (Player player : room.getPlayers()) { + for (PlayerInfo player : room.getPlayers()) { playerToRoom.put(player.getId(), roomId); } } @@ -129,7 +129,7 @@ public class RoomManager { public void removeRoom(String roomId) { Room room = rooms.remove(roomId); if (room != null) { - for (Player player : room.getPlayers()) { + for (PlayerInfo player : room.getPlayers()) { playerToRoom.remove(player.getId()); } } diff --git a/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java b/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java new file mode 100644 index 0000000..969afb5 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java @@ -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 bulletSet, GameMap map, boolean isZombieBullet) { + List 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); + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java b/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java new file mode 100644 index 0000000..8c8a303 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java @@ -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 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 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; + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/DamageSystem.java b/backend/src/main/java/com/zombie/game/systems/DamageSystem.java new file mode 100644 index 0000000..15563d8 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/DamageSystem.java @@ -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); + } + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/DifficultySystem.java b/backend/src/main/java/com/zombie/game/systems/DifficultySystem.java new file mode 100644 index 0000000..756c9d1 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/DifficultySystem.java @@ -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 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); + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/FireZoneSystem.java b/backend/src/main/java/com/zombie/game/systems/FireZoneSystem.java new file mode 100644 index 0000000..7ec900b --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/FireZoneSystem.java @@ -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 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); + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/LootSystem.java b/backend/src/main/java/com/zombie/game/systems/LootSystem.java new file mode 100644 index 0000000..79a2475 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/LootSystem.java @@ -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 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); + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/PlayerInputSystem.java b/backend/src/main/java/com/zombie/game/systems/PlayerInputSystem.java new file mode 100644 index 0000000..4640139 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/PlayerInputSystem.java @@ -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()); + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/RespawnSystem.java b/backend/src/main/java/com/zombie/game/systems/RespawnSystem.java new file mode 100644 index 0000000..5e6ce17 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/RespawnSystem.java @@ -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 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(); + } + } + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java b/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java new file mode 100644 index 0000000..c08587f --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java @@ -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 buildGameState(ECSWorld world, String forPlayerId) { + Map state = new LinkedHashMap<>(); + + // 玩家状态 + List> playerStates = new ArrayList<>(); + for (int entityId : world.getPlayers()) { + playerStates.add(buildPlayerState(world, entityId)); + } + state.put("players", playerStates); + + // 僵尸状态 + List> zombieStates = new ArrayList<>(); + for (int entityId : world.getZombies()) { + zombieStates.add(buildZombieState(world, entityId)); + } + state.put("zombies", zombieStates); + + // 玩家子弹状态 + List> bulletStates = new ArrayList<>(); + for (int entityId : world.getPlayerBullets()) { + bulletStates.add(buildBulletState(world, entityId)); + } + state.put("bullets", bulletStates); + + // 僵尸子弹状态 + List> zombieBulletStates = new ArrayList<>(); + for (int entityId : world.getZombieBullets()) { + zombieBulletStates.add(buildBulletState(world, entityId)); + } + state.put("zombieBullets", zombieBulletStates); + + // 掉落物状态 + List> 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 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 buildPlayerState(ECSWorld world, int entityId) { + Map 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 buildZombieState(ECSWorld world, int entityId) { + Map 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 buildBulletState(ECSWorld world, int entityId) { + Map 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 buildLootState(ECSWorld world, int entityId) { + Map 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() 显式调用 + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/TurretSystem.java b/backend/src/main/java/com/zombie/game/systems/TurretSystem.java new file mode 100644 index 0000000..f78ab72 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/TurretSystem.java @@ -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); + } + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java b/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java new file mode 100644 index 0000000..dff882d --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/ZombieDeathSystem.java b/backend/src/main/java/com/zombie/game/systems/ZombieDeathSystem.java new file mode 100644 index 0000000..d42f369 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/ZombieDeathSystem.java @@ -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 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); + } + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java b/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java new file mode 100644 index 0000000..85fc1c3 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java @@ -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 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 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]}; + } +} diff --git a/backend/src/main/java/com/zombie/game/template/TemplateManager.java b/backend/src/main/java/com/zombie/game/template/TemplateManager.java index 55b1a9a..01b5736 100644 --- a/backend/src/main/java/com/zombie/game/template/TemplateManager.java +++ b/backend/src/main/java/com/zombie/game/template/TemplateManager.java @@ -33,9 +33,6 @@ public class TemplateManager { return instance; } - /** - * 加载所有模板 - */ private void loadAll() { loadZombies(); loadWeapons(); @@ -82,6 +79,7 @@ public class TemplateManager { WeaponTemplate t = new WeaponTemplate( node.get("id").asText(), node.get("name").asText(), + node.has("category") ? node.get("category").asText() : "firearm", node.get("damage").asInt(), node.get("fireRate").asLong(), node.get("pelletCount").asInt(), @@ -91,7 +89,15 @@ public class TemplateManager { node.get("maxAmmo").asInt(), node.get("chargeable").asBoolean(), node.get("explosive").asBoolean(), - (float) node.get("explosionRadius").asDouble() + (float) node.get("explosionRadius").asDouble(), + node.has("fireZoneRadius") ? (float) node.get("fireZoneRadius").asDouble() : 0, + node.has("fireZoneDamage") ? (float) node.get("fireZoneDamage").asDouble() : 0, + node.has("fireZoneDuration") ? (float) node.get("fireZoneDuration").asDouble() : 0, + node.has("turretHealth") ? (float) node.get("turretHealth").asDouble() : 0, + node.has("turretRange") ? (float) node.get("turretRange").asDouble() : 0, + node.has("turretFireRate") ? node.get("turretFireRate").asLong() : 0, + node.has("turretDamage") ? node.get("turretDamage").asInt() : 0, + node.has("turretBulletSpeed") ? (float) node.get("turretBulletSpeed").asDouble() : 0 ); weaponTemplates.put(t.getId(), t); } @@ -137,9 +143,6 @@ public class TemplateManager { return playerTemplate; } - /** - * 获取武器列表的索引位置 - */ public int getWeaponIndex(String id) { int idx = 0; for (String key : weaponTemplates.keySet()) { @@ -149,9 +152,6 @@ public class TemplateManager { return 0; } - /** - * 根据索引获取武器ID - */ public String getWeaponId(int index) { int idx = 0; for (String key : weaponTemplates.keySet()) { diff --git a/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java b/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java index 9a9ccb6..922e683 100644 --- a/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java +++ b/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java @@ -6,11 +6,13 @@ import lombok.Getter; * 武器类型模板 * * 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。 + * 支持三种类别:firearm(枪械)、thrown(投掷)、placed(放置) */ @Getter public class WeaponTemplate { private final String id; private final String name; + private final String category; // firearm, thrown, placed private final int damage; private final long fireRate; private final int pelletCount; @@ -22,12 +24,28 @@ public class WeaponTemplate { private final boolean explosive; private final float explosionRadius; - public WeaponTemplate(String id, String name, int damage, long fireRate, + // Thrown weapon (molotov) fields + private final float fireZoneRadius; + private final float fireZoneDamage; + private final float fireZoneDuration; + + // Placed weapon (turret) fields + private final float turretHealth; + private final float turretRange; + private final long turretFireRate; + private final int turretDamage; + private final float turretBulletSpeed; + + public WeaponTemplate(String id, String name, String category, int damage, long fireRate, int pelletCount, float spread, float bulletSpeed, float range, int maxAmmo, boolean chargeable, - boolean explosive, float explosionRadius) { + boolean explosive, float explosionRadius, + float fireZoneRadius, float fireZoneDamage, float fireZoneDuration, + float turretHealth, float turretRange, long turretFireRate, + int turretDamage, float turretBulletSpeed) { this.id = id; this.name = name; + this.category = category; this.damage = damage; this.fireRate = fireRate; this.pelletCount = pelletCount; @@ -38,5 +56,13 @@ public class WeaponTemplate { this.chargeable = chargeable; this.explosive = explosive; this.explosionRadius = explosionRadius; + this.fireZoneRadius = fireZoneRadius; + this.fireZoneDamage = fireZoneDamage; + this.fireZoneDuration = fireZoneDuration; + this.turretHealth = turretHealth; + this.turretRange = turretRange; + this.turretFireRate = turretFireRate; + this.turretDamage = turretDamage; + this.turretBulletSpeed = turretBulletSpeed; } } diff --git a/backend/src/main/resources/data/weapons.json b/backend/src/main/resources/data/weapons.json index e7241bf..0ff20cb 100644 --- a/backend/src/main/resources/data/weapons.json +++ b/backend/src/main/resources/data/weapons.json @@ -3,6 +3,7 @@ { "id": "pistol", "name": "手枪", + "category": "firearm", "damage": 50, "fireRate": 400, "pelletCount": 1, @@ -12,11 +13,20 @@ "maxAmmo": 2147483647, "chargeable": false, "explosive": false, - "explosionRadius": 0 + "explosionRadius": 0, + "fireZoneRadius": 0, + "fireZoneDamage": 0, + "fireZoneDuration": 0, + "turretHealth": 0, + "turretRange": 0, + "turretFireRate": 0, + "turretDamage": 0, + "turretBulletSpeed": 0 }, { "id": "machine_gun", - "name": "机枪", + "name": "机关枪", + "category": "firearm", "damage": 50, "fireRate": 100, "pelletCount": 1, @@ -26,11 +36,20 @@ "maxAmmo": 100, "chargeable": false, "explosive": false, - "explosionRadius": 0 + "explosionRadius": 0, + "fireZoneRadius": 0, + "fireZoneDamage": 0, + "fireZoneDuration": 0, + "turretHealth": 0, + "turretRange": 0, + "turretFireRate": 0, + "turretDamage": 0, + "turretBulletSpeed": 0 }, { "id": "shotgun", - "name": "霰弹枪", + "name": "散弹枪", + "category": "firearm", "damage": 50, "fireRate": 800, "pelletCount": 10, @@ -40,11 +59,20 @@ "maxAmmo": 20, "chargeable": false, "explosive": false, - "explosionRadius": 0 + "explosionRadius": 0, + "fireZoneRadius": 0, + "fireZoneDamage": 0, + "fireZoneDuration": 0, + "turretHealth": 0, + "turretRange": 0, + "turretFireRate": 0, + "turretDamage": 0, + "turretBulletSpeed": 0 }, { "id": "grenade", "name": "手榴弹", + "category": "thrown", "damage": 120, "fireRate": 1500, "pelletCount": 1, @@ -54,7 +82,84 @@ "maxAmmo": 10, "chargeable": true, "explosive": true, - "explosionRadius": 3 + "explosionRadius": 3, + "fireZoneRadius": 0, + "fireZoneDamage": 0, + "fireZoneDuration": 0, + "turretHealth": 0, + "turretRange": 0, + "turretFireRate": 0, + "turretDamage": 0, + "turretBulletSpeed": 0 + }, + { + "id": "molotov", + "name": "燃烧瓶", + "category": "thrown", + "damage": 80, + "fireRate": 2000, + "pelletCount": 1, + "spread": 0, + "bulletSpeed": 12, + "range": 12, + "maxAmmo": 5, + "chargeable": true, + "explosive": true, + "explosionRadius": 2.0, + "fireZoneRadius": 2.5, + "fireZoneDamage": 10, + "fireZoneDuration": 3.0, + "turretHealth": 0, + "turretRange": 0, + "turretFireRate": 0, + "turretDamage": 0, + "turretBulletSpeed": 0 + }, + { + "id": "nut_wall", + "name": "坚果墙体", + "category": "placed", + "damage": 0, + "fireRate": 1000, + "pelletCount": 0, + "spread": 0, + "bulletSpeed": 0, + "range": 0, + "maxAmmo": 3, + "chargeable": false, + "explosive": false, + "explosionRadius": 0, + "fireZoneRadius": 0, + "fireZoneDamage": 0, + "fireZoneDuration": 0, + "turretHealth": 20, + "turretRange": 0, + "turretFireRate": 0, + "turretDamage": 0, + "turretBulletSpeed": 0 + }, + { + "id": "auto_turret", + "name": "自动机枪塔", + "category": "placed", + "damage": 0, + "fireRate": 2000, + "pelletCount": 0, + "spread": 0, + "bulletSpeed": 0, + "range": 0, + "maxAmmo": 2, + "chargeable": false, + "explosive": false, + "explosionRadius": 0, + "fireZoneRadius": 0, + "fireZoneDamage": 0, + "fireZoneDuration": 0, + "turretHealth": 100, + "turretRange": 6.0, + "turretFireRate": 300, + "turretDamage": 30, + "turretBulletSpeed": 20 } ] } diff --git a/frontend/src/game/engine.js b/frontend/src/game/engine.js index d9dfa3d..6aa8848 100644 --- a/frontend/src/game/engine.js +++ b/frontend/src/game/engine.js @@ -54,7 +54,10 @@ export class GameEngine { [WEAPONS.PISTOL]: Infinity, // 手枪无限弹药 [WEAPONS.MACHINE_GUN]: 100, [WEAPONS.SHOTGUN]: 20, - [WEAPONS.GRENADE]: 10 + [WEAPONS.GRENADE]: 10, + [WEAPONS.MOLOTOV]: 5, + [WEAPONS.NUT_WALL]: 3, + [WEAPONS.AUTO_TURRET]: 2 } // 手雷蓄力相关 @@ -217,8 +220,8 @@ export class GameEngine { const localPlayer = this.players.get(this.localPlayerId) if (localPlayer) { this.scene.updateCamera(localPlayer.x, localPlayer.y) - // 手雷瞄准指示器 - if (this.isChargingGrenade && this.currentWeaponIndex === 3) { + // 投掷武器瞄准指示器(手雷/燃烧瓶) + if (this.isChargingGrenade && (this.currentWeaponIndex === 3 || this.currentWeaponIndex === 4)) { this.scene.showGrenadeTarget(localPlayer.x, localPlayer.y, this.input.mouse.groundX || 0, this.input.mouse.groundY || 0, this.grenadeChargePercent) @@ -260,11 +263,16 @@ export class GameEngine { // 应用本地预测(客户端预测) this._applyLocalPrediction(inputState) - // 添加手雷相关数据到输入状态 + // 添加蓄力武器相关数据到输入状态(手雷/燃烧瓶) inputState.grenadeCharge = this.grenadeChargePercent inputState.grenadeReleased = this.grenadeReleased - // 手雷释放前不发火 - inputState.firing = this.currentWeaponIndex === 3 ? false : inputState.firing + // 蓄力武器释放前不发火 + const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET] + const currentWeaponId = weaponList7[this.currentWeaponIndex] + const currentConfig = WEAPON_CONFIG[currentWeaponId] + if (currentConfig && currentConfig.chargeable) { + inputState.firing = false + } // 保存输入以便进行客户端预测校正 this.pendingInputs.push(inputState) @@ -287,10 +295,11 @@ export class GameEngine { * 手雷需要长按鼠标蓄力,松开释放 */ _handleGrenadeCharge(inputState) { - const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE] - const currentWeapon = weaponList[this.currentWeaponIndex] + const weaponList7 = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET] + const currentWeapon = weaponList7[this.currentWeaponIndex] + const config = WEAPON_CONFIG[currentWeapon] - if (currentWeapon === WEAPONS.GRENADE && WEAPON_CONFIG[WEAPONS.GRENADE].chargeable) { + if (config && config.chargeable) { // 开始蓄力 if (inputState.firing && !this.isChargingGrenade) { this.isChargingGrenade = true @@ -298,13 +307,13 @@ export class GameEngine { } else if (inputState.firing && this.isChargingGrenade) { // 蓄力中,计算蓄力百分比 const elapsed = Date.now() - this.grenadeChargeStart - this.grenadeChargePercent = Math.min(1, elapsed / WEAPON_CONFIG[WEAPONS.GRENADE].maxCharge) + this.grenadeChargePercent = Math.min(1, elapsed / config.maxCharge) } else if (!inputState.firing && this.isChargingGrenade) { - // 释放手雷 + // 释放 this.grenadeReleased = true this.isChargingGrenade = false const elapsed = Date.now() - this.grenadeChargeStart - this.grenadeChargePercent = Math.min(1, elapsed / WEAPON_CONFIG[WEAPONS.GRENADE].maxCharge) + this.grenadeChargePercent = Math.min(1, elapsed / config.maxCharge) } } else { this.isChargingGrenade = false @@ -417,9 +426,9 @@ export class GameEngine { // 新增子弹 this.bullets.set(bs.id, { ...bs }) this.scene.addBullet(bs) - // 枪口火焰特效(手雷除外) + // 枪口火焰特效(投掷类武器除外) const player = this.players.get(bs.ownerId) - if (player && bs.weapon !== WEAPONS.GRENADE) { + if (player && bs.weaponIndex !== 3 && bs.weaponIndex !== 4) { this.scene.addMuzzleFlash(player.x, player.y, bs.angle || 0) } } else { diff --git a/frontend/src/game/scene.js b/frontend/src/game/scene.js index c97eb16..8168ed9 100644 --- a/frontend/src/game/scene.js +++ b/frontend/src/game/scene.js @@ -556,22 +556,30 @@ export class GameScene { */ addBullet(bullet) { const group = new THREE.Group() - const isGrenade = bullet.weapon === WEAPONS.GRENADE + // weaponIndex: 0=pistol, 1=machinegun, 2=shotgun, 3=grenade, 4=molotov + const wIdx = bullet.weaponIndex + const isGrenade = wIdx === 3 + const isMolotov = wIdx === 4 + const isThrown = isGrenade || isMolotov const z = bullet.z || 0.5 - if (isGrenade) { - // 手雷:球体 + 发光 + 拖尾 + if (isThrown) { + // 投掷物:球体 + 发光 + 拖尾 + const bodyColor = isMolotov ? 0xff6600 : 0x44ff44 + const glowColor = isMolotov ? 0xff4400 : 0x22cc22 + const trailColor = isMolotov ? 0xff8844 : 0x88ff88 + const geo = new THREE.SphereGeometry(0.12, 8, 8) - const mat = new THREE.MeshBasicMaterial({ color: 0x44ff44 }) + const mat = new THREE.MeshBasicMaterial({ color: bodyColor }) const mesh = new THREE.Mesh(geo, mat) mesh.position.set(bullet.x, z, bullet.y) group.add(mesh) const glowGeo = new THREE.SphereGeometry(0.18, 8, 8) const glowMat = new THREE.MeshBasicMaterial({ - color: 0x22cc22, + color: glowColor, transparent: true, - opacity: 0.4 + opacity: 0.5 }) const glow = new THREE.Mesh(glowGeo, glowMat) glow.position.set(bullet.x, z, bullet.y) @@ -580,7 +588,7 @@ export class GameScene { // 拖尾线 const trailGeo = new THREE.BufferGeometry() const trailMat = new THREE.LineBasicMaterial({ - color: 0x88ff88, + color: trailColor, transparent: true, opacity: 0.6 }) @@ -598,14 +606,8 @@ export class GameScene { const angle = bullet.angle || 0 // 根据武器类型调整大小 - switch (bullet.weapon) { - case WEAPONS.MACHINE_GUN: - bulletSize = 0.05 - break - case WEAPONS.SHOTGUN: - bulletSize = 0.04 - break - } + if (wIdx === 1) bulletSize = 0.05 // 机枪 + else if (wIdx === 2) bulletSize = 0.04 // 霰弹枪 // 拖尾 const trailLength = 2.5 @@ -651,9 +653,10 @@ export class GameScene { x: bullet.x, y: bullet.y, z: z, - weapon: bullet.weapon, + weaponIndex: wIdx, angle: bullet.angle || 0, - isGrenade + isGrenade, + isMolotov }) } @@ -695,7 +698,7 @@ export class GameScene { child.position.z = y } // 更新拖尾位置 - if (child.isLine && !bullet.isGrenade) { + if (child.isLine && !bullet.isGrenade && !bullet.isMolotov) { const trailLength = 2.5 const positions = child.geometry.attributes.position.array positions[0] = x - Math.sin(angle) * trailLength @@ -1083,7 +1086,7 @@ export class GameScene { // 更新子弹拖尾 for (const bullet of this.bullets) { - if (!bullet.isGrenade && bullet.trail) { + if (!bullet.isGrenade && !bullet.isMolotov && bullet.trail) { const positions = bullet.trail.geometry.attributes.position.array positions[0] = bullet.x - Math.sin(bullet.angle) * 1.5 positions[1] = 0.5 diff --git a/frontend/src/ui/hud.js b/frontend/src/ui/hud.js index 7700669..836d2e3 100644 --- a/frontend/src/ui/hud.js +++ b/frontend/src/ui/hud.js @@ -28,7 +28,7 @@ export class HUD { this.weaponPanel.className = 'hud-weapon-panel' // 武器列表 - const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE] + const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET] this.weaponSlots = [] for (let i = 0; i < weaponList.length; i++) { const slot = document.createElement('div') @@ -135,7 +135,7 @@ export class HUD { * @param {Object} ammo 各武器弹药数 */ updateWeapons(currentIndex, ammo) { - const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE] + const weaponList = [WEAPONS.PISTOL, WEAPONS.MACHINE_GUN, WEAPONS.SHOTGUN, WEAPONS.GRENADE, WEAPONS.MOLOTOV, WEAPONS.NUT_WALL, WEAPONS.AUTO_TURRET] for (let i = 0; i < this.weaponSlots.length; i++) { const slot = this.weaponSlots[i] // 高亮当前武器 diff --git a/frontend/src/ui/settings.js b/frontend/src/ui/settings.js index da91247..bdfa305 100644 --- a/frontend/src/ui/settings.js +++ b/frontend/src/ui/settings.js @@ -17,7 +17,10 @@ export class SettingsUI { weapon1: 'Digit1', weapon2: 'Digit2', weapon3: 'Digit3', - weapon4: 'Digit4' + weapon4: 'Digit4', + weapon5: 'Digit5', + weapon6: 'Digit6', + weapon7: 'Digit7' } this.bindings = { ...this.defaultBindings } } @@ -68,7 +71,10 @@ export class SettingsUI { weapon1: 'Weapon 1 (Pistol)', weapon2: 'Weapon 2 (MG)', weapon3: 'Weapon 3 (Shotgun)', - weapon4: 'Weapon 4 (Grenade)' + weapon4: 'Weapon 4 (Grenade)', + weapon5: 'Weapon 5 (Molotov)', + weapon6: 'Weapon 6 (Wall)', + weapon7: 'Weapon 7 (Turret)' } // 生成每种操作的按键配置行 diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 4da9d13..f5af7a4 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -23,7 +23,10 @@ export const WEAPONS = { PISTOL: 'pistol', // 手枪 MACHINE_GUN: 'machine_gun', // 机枪 SHOTGUN: 'shotgun', // 霰弹枪 - GRENADE: 'grenade' // 手雷 + GRENADE: 'grenade', // 手雷 + MOLOTOV: 'molotov', // 燃烧瓶 + NUT_WALL: 'nut_wall', // 坚果墙体 + AUTO_TURRET: 'auto_turret' // 自动机枪塔 } // ========== 武器配置 ========== @@ -84,7 +87,51 @@ export const WEAPON_CONFIG = { auto: false, chargeable: true, // 可蓄力 maxCharge: 2000, // 最大蓄力时间(毫秒) - explosionRadius: 3 // 爆炸半径 + explosionRadius: 3 // 爆炸半径 + }, + // 燃烧瓶配置 + [WEAPONS.MOLOTOV]: { + name: 'Molotov', + damage: 80, + fireRate: 2000, + ammo: 5, + maxAmmo: 5, + speed: 12, + spread: 0, + pellets: 1, + range: 12, + auto: false, + chargeable: true, // 可蓄力 + maxCharge: 2000, + explosionRadius: 2 + }, + // 坚果墙体配置 + [WEAPONS.NUT_WALL]: { + name: 'Wall', + damage: 0, + fireRate: 1000, + ammo: 3, + maxAmmo: 3, + speed: 0, + spread: 0, + pellets: 0, + range: 0, + auto: false, + chargeable: false + }, + // 自动机枪塔配置 + [WEAPONS.AUTO_TURRET]: { + name: 'Turret', + damage: 0, + fireRate: 2000, + ammo: 2, + maxAmmo: 2, + speed: 0, + spread: 0, + pellets: 0, + range: 0, + auto: false, + chargeable: false } } diff --git a/frontend/src/utils/input.js b/frontend/src/utils/input.js index e584a83..71c33ef 100644 --- a/frontend/src/utils/input.js +++ b/frontend/src/utils/input.js @@ -18,7 +18,10 @@ export class InputManager { weapon1: 'Digit1', weapon2: 'Digit2', weapon3: 'Digit3', - weapon4: 'Digit4' + weapon4: 'Digit4', + weapon5: 'Digit5', + weapon6: 'Digit6', + weapon7: 'Digit7' } // 待处理的输入序列(用于客户端预测) @@ -100,13 +103,16 @@ export class InputManager { /** * 获取当前选择的武器 - * @returns {number} 武器索引(0-3),无选择返回-1 + * @returns {number} 武器索引(0-6),无选择返回-1 */ getSelectedWeapon() { if (this.keys[this.keyBindings.weapon1]) return 0 if (this.keys[this.keyBindings.weapon2]) return 1 if (this.keys[this.keyBindings.weapon3]) return 2 if (this.keys[this.keyBindings.weapon4]) return 3 + if (this.keys[this.keyBindings.weapon5]) return 4 + if (this.keys[this.keyBindings.weapon6]) return 5 + if (this.keys[this.keyBindings.weapon7]) return 6 return -1 } diff --git a/maps/d540209a.json b/maps/d540209a.json index 7f7d7e8..99790fd 100644 --- a/maps/d540209a.json +++ b/maps/d540209a.json @@ -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}]} \ No newline at end of file + { + "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 + } + ] + } \ No newline at end of file