fixup
This commit is contained in:
@@ -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<Integer, WallEntity> wallEntityDatas = new HashMap<>();
|
||||
|
||||
// ==================== 对象池(子弹组件) ====================
|
||||
private static final int BULLET_POOL_SIZE = 100;
|
||||
private final ObjectPool<Position> positionPool = new ObjectPool<>(Position::new, BULLET_POOL_SIZE);
|
||||
private final ObjectPool<Velocity> velocityPool = new ObjectPool<>(() -> new Velocity(0, 0, 0), BULLET_POOL_SIZE);
|
||||
private final ObjectPool<BulletData> bulletDataPool = new ObjectPool<>(() -> new BulletData(0, "", 0, 0), BULLET_POOL_SIZE);
|
||||
private final ObjectPool<Collision> collisionPool = new ObjectPool<>(() -> new Collision(0), BULLET_POOL_SIZE);
|
||||
private final ObjectPool<RenderInfo> renderInfoPool = new ObjectPool<>(() -> new RenderInfo(RenderInfo.EntityType.BULLET), BULLET_POOL_SIZE);
|
||||
|
||||
// ==================== 系统列表 ====================
|
||||
private final List<System> 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(); }
|
||||
|
||||
|
||||
41
backend/src/main/java/com/zombie/game/ecs/ObjectPool.java
Normal file
41
backend/src/main/java/com/zombie/game/ecs/ObjectPool.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.zombie.game.ecs;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 通用对象池
|
||||
* 避免频繁创建/销毁短生命周期对象(如子弹组件),减少 GC 压力。
|
||||
*
|
||||
* @param <T> 池化对象类型
|
||||
*/
|
||||
public class ObjectPool<T> {
|
||||
|
||||
private final ArrayDeque<T> pool;
|
||||
private final Supplier<T> factory;
|
||||
|
||||
public ObjectPool(Supplier<T> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -186,12 +186,13 @@ public class FlowField {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查格子是否被阻挡
|
||||
* 检查格子是否被完全阻挡(只有静态墙不可通行)
|
||||
* 坚果墙视为高代价可通行,僵尸会选择攻击而非绕路
|
||||
*/
|
||||
private boolean isBlocked(Map<String, Wall> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除墙体(当坚果被破坏时调用)
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
/** 当前血量 */
|
||||
|
||||
48
backend/src/main/java/com/zombie/game/model/TurretWall.java
Normal file
48
backend/src/main/java/com/zombie/game/model/TurretWall.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
// 落地
|
||||
if (data.isMolotov()) {
|
||||
// 落地 —— 触发爆炸伤害
|
||||
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<String, Object> 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;
|
||||
}
|
||||
|
||||
@@ -51,10 +51,26 @@ public class CollisionSystem implements System {
|
||||
}
|
||||
}
|
||||
|
||||
// 检测与坚果墙的碰撞
|
||||
// 检测与机枪塔/坚果墙的碰撞
|
||||
if (!hit) {
|
||||
int gx = (int) Math.floor(bulletPos.getX());
|
||||
int gy = (int) Math.floor(bulletPos.getY());
|
||||
|
||||
// 优先检测 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(hitTurretId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 坚果墙碰撞(500HP,不会被一枪打掉)
|
||||
Wall wall = world.getMap().getWall(gx, gy);
|
||||
if (wall instanceof NutWall && !wall.isDestroyed()) {
|
||||
wall.takeDamage(data.getDamage());
|
||||
@@ -64,25 +80,6 @@ public class CollisionSystem implements System {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测与机枪塔的碰撞
|
||||
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) {
|
||||
@@ -123,10 +120,22 @@ public class CollisionSystem implements System {
|
||||
}
|
||||
}
|
||||
|
||||
// 检测与坚果墙的碰撞
|
||||
// 检测与机枪塔/坚果墙的碰撞
|
||||
if (!hit) {
|
||||
int gx = (int) Math.floor(bulletPos.getX());
|
||||
int gy = (int) Math.floor(bulletPos.getY());
|
||||
|
||||
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 (!turretHealth.isAlive()) {
|
||||
world.getMap().removeWall(we.getGridX(), we.getGridY());
|
||||
world.destroyEntity(hitTurretId);
|
||||
}
|
||||
} else {
|
||||
Wall wall = world.getMap().getWall(gx, gy);
|
||||
if (wall instanceof NutWall && !wall.isDestroyed()) {
|
||||
wall.takeDamage(data.getDamage());
|
||||
@@ -136,25 +145,6 @@ public class CollisionSystem implements System {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测与机枪塔的碰撞
|
||||
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) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断子弹是否命中实体(基于距离检测)
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,20 @@ public class StateSyncSystem implements System {
|
||||
}
|
||||
state.put("loots", lootStates);
|
||||
|
||||
// 燃烧区域状态
|
||||
List<Map<String, Object>> fireZoneStates = new ArrayList<>();
|
||||
for (int entityId : world.getFireZones()) {
|
||||
fireZoneStates.add(buildFireZoneState(world, entityId));
|
||||
}
|
||||
state.put("fireZones", fireZoneStates);
|
||||
|
||||
// 机枪塔状态
|
||||
List<Map<String, Object>> 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<String, Object> buildFireZoneState(ECSWorld world, int entityId) {
|
||||
Map<String, Object> 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<String, Object> buildTurretState(ECSWorld world, int entityId) {
|
||||
Map<String, Object> 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() 显式调用
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -45,23 +45,95 @@ public class ZombieMovementSystem implements System {
|
||||
float wallCenterY = ai.getAttackingWallGridY() + 0.5f;
|
||||
float distToWall = pos.distanceTo(wallCenterX, wallCenterY);
|
||||
|
||||
if (distToWall < 0.8f) {
|
||||
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) {
|
||||
wall.takeDamage(1.0f);
|
||||
ai.setLastAttackTime(now);
|
||||
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;
|
||||
}
|
||||
// 墙已被摧毁,清除攻击状态
|
||||
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);
|
||||
}
|
||||
|
||||
// 目标选择
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -31,6 +31,8 @@ export class GameScene {
|
||||
this.bullets = [] // 玩家子弹数组
|
||||
this.zombieBullets = [] // 僵尸子弹数组
|
||||
this.loots = new Map() // Map<lootId, {mesh, type}>
|
||||
this.fireZones = new Map() // Map<fireZoneId, {mesh, light, startTime, duration}
|
||||
this.turrets = new Map() // Map<turretId, {group, healthBar, healthBarBg, isAutoTurret}
|
||||
this.effects = [] // 特效数组
|
||||
this.wallMeshes = [] // 墙壁网格
|
||||
this.nutWalls = new Map() // Map<key, {mesh, healthBar}>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user