diff --git a/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java b/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java index df40eaf..1be5dbd 100644 --- a/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java +++ b/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java @@ -2,6 +2,7 @@ package com.zombie.game.ecs; import com.zombie.game.ecs.components.*; import com.zombie.game.model.GameMap; +import com.zombie.game.model.TurretWall; import com.zombie.game.template.TemplateManager; import com.zombie.game.template.ZombieTemplate; import lombok.Getter; @@ -78,6 +79,14 @@ public class ECSWorld { /** 墙体实体组件 */ private final Map wallEntityDatas = new HashMap<>(); + // ==================== 对象池(子弹组件) ==================== + private static final int BULLET_POOL_SIZE = 100; + private final ObjectPool positionPool = new ObjectPool<>(Position::new, BULLET_POOL_SIZE); + private final ObjectPool velocityPool = new ObjectPool<>(() -> new Velocity(0, 0, 0), BULLET_POOL_SIZE); + private final ObjectPool bulletDataPool = new ObjectPool<>(() -> new BulletData(0, "", 0, 0), BULLET_POOL_SIZE); + private final ObjectPool collisionPool = new ObjectPool<>(() -> new Collision(0), BULLET_POOL_SIZE); + private final ObjectPool renderInfoPool = new ObjectPool<>(() -> new RenderInfo(RenderInfo.EntityType.BULLET), BULLET_POOL_SIZE); + // ==================== 系统列表 ==================== private final List systems = new ArrayList<>(); @@ -136,8 +145,14 @@ public class ECSWorld { return id; } - /** 销毁实体,清除所有组件和集合引用 */ + /** 销毁实体,清除所有组件和集合引用,子弹组件归还对象池 */ public void destroyEntity(int id) { + // 机枪塔销毁时同步移除流场障碍物 + WallEntity we = wallEntityDatas.get(id); + if (we != null && turrets.contains(id)) { + map.removeWall(we.getGridX(), we.getGridY()); + } + entities.remove(id); players.remove(id); zombies.remove(id); @@ -149,21 +164,32 @@ public class ECSWorld { 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); + + // 子弹高频组件归还对象池 + Position p = positions.remove(id); + if (p != null) positionPool.free(p); + + Velocity v = velocities.remove(id); + if (v != null) velocityPool.free(v); + + BulletData bd = bulletDatas.remove(id); + if (bd != null) bulletDataPool.free(bd); + + Collision c = collisions.remove(id); + if (c != null) collisionPool.free(c); + + RenderInfo ri = renderInfos.remove(id); + if (ri != null) renderInfoPool.free(ri); } // ==================== 玩家实体创建 ==================== @@ -230,21 +256,39 @@ public class ECSWorld { // ==================== 子弹实体创建 ==================== /** - * 创建标准子弹实体(直线飞行) + * 创建标准子弹实体(直线飞行),使用对象池复用组件 */ 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 - )); + + Position pos = positionPool.obtain(); + pos.setX(x); pos.setY(y); pos.setAngle(angle); + positions.put(entityId, pos); + + Velocity vel = velocityPool.obtain(); + vel.setVx((float) Math.sin(angle) * speed); + vel.setVy((float) Math.cos(angle) * speed); + vel.setVz(0); + velocities.put(entityId, vel); + + BulletData bd = bulletDataPool.obtain(); + bd.setDamage(damage); bd.setOwnerId(ownerId); bd.setWeaponIndex(weaponIndex); + bd.setRange(range); bd.setDistanceTraveled(0); + bd.setGrenade(false); bd.setMolotov(false); + bd.setFlightTime(0); bd.setMaxFlightTime(0); + bd.setStartX(0); bd.setStartY(0); + bd.setTargetX(0); bd.setTargetY(0); bd.setHasTarget(false); + bulletDatas.put(entityId, bd); + + Collision col = collisionPool.obtain(); + col.setSize(0.2f); + collisions.put(entityId, col); + + RenderInfo ri = renderInfoPool.obtain(); + ri.setType(isZombieBullet ? RenderInfo.EntityType.ZOMBIE_BULLET : RenderInfo.EntityType.BULLET); + ri.setSubType(""); ri.setWeaponIndex(-1); + renderInfos.put(entityId, ri); if (isZombieBullet) { zombieBullets.add(entityId); @@ -255,55 +299,83 @@ public class ECSWorld { } /** - * 创建手榴弹实体(抛物线飞行,落地爆炸) + * 创建手榴弹实体(抛物线飞行,落地爆炸),使用对象池复用组件 */ public int createGrenadeEntity(float startX, float startY, float targetX, float targetY, - float flightDuration, int damage, String ownerId, float explosionRadius) { + float flightDuration, int damage, String ownerId, int weaponIndex, float explosionRadius) { int entityId = createEntity(); - positions.put(entityId, new Position(startX, startY)); + + Position pos = positionPool.obtain(); + pos.setX(startX); pos.setY(startY); pos.setAngle(0); + positions.put(entityId, pos); float vx = (targetX - startX) / flightDuration; float vy = (targetY - startY) / flightDuration; - velocities.put(entityId, new Velocity(vx, vy, 3.0f)); + Velocity vel = velocityPool.obtain(); + vel.setVx(vx); vel.setVy(vy); vel.setVz(3.0f); + velocities.put(entityId, vel); - BulletData data = BulletData.createGrenade(damage, ownerId, targetX, targetY, flightDuration); - data.setStartX(startX); - data.setStartY(startY); - bulletDatas.put(entityId, data); + BulletData bd = bulletDataPool.obtain(); + bd.setDamage(damage); bd.setOwnerId(ownerId); bd.setWeaponIndex(weaponIndex); bd.setRange(0); + bd.setDistanceTraveled(0); bd.setGrenade(true); bd.setMolotov(false); + bd.setFlightTime(0); bd.setMaxFlightTime(flightDuration); + bd.setStartX(startX); bd.setStartY(startY); + bd.setTargetX(targetX); bd.setTargetY(targetY); bd.setHasTarget(true); + bulletDatas.put(entityId, bd); explosives.put(entityId, new Explosive(explosionRadius, damage)); - collisions.put(entityId, new Collision(0.2f)); - renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.BULLET)); + + Collision col = collisionPool.obtain(); + col.setSize(0.2f); + collisions.put(entityId, col); + + RenderInfo ri = renderInfoPool.obtain(); + ri.setType(RenderInfo.EntityType.BULLET); ri.setSubType(""); ri.setWeaponIndex(-1); + renderInfos.put(entityId, ri); playerBullets.add(entityId); return entityId; } /** - * 创建燃烧瓶实体(抛物线飞行,落地后产生火焰区域) + * 创建燃烧瓶实体(抛物线飞行,落地后产生火焰区域),使用对象池复用组件 */ public int createMolotovEntity(float startX, float startY, float targetX, float targetY, - float flightDuration, int damage, String ownerId, + float flightDuration, int damage, String ownerId, int weaponIndex, float explosionRadius, float fireZoneRadius, float fireZoneDamage, float fireZoneDuration) { int entityId = createEntity(); - positions.put(entityId, new Position(startX, startY)); + + Position pos = positionPool.obtain(); + pos.setX(startX); pos.setY(startY); pos.setAngle(0); + positions.put(entityId, pos); float vx = (targetX - startX) / flightDuration; float vy = (targetY - startY) / flightDuration; - velocities.put(entityId, new Velocity(vx, vy, 3.0f)); + Velocity vel = velocityPool.obtain(); + vel.setVx(vx); vel.setVy(vy); vel.setVz(3.0f); + velocities.put(entityId, vel); - BulletData data = BulletData.createMolotov(damage, ownerId, targetX, targetY, flightDuration); - data.setStartX(startX); - data.setStartY(startY); - bulletDatas.put(entityId, data); + BulletData bd = bulletDataPool.obtain(); + bd.setDamage(damage); bd.setOwnerId(ownerId); bd.setWeaponIndex(weaponIndex); bd.setRange(0); + bd.setDistanceTraveled(0); bd.setGrenade(false); bd.setMolotov(true); + bd.setFlightTime(0); bd.setMaxFlightTime(flightDuration); + bd.setStartX(startX); bd.setStartY(startY); + bd.setTargetX(targetX); bd.setTargetY(targetY); bd.setHasTarget(true); + bulletDatas.put(entityId, bd); // 存储爆炸参数(落地初始爆炸使用) 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)); + + Collision col = collisionPool.obtain(); + col.setSize(0.2f); + collisions.put(entityId, col); + + RenderInfo ri = renderInfoPool.obtain(); + ri.setType(RenderInfo.EntityType.BULLET); ri.setSubType(""); ri.setWeaponIndex(-1); + renderInfos.put(entityId, ri); playerBullets.add(entityId); return entityId; @@ -356,6 +428,9 @@ public class ECSWorld { wallEntityDatas.put(entityId, new WallEntity(gridX, gridY)); renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.TURRET)); + // 注册为流场障碍物,僵尸会将其视为可攻击目标 + map.addWall(gridX, gridY, new TurretWall(gridX, gridY, entityId)); + turrets.add(entityId); wallEntities.add(entityId); return entityId; @@ -375,6 +450,19 @@ public class ECSWorld { /** 获取当前僵尸数量 */ public int getZombieCount() { return zombies.size(); } + /** 根据网格坐标查找机枪塔实体ID,未找到返回 -1 */ + public int getTurretAtGrid(int gridX, int gridY) { + for (int turretId : turrets) { + WallEntity we = wallEntityDatas.get(turretId); + Health h = healths.get(turretId); + if (we != null && h != null && h.isAlive() + && we.getGridX() == gridX && we.getGridY() == gridY) { + return turretId; + } + } + return -1; + } + /** 获取地图网格数据 */ public int[][] getMapData() { return map.getCells(); } diff --git a/backend/src/main/java/com/zombie/game/ecs/ObjectPool.java b/backend/src/main/java/com/zombie/game/ecs/ObjectPool.java new file mode 100644 index 0000000..f79f633 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/ecs/ObjectPool.java @@ -0,0 +1,41 @@ +package com.zombie.game.ecs; + +import java.util.ArrayDeque; +import java.util.function.Supplier; + +/** + * 通用对象池 + * 避免频繁创建/销毁短生命周期对象(如子弹组件),减少 GC 压力。 + * + * @param 池化对象类型 + */ +public class ObjectPool { + + private final ArrayDeque pool; + private final Supplier factory; + + public ObjectPool(Supplier factory, int initialCapacity) { + this.factory = factory; + this.pool = new ArrayDeque<>(initialCapacity); + for (int i = 0; i < initialCapacity; i++) { + pool.push(factory.get()); + } + } + + /** 从池中获取一个对象,池空时自动创建新实例 */ + public T obtain() { + return pool.isEmpty() ? factory.get() : pool.pop(); + } + + /** 归还对象到池中(调用方需确保不再引用该对象) */ + public void free(T obj) { + if (obj != null) { + pool.push(obj); + } + } + + /** 当前池中可用对象数 */ + public int getFreeCount() { + return pool.size(); + } +} 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 index 2d2ee11..06d7564 100644 --- a/backend/src/main/java/com/zombie/game/ecs/components/Position.java +++ b/backend/src/main/java/com/zombie/game/ecs/components/Position.java @@ -15,6 +15,13 @@ public class Position { /** 朝向角度(弧度) */ private float angle; + /** 无参构造(对象池复用) */ + public Position() { + this.x = 0; + this.y = 0; + this.angle = 0; + } + public Position(float x, float y) { this.x = x; this.y = y; 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 index 2d91ac9..96490ea 100644 --- a/backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java +++ b/backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java @@ -28,6 +28,12 @@ public class ZombieAI { private long lastAttackTime; /** 上次远程攻击时间戳(毫秒,精英僵尸使用) */ private long lastRangedAttackTime; + /** 正在攻击的机枪塔实体ID(-1表示未攻击机枪塔) */ + private int attackingTurretId = -1; + /** 是否正在攻击(用于前端播放攻击动画) */ + private boolean isAttacking; + /** 攻击动画计时器(秒),>0 时持续播放攻击动画 */ + private float attackAnimTimer; public ZombieAI(String templateId) { this.templateId = templateId; diff --git a/backend/src/main/java/com/zombie/game/model/FlowField.java b/backend/src/main/java/com/zombie/game/model/FlowField.java index a716026..3ad33aa 100644 --- a/backend/src/main/java/com/zombie/game/model/FlowField.java +++ b/backend/src/main/java/com/zombie/game/model/FlowField.java @@ -186,12 +186,13 @@ public class FlowField { } /** - * 检查格子是否被阻挡 + * 检查格子是否被完全阻挡(只有静态墙不可通行) + * 坚果墙视为高代价可通行,僵尸会选择攻击而非绕路 */ private boolean isBlocked(Map walls, int gx, int gy) { if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true; Wall wall = walls.get(key(gx, gy)); - return wall != null && wall.blocksMovement(); + return wall instanceof StaticWall; } /** diff --git a/backend/src/main/java/com/zombie/game/model/GameMap.java b/backend/src/main/java/com/zombie/game/model/GameMap.java index 0a42b79..dcc0ed8 100644 --- a/backend/src/main/java/com/zombie/game/model/GameMap.java +++ b/backend/src/main/java/com/zombie/game/model/GameMap.java @@ -133,6 +133,22 @@ public class GameMap { return true; } + /** + * 添加任意类型的墙体(用于机枪塔等非坚果墙体) + * + * @param gx 格子X坐标 + * @param gy 格子Y坐标 + * @param wall 墙体对象 + * @return true 表示添加成功 + */ + public boolean addWall(int gx, int gy, Wall wall) { + if (gx < 0 || gx >= width || gy < 0 || gy >= height) return false; + if (walls.containsKey(key(gx, gy))) return false; + walls.put(key(gx, gy), wall); + flowField.invalidate(); + return true; + } + /** * 移除墙体(当坚果被破坏时调用) * diff --git a/backend/src/main/java/com/zombie/game/model/NutWall.java b/backend/src/main/java/com/zombie/game/model/NutWall.java index 50f1afd..34ef259 100644 --- a/backend/src/main/java/com/zombie/game/model/NutWall.java +++ b/backend/src/main/java/com/zombie/game/model/NutWall.java @@ -14,8 +14,8 @@ import lombok.Getter; * 这样 BFS 会自动权衡绕道 vs 摧毁。 */ public class NutWall extends Wall { - /** 坚果最大血量(20个时间单位) */ - public static final float MAX_HEALTH = 20.0f; + /** 坚果最大血量 */ + public static final float MAX_HEALTH = 500.0f; /** 坚果在流场中的额外移动代价(摧毁时间折算) */ public static final float MOVEMENT_COST_PENALTY = 20.0f; /** 当前血量 */ diff --git a/backend/src/main/java/com/zombie/game/model/TurretWall.java b/backend/src/main/java/com/zombie/game/model/TurretWall.java new file mode 100644 index 0000000..c3a478c --- /dev/null +++ b/backend/src/main/java/com/zombie/game/model/TurretWall.java @@ -0,0 +1,48 @@ +package com.zombie.game.model; + +import lombok.Getter; + +/** + * 机枪塔墙体 + * + * 用于流场寻路中将机枪塔视为障碍物。 + * 实际生命值由 ECS Health 组件管理,此类的 takeDamage 为空操作。 + * 僵尸路径经过时会攻击机枪塔实体。 + */ +public class TurretWall extends Wall { + /** 关联的机枪塔实体ID */ + @Getter + private final int turretEntityId; + /** 流场中的额外移动代价(比坚果低,僵尸更倾向攻击塔而非绕路) */ + public static final float MOVEMENT_COST_PENALTY = 5.0f; + + public TurretWall(int gridX, int gridY, int turretEntityId) { + super(gridX, gridY); + this.turretEntityId = turretEntityId; + } + + @Override + public boolean isDestructible() { + return true; + } + + @Override + public boolean isDestroyed() { + return false; + } + + @Override + public float getHealth() { + return Float.MAX_VALUE; + } + + @Override + public void takeDamage(float damage) { + // 空操作:实际HP由 ECS Health 组件管理 + } + + @Override + public float getMovementCost() { + return 1.0f + MOVEMENT_COST_PENALTY; + } +} diff --git a/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java b/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java index 969afb5..9c102ea 100644 --- a/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java +++ b/backend/src/main/java/com/zombie/game/systems/BulletMovementSystem.java @@ -52,16 +52,40 @@ public class BulletMovementSystem implements System { float z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI); if (data.getFlightTime() >= data.getMaxFlightTime() || (z <= 0.5f && progress > 0.5f)) { - // 落地 + // 落地 —— 触发爆炸伤害 + Explosive explosive = world.getExplosives().get(entityId); + if (explosive != null) { + float radius = explosive.getExplosionRadius(); + int damage = explosive.getExplosionDamage(); + + // 对范围内僵尸造成爆炸伤害 + 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 = pos.distanceTo(zombiePos.getX(), zombiePos.getY()); + if (dist < radius) { + zombieHealth.takeDamage(damage); + } + } + + // 记录爆炸效果(同步给客户端渲染) + Map explosion = new HashMap<>(); + explosion.put("x", pos.getX()); + explosion.put("y", pos.getY()); + explosion.put("radius", radius); + explosion.put("type", data.isMolotov() ? "molotov" : "grenade"); + world.getExplosions().add(explosion); + } + + // 燃烧瓶落地后创建火焰区域 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; } diff --git a/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java b/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java index 8c8a303..2cd789b 100644 --- a/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java +++ b/backend/src/main/java/com/zombie/game/systems/CollisionSystem.java @@ -51,36 +51,33 @@ public class CollisionSystem implements System { } } - // 检测与坚果墙的碰撞 + // 检测与机枪塔/坚果墙的碰撞 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)) { + // 优先检测 turret 实体(auto_turret 走实体血量) + int hitTurretId = findTurretAtGrid(world, gx, gy, bulletPos); + if (hitTurretId >= 0) { + if (!isOwnTurretBullet(data.getOwnerId(), hitTurretId)) { + Health turretHealth = world.getHealths().get(hitTurretId); + WallEntity we = world.getWallEntityDatas().get(hitTurretId); turretHealth.takeDamage(data.getDamage()); hit = true; if (!turretHealth.isAlive()) { world.getMap().removeWall(we.getGridX(), we.getGridY()); - world.destroyEntity(turretId); + world.destroyEntity(hitTurretId); + } + } + } else { + // 坚果墙碰撞(500HP,不会被一枪打掉) + 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); } - break; } } } @@ -123,36 +120,29 @@ public class CollisionSystem implements System { } } - // 检测与坚果墙的碰撞 + // 检测与机枪塔/坚果墙的碰撞 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()); + + int hitTurretId = findTurretAtGrid(world, gx, gy, bulletPos); + if (hitTurretId >= 0) { + Health turretHealth = world.getHealths().get(hitTurretId); + WallEntity we = world.getWallEntityDatas().get(hitTurretId); + turretHealth.takeDamage(data.getDamage()); hit = true; - if (wall.isDestroyed()) { - world.getMap().removeWall(gx, gy); + if (!turretHealth.isAlive()) { + world.getMap().removeWall(we.getGridX(), we.getGridY()); + world.destroyEntity(hitTurretId); } - } - } - - // 检测与机枪塔的碰撞 - 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()); + } else { + Wall wall = world.getMap().getWall(gx, gy); + if (wall instanceof NutWall && !wall.isDestroyed()) { + wall.takeDamage(data.getDamage()); hit = true; - if (!turretHealth.isAlive()) { - world.getMap().removeWall(we.getGridX(), we.getGridY()); - world.destroyEntity(turretId); + if (wall.isDestroyed()) { + world.getMap().removeWall(gx, gy); } - break; } } } @@ -169,6 +159,36 @@ public class CollisionSystem implements System { } } + /** + * 在指定网格查找活着的 turret 实体 + */ + private int findTurretAtGrid(ECSWorld world, int gx, int gy, Position bulletPos) { + for (int turretId : world.getTurrets()) { + WallEntity we = world.getWallEntityDatas().get(turretId); + Health health = world.getHealths().get(turretId); + Position pos = world.getPositions().get(turretId); + if (we == null || health == null || !health.isAlive() || pos == null) continue; + if (we.getGridX() == gx && we.getGridY() == gy && hitsEntity(bulletPos, pos, ZOMBIE_SIZE)) { + return turretId; + } + } + return -1; + } + + /** + * 判断子弹是否由该 turret 自己发射(避免自伤) + * ownerId 格式: "turret_{entityId}" + */ + private boolean isOwnTurretBullet(String ownerId, int turretId) { + if (ownerId == null) return false; + try { + if (ownerId.startsWith("turret_")) { + return Integer.parseInt(ownerId.substring(7)) == turretId; + } + } catch (NumberFormatException ignored) {} + return false; + } + /** * 判断子弹是否命中实体(基于距离检测) */ diff --git a/backend/src/main/java/com/zombie/game/systems/DamageSystem.java b/backend/src/main/java/com/zombie/game/systems/DamageSystem.java index 15563d8..3c19dda 100644 --- a/backend/src/main/java/com/zombie/game/systems/DamageSystem.java +++ b/backend/src/main/java/com/zombie/game/systems/DamageSystem.java @@ -33,6 +33,8 @@ public class DamageSystem implements System { if (dist < 1.0f && now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) { playerHealth.takeDamage(template.getDamage()); ai.setLastAttackTime(now); + ai.setAttacking(true); + ai.setAttackAnimTimer(0.5f); // 玩家死亡时启动重生计时器 if (!playerHealth.isAlive()) { @@ -87,6 +89,8 @@ public class DamageSystem implements System { template.getRangedDamage(), "zombie_" + zombieId, -1, 15, true); ai.setLastRangedAttackTime(now); + ai.setAttacking(true); + ai.setAttackAnimTimer(0.5f); } } } diff --git a/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java b/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java index c08587f..4c808ef 100644 --- a/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java +++ b/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java @@ -58,6 +58,20 @@ public class StateSyncSystem implements System { } state.put("loots", lootStates); + // 燃烧区域状态 + List> fireZoneStates = new ArrayList<>(); + for (int entityId : world.getFireZones()) { + fireZoneStates.add(buildFireZoneState(world, entityId)); + } + state.put("fireZones", fireZoneStates); + + // 机枪塔状态 + List> turretStates = new ArrayList<>(); + for (int entityId : world.getTurrets()) { + turretStates.add(buildTurretState(world, entityId)); + } + state.put("turrets", turretStates); + // 爆炸效果、移除子弹、坚果墙状态、游戏信息 state.put("explosions", new ArrayList<>(world.getExplosions())); state.put("removedBullets", new ArrayList<>(world.getRemovedBullets())); @@ -142,6 +156,7 @@ public class StateSyncSystem implements System { var template = world.getZombieTemplate(ai.getTemplateId()); map.put("isElite", template.isCanRangedAttack()); map.put("isSplitter", template.isCanSplit()); + map.put("isAttacking", ai.isAttacking()); } return map; } @@ -197,6 +212,52 @@ public class StateSyncSystem implements System { return map; } + /** 构建单个燃烧区域的状态数据 */ + private Map buildFireZoneState(ECSWorld world, int entityId) { + Map map = new LinkedHashMap<>(); + Position pos = world.getPositions().get(entityId); + FireZone fz = world.getFireZonesData().get(entityId); + + map.put("id", entityId); + if (pos != null) { + map.put("x", pos.getX()); + map.put("y", pos.getY()); + } + if (fz != null) { + map.put("radius", fz.getRadius()); + map.put("duration", fz.getDuration()); + map.put("elapsed", fz.getElapsed()); + } + return map; + } + + /** 构建单个机枪塔的状态数据 */ + private Map buildTurretState(ECSWorld world, int entityId) { + Map map = new LinkedHashMap<>(); + Position pos = world.getPositions().get(entityId); + Health health = world.getHealths().get(entityId); + WallEntity we = world.getWallEntityDatas().get(entityId); + TurretState ts = world.getTurretStates().get(entityId); + + map.put("id", entityId); + if (pos != null) { + map.put("x", pos.getX()); + map.put("y", pos.getY()); + } + if (health != null) { + map.put("health", health.getHealth()); + map.put("maxHealth", health.getMaxHealth()); + } + if (we != null) { + map.put("gridX", we.getGridX()); + map.put("gridY", we.getGridY()); + } + if (ts != null) { + map.put("isAutoTurret", ts.getFireRange() > 0); + } + return map; + } + @Override public void update(float dt, ECSWorld world) { // 空实现:本系统通过 buildGameState() 显式调用 diff --git a/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java b/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java index dff882d..7cf1c83 100644 --- a/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java +++ b/backend/src/main/java/com/zombie/game/systems/WeaponFiringSystem.java @@ -118,12 +118,14 @@ public class WeaponFiringSystem implements System { if ("molotov".equals(currentWeapon.getId())) { world.createMolotovEntity(startX, startY, targetX, targetY, flightDuration, - currentWeapon.getDamage(), playerId, currentWeapon.getExplosionRadius(), + currentWeapon.getDamage(), playerId, weapon.getWeaponIndex(), + currentWeapon.getExplosionRadius(), currentWeapon.getFireZoneRadius(), currentWeapon.getFireZoneDamage(), currentWeapon.getFireZoneDuration()); } else { world.createGrenadeEntity(startX, startY, targetX, targetY, flightDuration, - currentWeapon.getDamage(), playerId, currentWeapon.getExplosionRadius()); + currentWeapon.getDamage(), playerId, weapon.getWeaponIndex(), + currentWeapon.getExplosionRadius()); } } else if (input.isFiring() && !weapon.isChargingGrenade()) { weapon.startGrenadeCharge(); @@ -151,11 +153,8 @@ public class WeaponFiringSystem implements System { 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); + // createTurretEntity 内部会注册 TurretWall 到流场 world.createTurretEntity(gridX + 0.5f, gridY + 0.5f, gridX, gridY, currentWeapon.getTurretRange(), currentWeapon.getTurretFireRate(), currentWeapon.getTurretDamage(), currentWeapon.getTurretBulletSpeed(), diff --git a/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java b/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java index 85fc1c3..58eb970 100644 --- a/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java +++ b/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java @@ -45,25 +45,97 @@ public class ZombieMovementSystem implements System { 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()); + if (distToWall < 1.5f) { + // 每次攻击前检查流场是否有更快的路 + float[] flowDir = map.getFlowDirection(pos.getX(), pos.getY()); + int cx = (int) Math.floor(pos.getX()); + int cy = (int) Math.floor(pos.getY()); + int flowNextX = cx + Math.round(flowDir[0]); + int flowNextY = cy + Math.round(flowDir[1]); + boolean flowPointsToWall = (flowNextX == ai.getAttackingWallGridX() + && flowNextY == ai.getAttackingWallGridY()); + + if (flowPointsToWall) { + // 流场仍指向这面墙,继续攻击 + Wall wall = map.getWall(ai.getAttackingWallGridX(), ai.getAttackingWallGridY()); + if (wall != null && !wall.isDestroyed()) { + if (now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) { + int turretId = world.getTurretAtGrid(ai.getAttackingWallGridX(), ai.getAttackingWallGridY()); + if (turretId >= 0) { + Health turretHealth = world.getHealths().get(turretId); + if (turretHealth != null) { + turretHealth.takeDamage(template.getDamage()); + if (!turretHealth.isAlive()) { + world.destroyEntity(turretId); + } + } + } else { + wall.takeDamage(template.getDamage()); + if (wall.isDestroyed()) { + map.removeWall(wall.getGridX(), wall.getGridY()); + } + } + ai.setLastAttackTime(now); + ai.setAttackAnimTimer(0.5f); } + ai.setAttacking(true); + continue; } - continue; + // 墙已被摧毁,清除攻击状态 + ai.setAttackingWall(false); + ai.setAttackingWallGridX(-1); + ai.setAttackingWallGridY(-1); + } else { + // 流场不再指向这面墙,放弃攻击走更快的路 + ai.setAttackingWall(false); + ai.setAttackingWallGridX(-1); + ai.setAttackingWallGridY(-1); } + } else { + // 距离太远,清除攻击状态 ai.setAttackingWall(false); ai.setAttackingWallGridX(-1); ai.setAttackingWallGridY(-1); } } + // 处理机枪塔攻击状态 + if (ai.getAttackingTurretId() >= 0) { + Health turretHealth = world.getHealths().get(ai.getAttackingTurretId()); + Position turretPos = world.getPositions().get(ai.getAttackingTurretId()); + if (turretHealth != null && turretHealth.isAlive() && turretPos != null) { + float distToTurret = pos.distanceTo(turretPos.getX(), turretPos.getY()); + if (distToTurret < 1.5f) { + // 检查流场是否有更快的路 + float[] flowDir = map.getFlowDirection(pos.getX(), pos.getY()); + int cx = (int) Math.floor(pos.getX()); + int cy = (int) Math.floor(pos.getY()); + int flowNextX = cx + Math.round(flowDir[0]); + int flowNextY = cy + Math.round(flowDir[1]); + int turretGridX = (int) Math.floor(turretPos.getX()); + int turretGridY = (int) Math.floor(turretPos.getY()); + boolean flowPointsToTurret = (flowNextX == turretGridX && flowNextY == turretGridY); + + if (flowPointsToTurret) { + if (now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) { + turretHealth.takeDamage(template.getDamage()); + ai.setLastAttackTime(now); + ai.setAttackAnimTimer(0.5f); + if (!turretHealth.isAlive()) { + world.destroyEntity(ai.getAttackingTurretId()); + ai.setAttackingTurretId(-1); + } + } + ai.setAttacking(true); + continue; + } + // 流场不再指向这个机枪塔,放弃攻击 + ai.setAttackingTurretId(-1); + } + } + ai.setAttackingTurretId(-1); + } + // 目标选择 float centerDist = Float.MAX_VALUE; if (ai.isHasTarget()) { @@ -94,12 +166,21 @@ public class ZombieMovementSystem implements System { 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); + // 攻击时不占位,让其他僵尸可以穿过当前位置 + ai.setReservation(false); + ai.setHasTarget(false); + ai.setAttacking(true); + ai.setAttackAnimTimer(0.5f); + continue; + } else if (world.getTurretAtGrid(nextGridX, nextGridY) >= 0) { + // 下一格有机枪塔,进入攻击模式 + int turretId = world.getTurretAtGrid(nextGridX, nextGridY); + ai.setAttackingTurretId(turretId); + ai.setReservation(false); + ai.setHasTarget(false); + ai.setAttacking(true); + ai.setAttackAnimTimer(0.5f); + continue; } else if (map.isWall(nextGridX, nextGridY)) { nextGridX = currentGridX + (int) Math.signum(dirX); nextGridY = currentGridY + (int) Math.signum(dirY); @@ -213,6 +294,15 @@ public class ZombieMovementSystem implements System { if (canMoveY) pos.setY(newY); } + // 攻击动画计时器递减,保持动画持续播放 + if (ai.getAttackAnimTimer() > 0) { + ai.setAttackAnimTimer(ai.getAttackAnimTimer() - dt); + ai.setAttacking(true); + } else { + ai.setAttacking(false); + ai.setAttackAnimTimer(0); + } + // 僵尸间分离(防止重叠) for (int otherId : world.getZombies()) { if (otherId == entityId) continue; diff --git a/backend/src/main/resources/data/weapons.json b/backend/src/main/resources/data/weapons.json index 0ff20cb..d1946bc 100644 --- a/backend/src/main/resources/data/weapons.json +++ b/backend/src/main/resources/data/weapons.json @@ -102,7 +102,7 @@ "spread": 0, "bulletSpeed": 12, "range": 12, - "maxAmmo": 5, + "maxAmmo": 10, "chargeable": true, "explosive": true, "explosionRadius": 2.0, @@ -125,14 +125,14 @@ "spread": 0, "bulletSpeed": 0, "range": 0, - "maxAmmo": 3, + "maxAmmo": 10, "chargeable": false, "explosive": false, "explosionRadius": 0, "fireZoneRadius": 0, "fireZoneDamage": 0, "fireZoneDuration": 0, - "turretHealth": 20, + "turretHealth": 500, "turretRange": 0, "turretFireRate": 0, "turretDamage": 0, @@ -148,7 +148,7 @@ "spread": 0, "bulletSpeed": 0, "range": 0, - "maxAmmo": 2, + "maxAmmo": 5, "chargeable": false, "explosive": false, "explosionRadius": 0, diff --git a/frontend/src/game/engine.js b/frontend/src/game/engine.js index 6aa8848..804ca48 100644 --- a/frontend/src/game/engine.js +++ b/frontend/src/game/engine.js @@ -34,6 +34,8 @@ export class GameEngine { this.zombieBullets = new Map() // 掉落物列表 this.loots = new Map() + // 机枪塔列表 + this.turrets = new Map() // 游戏运行状态 this.running = false @@ -55,9 +57,9 @@ export class GameEngine { [WEAPONS.MACHINE_GUN]: 100, [WEAPONS.SHOTGUN]: 20, [WEAPONS.GRENADE]: 10, - [WEAPONS.MOLOTOV]: 5, - [WEAPONS.NUT_WALL]: 3, - [WEAPONS.AUTO_TURRET]: 2 + [WEAPONS.MOLOTOV]: 10, + [WEAPONS.NUT_WALL]: 10, + [WEAPONS.AUTO_TURRET]: 5 } // 手雷蓄力相关 @@ -396,7 +398,7 @@ export class GameEngine { zombie.y = zs.y zombie.health = zs.health const angle = zs.angle || 0 - this.scene.updateZombie(zs.id, zs.x, zs.y, angle, zs.health) + this.scene.updateZombie(zs.id, zs.x, zs.y, angle, zs.health, zs.isAttacking || false) // 受伤特效 if (prevHealth > zs.health && zs.health > 0) { this.scene.addHitEffect(zs.x, zs.y) @@ -465,13 +467,46 @@ export class GameEngine { // 爆炸效果 if (state.explosions) { - console.log('Explosions received:', state.explosions) for (const exp of state.explosions) { - console.log('Creating explosion at:', exp.x, exp.y, 'radius:', exp.radius) this.scene.addExplosion(exp.x, exp.y, exp.radius || 3) } } + // 燃烧区域 + if (state.fireZones) { + const serverIds = new Set(state.fireZones.map(f => f.id)) + // 新增或更新 + for (const fz of state.fireZones) { + this.scene.addFireZone(fz.id, fz.x, fz.y, fz.radius || 2, fz.duration || 5, fz.elapsed || 0) + } + // 移除已消失的 + for (const [id] of this.scene.fireZones) { + if (!serverIds.has(id)) { + this.scene.removeFireZone(id) + } + } + } + + // 机枪塔 + if (state.turrets) { + const serverIds = new Set(state.turrets.map(t => t.id)) + for (const ts of state.turrets) { + if (!this.turrets.has(ts.id)) { + this.turrets.set(ts.id, ts) + this.scene.addTurret(ts.id, ts.x, ts.y, ts.health, ts.maxHealth, ts.isAutoTurret) + } else { + this.turrets.set(ts.id, ts) + this.scene.updateTurret(ts.id, ts.health) + } + } + for (const [id] of this.turrets) { + if (!serverIds.has(id)) { + this.turrets.delete(id) + this.scene.removeTurret(id) + } + } + } + // 命中效果 if (state.hits) { for (const hit of state.hits) { diff --git a/frontend/src/game/scene.js b/frontend/src/game/scene.js index 8168ed9..9716353 100644 --- a/frontend/src/game/scene.js +++ b/frontend/src/game/scene.js @@ -31,6 +31,8 @@ export class GameScene { this.bullets = [] // 玩家子弹数组 this.zombieBullets = [] // 僵尸子弹数组 this.loots = new Map() // Map + this.fireZones = new Map() // Map @@ -413,18 +415,24 @@ export class GameScene { head.castShadow = true group.add(head) - // 手臂 - const armGeo = new THREE.BoxGeometry(0.12, 0.6, 0.12) + // 手臂(比身体宽,确保从各个角度都可见) + const armGeo = new THREE.BoxGeometry(0.18, 0.7, 0.18) const armMat = new THREE.MeshLambertMaterial({ color: armColor }) const leftArm = new THREE.Mesh(armGeo, armMat) - leftArm.position.set(-0.35, 0.5, 0.2) + leftArm.position.set(-0.5, 0.5, 0.0) leftArm.rotation.x = -0.5 + leftArm.castShadow = true group.add(leftArm) const rightArm = new THREE.Mesh(armGeo, armMat) - rightArm.position.set(0.35, 0.5, 0.2) + rightArm.position.set(0.5, 0.5, 0.0) rightArm.rotation.x = -0.5 + rightArm.castShadow = true group.add(rightArm) + // 存储手臂引用供攻击动画使用 + group.userData.leftArm = leftArm + group.userData.rightArm = rightArm + // 精英僵尸发光效果 if (isElite) { const glowGeo = new THREE.SphereGeometry(0.6, 8, 8) @@ -514,7 +522,12 @@ export class GameScene { const model = this.createZombieModel(isElite, isSplitter) model.position.set(x, 0, y) this.scene.add(model) - this.zombies.set(id, { model, isElite, isSplitter }) + this.zombies.set(id, { + model, isElite, isSplitter, + leftArm: model.userData.leftArm, + rightArm: model.userData.rightArm, + isAttacking: false + }) } /** @@ -534,19 +547,34 @@ export class GameScene { } /** - * 更新僵尸位置和角度 + * 更新僵尸位置、角度和攻击动画 * @param {string} id 僵尸ID * @param {number} x X坐标 * @param {number} y Y坐标 * @param {number} angle 旋转角度 * @param {number} health 生命值 + * @param {boolean} isAttacking 是否正在攻击 */ - updateZombie(id, x, y, angle, health) { + updateZombie(id, x, y, angle, health, isAttacking = false) { const zombie = this.zombies.get(id) if (zombie) { zombie.model.position.x = x zombie.model.position.z = y zombie.model.rotation.y = angle + + // 攻击动画:手臂前后挥动 + if (zombie.leftArm && zombie.rightArm) { + if (isAttacking) { + const t = Date.now() * 0.012 + const swing = Math.sin(t) * 1.0 + zombie.leftArm.rotation.x = -1.5 + swing + zombie.rightArm.rotation.x = -1.5 - swing + } else { + zombie.leftArm.rotation.x = -0.5 + zombie.rightArm.rotation.x = -0.5 + } + } + zombie.isAttacking = isAttacking } } @@ -864,6 +892,164 @@ export class GameScene { } } + /** + * 添加火焰区域效果 + * @param {number} id 火焰区域ID + * @param {number} x X坐标 + * @param {number} y Y坐标 + * @param {number} radius 火焰半径 + * @param {number} duration 持续时间(秒) + * @param {number} elapsed 已经过时间(秒) + */ + addFireZone(id, x, y, radius, duration, elapsed) { + if (this.fireZones.has(id)) return + + // 火焰地面圆 + const geo = new THREE.CircleGeometry(radius, 24) + const mat = new THREE.MeshBasicMaterial({ + color: 0xff4400, + transparent: true, + opacity: 0.5, + side: THREE.DoubleSide + }) + const mesh = new THREE.Mesh(geo, mat) + mesh.rotation.x = -Math.PI / 2 + mesh.position.set(x, 0.05, y) + this.scene.add(mesh) + + // 火焰光源 + const light = new THREE.PointLight(0xff6600, 2, radius * 3) + light.position.set(x, 1, y) + this.scene.add(light) + + this.fireZones.set(id, { mesh, light, startTime: Date.now() - elapsed * 1000, duration: duration * 1000 }) + } + + /** + * 移除火焰区域效果 + * @param {number} id 火焰区域ID + */ + removeFireZone(id) { + const fz = this.fireZones.get(id) + if (!fz) return + this.scene.remove(fz.mesh) + this.scene.remove(fz.light) + fz.mesh.geometry.dispose() + fz.mesh.material.dispose() + this.fireZones.delete(id) + } + + /** + * 添加机枪塔渲染 + * @param {number} id 实体ID + * @param {number} x X坐标 + * @param {number} y Y坐标 + * @param {number} health 当前生命值 + * @param {number} maxHealth 最大生命值 + * @param {boolean} isAutoTurret 是否为自动机枪塔(false则为坚果墙塔基) + */ + addTurret(id, x, y, health, maxHealth, isAutoTurret) { + if (this.turrets.has(id)) return + + const group = new THREE.Group() + + // 塔基(底座) + const baseGeo = new THREE.CylinderGeometry(0.35, 0.4, 0.3, 8) + const baseMat = new THREE.MeshLambertMaterial({ color: 0x555555 }) + const base = new THREE.Mesh(baseGeo, baseMat) + base.position.y = 0.15 + base.castShadow = true + group.add(base) + + if (isAutoTurret) { + // 机身 + const bodyGeo = new THREE.CylinderGeometry(0.2, 0.25, 0.4, 8) + const bodyMat = new THREE.MeshLambertMaterial({ color: 0x336633 }) + const body = new THREE.Mesh(bodyGeo, bodyMat) + body.position.y = 0.5 + body.castShadow = true + group.add(body) + + // 枪管 + const barrelGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.5, 6) + const barrelMat = new THREE.MeshLambertMaterial({ color: 0x222222 }) + const barrel = new THREE.Mesh(barrelGeo, barrelMat) + barrel.rotation.x = Math.PI / 2 + barrel.position.set(0, 0.55, 0.25) + barrel.castShadow = true + group.add(barrel) + + // 炮塔顶部 + const topGeo = new THREE.SphereGeometry(0.15, 8, 6) + const topMat = new THREE.MeshLambertMaterial({ color: 0x446644 }) + const top = new THREE.Mesh(topGeo, topMat) + top.position.y = 0.75 + group.add(top) + } else { + // 坚果墙塔基 - 木箱外观 + const boxGeo = new THREE.BoxGeometry(0.7, 0.8, 0.7) + const boxMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 }) + const box = new THREE.Mesh(boxGeo, boxMat) + box.position.y = 0.7 + box.castShadow = true + group.add(box) + } + + // 血条背景 + const barBgGeo = new THREE.PlaneGeometry(0.8, 0.08) + const barBgMat = new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide }) + const barBg = new THREE.Mesh(barBgGeo, barBgMat) + barBg.position.set(0, 1.3, 0) + barBg.rotation.x = -0.3 + group.add(barBg) + + // 血条前景 + const barGeo = new THREE.PlaneGeometry(0.78, 0.06) + const barMat = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide }) + const healthBar = new THREE.Mesh(barGeo, barMat) + healthBar.position.set(0, 1.3, 0.01) + healthBar.rotation.x = -0.3 + group.add(healthBar) + + group.position.set(x, 0, y) + this.scene.add(group) + this.turrets.set(id, { group, healthBar, healthBarBg: barBg, isAutoTurret, maxHealth }) + } + + /** + * 更新机枪塔血量显示 + */ + updateTurret(id, health) { + const turret = this.turrets.get(id) + if (!turret) return + + const pct = Math.max(0, health / turret.maxHealth) + turret.healthBar.scale.x = pct + turret.healthBar.position.x = -(1 - pct) * 0.39 + + if (pct > 0.5) { + turret.healthBar.material.color.setHex(0x00ff00) + } else if (pct > 0.25) { + turret.healthBar.material.color.setHex(0xffff00) + } else { + turret.healthBar.material.color.setHex(0xff0000) + } + } + + /** + * 移除机枪塔 + */ + removeTurret(id) { + const turret = this.turrets.get(id) + if (!turret) return + this.scene.remove(turret.group) + turret.group.traverse(child => { + if (child.geometry) child.geometry.dispose() + if (child.material) child.material.dispose() + }) + this.turrets.delete(id) + } + /** * 添加枪口火焰效果 * @param {number} x X坐标 @@ -1084,6 +1270,19 @@ export class GameScene { } } + // 更新火焰区域闪烁效果 + for (const [id, fz] of this.fireZones) { + const elapsed = (now - fz.startTime) / 1000 + if (elapsed * 1000 >= fz.duration) { + this.removeFireZone(id) + } else { + // 火焰闪烁 + const flicker = 0.3 + Math.sin(now * 0.01 + id) * 0.15 + Math.sin(now * 0.023) * 0.1 + fz.mesh.material.opacity = flicker + fz.light.intensity = 1.5 + Math.sin(now * 0.015) * 0.5 + } + } + // 更新子弹拖尾 for (const bullet of this.bullets) { if (!bullet.isGrenade && !bullet.isMolotov && bullet.trail) { diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index f5af7a4..285b122 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -94,8 +94,8 @@ export const WEAPON_CONFIG = { name: 'Molotov', damage: 80, fireRate: 2000, - ammo: 5, - maxAmmo: 5, + ammo: 10, + maxAmmo: 10, speed: 12, spread: 0, pellets: 1, @@ -110,8 +110,8 @@ export const WEAPON_CONFIG = { name: 'Wall', damage: 0, fireRate: 1000, - ammo: 3, - maxAmmo: 3, + ammo: 10, + maxAmmo: 10, speed: 0, spread: 0, pellets: 0, @@ -124,8 +124,8 @@ export const WEAPON_CONFIG = { name: 'Turret', damage: 0, fireRate: 2000, - ammo: 2, - maxAmmo: 2, + ammo: 5, + maxAmmo: 5, speed: 0, spread: 0, pellets: 0,