迁移证esc
This commit is contained in:
@@ -1,173 +0,0 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/**
|
||||
* 子弹/投掷物类
|
||||
*
|
||||
* 管理玩家和僵尸发射的子弹、手榴弹等投掷物。
|
||||
* 支持普通子弹的直线飞行和手榴弹的抛物线轨迹。
|
||||
*/
|
||||
@Getter
|
||||
public class Bullet {
|
||||
private int id;
|
||||
private float x, y;
|
||||
private float z;
|
||||
private float vx, vy, vz;
|
||||
private float speed;
|
||||
private int damage;
|
||||
private String ownerId;
|
||||
private int weaponIndex;
|
||||
private float range;
|
||||
private float distanceTraveled;
|
||||
private boolean explosive;
|
||||
private float explosionRadius;
|
||||
private float flightTime;
|
||||
private float maxFlightTime;
|
||||
private float targetX, targetY;
|
||||
private boolean isGrenade;
|
||||
|
||||
public Bullet(int id, float x, float y, float angle, float speed, int damage,
|
||||
String ownerId, int weaponIndex, float range) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = 0.5f;
|
||||
this.speed = speed;
|
||||
this.vx = (float) Math.sin(angle) * speed;
|
||||
this.vy = (float) Math.cos(angle) * speed;
|
||||
this.vz = 0;
|
||||
this.damage = damage;
|
||||
this.ownerId = ownerId;
|
||||
this.weaponIndex = weaponIndex;
|
||||
this.range = range;
|
||||
this.distanceTraveled = 0;
|
||||
this.explosive = false;
|
||||
this.explosionRadius = 0;
|
||||
this.flightTime = 0;
|
||||
this.maxFlightTime = 0;
|
||||
this.isGrenade = false;
|
||||
this.targetX = x + (float) Math.sin(angle) * range;
|
||||
this.targetY = y + (float) Math.cos(angle) * range;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数 - 手榴弹
|
||||
*
|
||||
* @param id 子弹ID
|
||||
* @param startX 起始X坐标
|
||||
* @param startY 起始Y坐标
|
||||
* @param targetX 目标X坐标
|
||||
* @param targetY 目标Y坐标
|
||||
* @param flightDuration 飞行时长
|
||||
* @param damage 伤害值
|
||||
* @param ownerId 发射者ID
|
||||
* @param explosionRadius 爆炸半径
|
||||
*/
|
||||
public Bullet(int id, float startX, float startY, float targetX, float targetY,
|
||||
float flightDuration, int damage, String ownerId, float explosionRadius) {
|
||||
this.id = id;
|
||||
this.x = startX;
|
||||
this.y = startY;
|
||||
this.z = 0.5f;
|
||||
this.targetX = targetX;
|
||||
this.targetY = targetY;
|
||||
this.damage = damage;
|
||||
this.ownerId = ownerId;
|
||||
this.weaponIndex = -1;
|
||||
this.range = 0;
|
||||
this.distanceTraveled = 0;
|
||||
this.explosive = true;
|
||||
this.explosionRadius = explosionRadius;
|
||||
this.flightTime = 0;
|
||||
this.maxFlightTime = flightDuration;
|
||||
this.isGrenade = true;
|
||||
this.speed = 0;
|
||||
|
||||
float dx = targetX - startX;
|
||||
float dy = targetY - startY;
|
||||
this.vx = dx / flightDuration;
|
||||
this.vy = dy / flightDuration;
|
||||
this.vz = 3.0f;
|
||||
}
|
||||
|
||||
public boolean update(float dt, GameMap map) {
|
||||
if (isGrenade) {
|
||||
flightTime += dt;
|
||||
|
||||
x += vx * dt;
|
||||
y += vy * dt;
|
||||
|
||||
float progress = flightTime / maxFlightTime;
|
||||
z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
|
||||
|
||||
if (flightTime >= maxFlightTime || z <= 0.5f && progress > 0.5f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
float moveX = vx * dt;
|
||||
float moveY = vy * dt;
|
||||
x += moveX;
|
||||
y += moveY;
|
||||
distanceTraveled += (float) Math.sqrt(moveX * moveX + moveY * moveY);
|
||||
|
||||
if (distanceTraveled >= range) return false;
|
||||
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false;
|
||||
|
||||
int gx = (int) Math.floor(x);
|
||||
int gy = (int) Math.floor(y);
|
||||
// 只有碰到静态墙壁才销毁子弹,碰到坚果墙体让碰撞检测处理
|
||||
Wall wall = map.getWall(gx, gy);
|
||||
if (wall instanceof StaticWall) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测子弹是否命中实体
|
||||
*
|
||||
* @param ex 实体X坐标
|
||||
* @param ey 实体Y坐标
|
||||
* @param size 实体碰撞体大小
|
||||
* @return true 表示命中
|
||||
*/
|
||||
public boolean hitsEntity(float ex, float ey, float size) {
|
||||
float dx = x - ex;
|
||||
float dy = y - ey;
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
return dist < size / 2 + 0.1f;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将子弹状态转换为Map格式,用于网络传输
|
||||
*
|
||||
* @return 包含子弹状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("x", x);
|
||||
map.put("y", y);
|
||||
map.put("z", z);
|
||||
if (isGrenade) {
|
||||
map.put("angle", (float) Math.atan2(vx, vy));
|
||||
map.put("targetX", targetX);
|
||||
map.put("targetY", targetY);
|
||||
} else {
|
||||
map.put("angle", (float) Math.atan2(vx, vy));
|
||||
}
|
||||
map.put("weaponIndex", weaponIndex);
|
||||
map.put("ownerId", ownerId);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,693 +0,0 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import com.zombie.game.template.TemplateManager;
|
||||
import com.zombie.game.template.ZombieTemplate;
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/**
|
||||
* 游戏世界类
|
||||
*
|
||||
* 管理整个游戏状态,包括:
|
||||
* - 玩家、僵尸、子弹、掉落物等实体
|
||||
* - 游戏时间、波数、分数
|
||||
* - 难度递增系统
|
||||
* - 碰撞检测和游戏逻辑更新
|
||||
*/
|
||||
@Getter
|
||||
public class GameWorld {
|
||||
private final Object lock = new Object();
|
||||
private GameMap map;
|
||||
private Map<String, Player> players;
|
||||
private Map<Integer, Zombie> zombies;
|
||||
private Map<Integer, Bullet> bullets;
|
||||
private Map<Integer, Bullet> zombieBullets;
|
||||
private Map<Integer, Loot> loots;
|
||||
private int nextBulletId;
|
||||
private int nextZombieId;
|
||||
private int nextLootId;
|
||||
private int nextZombieBulletId;
|
||||
private float gameTime;
|
||||
private int waveNumber;
|
||||
private int score;
|
||||
private float spawnTimer;
|
||||
private float difficultyTimer;
|
||||
private float zombieHealth;
|
||||
private float zombieSpeed;
|
||||
private float spawnInterval;
|
||||
private Random random;
|
||||
private List<Map<String, Object>> explosions;
|
||||
private List<Integer> removedBullets;
|
||||
private List<Integer> removedZombieBullets;
|
||||
|
||||
private static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
|
||||
private static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.2f;
|
||||
private static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
|
||||
private static final float ZOMBIE_HEALTH_INCREASE = 20;
|
||||
private static final float ZOMBIE_SPEED_INCREASE = 0.1f;
|
||||
private static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
|
||||
|
||||
public GameWorld() {
|
||||
this("/Users/wfz/workspace/zp1/maps/d540209a.json");
|
||||
}
|
||||
|
||||
public GameWorld(String mapFilePath) {
|
||||
this.map = new GameMap(mapFilePath);
|
||||
this.players = new LinkedHashMap<>();
|
||||
this.zombies = new LinkedHashMap<>();
|
||||
this.bullets = new LinkedHashMap<>();
|
||||
this.zombieBullets = new LinkedHashMap<>();
|
||||
this.loots = new LinkedHashMap<>();
|
||||
this.nextBulletId = 0;
|
||||
this.nextZombieId = 0;
|
||||
this.nextLootId = 0;
|
||||
this.nextZombieBulletId = 0;
|
||||
this.gameTime = 0;
|
||||
this.waveNumber = 0;
|
||||
this.score = 0;
|
||||
this.spawnTimer = 0;
|
||||
this.difficultyTimer = 0;
|
||||
ZombieTemplate normal = TemplateManager.getInstance().getZombieTemplate("normal");
|
||||
this.zombieHealth = normal.getBaseHealth();
|
||||
this.zombieSpeed = normal.getBaseSpeed();
|
||||
this.spawnInterval = ZOMBIE_SPAWN_INTERVAL_BASE;
|
||||
this.random = new Random();
|
||||
this.explosions = new ArrayList<>();
|
||||
this.removedBullets = new ArrayList<>();
|
||||
this.removedZombieBullets = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加玩家到游戏世界
|
||||
*
|
||||
* @param player 玩家对象
|
||||
*/
|
||||
public void addPlayer(Player player) {
|
||||
List<int[]> spawnPoints = map.getSpawnPoints();
|
||||
int idx = players.size() % spawnPoints.size();
|
||||
int[] sp = spawnPoints.get(idx);
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
player.applyMovement(0, 0, map);
|
||||
players.put(player.getId(), player);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从游戏世界移除玩家
|
||||
*
|
||||
* @param playerId 玩家ID
|
||||
*/
|
||||
public void removePlayer(String playerId) {
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
/** 获取指定ID的玩家 */
|
||||
public Player getPlayer(String id) { return players.get(id); }
|
||||
/** 获取所有玩家 */
|
||||
public Collection<Player> getPlayers() { return players.values(); }
|
||||
/** 获取所有僵尸 */
|
||||
public Collection<Zombie> getZombies() { return zombies.values(); }
|
||||
/** 获取所有玩家子弹 */
|
||||
public Collection<Bullet> getBullets() { return bullets.values(); }
|
||||
/** 获取所有掉落物 */
|
||||
public Collection<Loot> getLoots() { return loots.values(); }
|
||||
|
||||
/**
|
||||
* 更新游戏世界状态
|
||||
*
|
||||
* 每帧调用,处理:
|
||||
* - 时间流逝和难度提升
|
||||
* - 僵尸生成
|
||||
* - 实体移动和碰撞
|
||||
* - 掉落物收集
|
||||
* - 玩家重生
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
public void update(float dt) {
|
||||
explosions.clear();
|
||||
removedBullets.clear();
|
||||
removedZombieBullets.clear();
|
||||
|
||||
gameTime += dt;
|
||||
spawnTimer += dt;
|
||||
difficultyTimer += dt;
|
||||
|
||||
updateFlowField();
|
||||
|
||||
if (difficultyTimer >= DIFFICULTY_INCREASE_INTERVAL) {
|
||||
difficultyTimer -= DIFFICULTY_INCREASE_INTERVAL;
|
||||
waveNumber++;
|
||||
spawnInterval = Math.max(ZOMBIE_SPAWN_INTERVAL_MIN,
|
||||
spawnInterval - 0.3f);
|
||||
}
|
||||
|
||||
if (spawnTimer >= spawnInterval) {
|
||||
spawnTimer -= spawnInterval;
|
||||
spawnZombie();
|
||||
}
|
||||
|
||||
updateZombies(dt);
|
||||
updateBullets(dt);
|
||||
updateZombieBullets(dt);
|
||||
checkBulletCollisions();
|
||||
checkZombieBulletCollisions();
|
||||
checkZombieAttacks();
|
||||
checkLootCollection();
|
||||
checkPlayerRespawn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新流场导航
|
||||
*
|
||||
* 基于存活玩家的位置更新流场
|
||||
*/
|
||||
private void updateFlowField() {
|
||||
List<float[]> playerPositions = new ArrayList<>();
|
||||
for (Player p : players.values()) {
|
||||
if (p.isAlive()) {
|
||||
playerPositions.add(new float[]{p.getX(), p.getY()});
|
||||
}
|
||||
}
|
||||
if (!playerPositions.isEmpty()) {
|
||||
map.updateFlowField(playerPositions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成僵尸
|
||||
*
|
||||
* 根据概率生成普通僵尸、精英僵尸或分裂僵尸
|
||||
*/
|
||||
private void spawnZombie() {
|
||||
List<int[]> spawnPoints = map.getZombieSpawnPoints();
|
||||
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
|
||||
ZombieTemplate elite = TemplateManager.getInstance().getZombieTemplate("elite");
|
||||
ZombieTemplate splitter = TemplateManager.getInstance().getZombieTemplate("splitter");
|
||||
float eliteChance = elite.getSpawnWeight();
|
||||
float splitterChance = splitter.getSpawnWeight();
|
||||
|
||||
float roll = random.nextFloat();
|
||||
Zombie zombie;
|
||||
if (roll < eliteChance) {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, "elite");
|
||||
} else if (roll < eliteChance + splitterChance) {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, "splitter");
|
||||
} else {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, "normal");
|
||||
}
|
||||
zombies.put(zombie.getId(), zombie);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找最近的存活玩家
|
||||
*
|
||||
* @param x X坐标
|
||||
* @param y Y坐标
|
||||
* @return 最近的玩家,如果没有存活玩家则返回 null
|
||||
*/
|
||||
private Player findNearestPlayer(float x, float y) {
|
||||
Player nearest = null;
|
||||
float minDist = Float.MAX_VALUE;
|
||||
for (Player p : players.values()) {
|
||||
if (!p.isAlive()) continue;
|
||||
float dist = p.distanceTo(x, y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = p;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有僵尸
|
||||
*
|
||||
* 处理僵尸移动、攻击和死亡
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateZombies(float dt) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
List<Zombie> sortedZombies = new ArrayList<>(zombies.values());
|
||||
sortedZombies.sort((a, b) -> Integer.compare(a.getId(), b.getId()));
|
||||
|
||||
for (Zombie z : sortedZombies) {
|
||||
if (!z.isAlive()) {
|
||||
onZombieKilled(z);
|
||||
zombies.remove(z.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (z.isElite()) {
|
||||
Player nearest = findNearestPlayer(z.getX(), z.getY());
|
||||
if (nearest != null) {
|
||||
float dist = z.distanceTo(nearest.getX(), nearest.getY());
|
||||
if (dist <= z.getTemplate().getRangedRange() && z.canRangedAttack(now)) {
|
||||
fireZombieBullet(z, nearest);
|
||||
z.rangedAttack(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Wall attackedWall = z.move(map, dt, zombies.values(), now);
|
||||
if (attackedWall != null && z.canAttack(now)) {
|
||||
attackedWall.takeDamage(1.0f);
|
||||
z.attack(now);
|
||||
if (attackedWall.isDestroyed()) {
|
||||
map.removeWall(attackedWall.getGridX(), attackedWall.getGridY());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 精英僵尸发射子弹
|
||||
*
|
||||
* @param zombie 发射子弹的僵尸
|
||||
* @param target 目标玩家
|
||||
*/
|
||||
private void fireZombieBullet(Zombie zombie, Player target) {
|
||||
float dx = target.getX() - zombie.getX();
|
||||
float dy = target.getY() - zombie.getY();
|
||||
float angle = (float) Math.atan2(dx, dy);
|
||||
|
||||
float startX = zombie.getX() + (float) Math.sin(angle) * 0.5f;
|
||||
float startY = zombie.getY() + (float) Math.cos(angle) * 0.5f;
|
||||
|
||||
Bullet bullet = new Bullet(nextZombieBulletId++, startX, startY, angle,
|
||||
zombie.getTemplate().getRangedBulletSpeed(), zombie.getTemplate().getRangedDamage(),
|
||||
"zombie_" + zombie.getId(), -1, 15);
|
||||
zombieBullets.put(bullet.getId(), bullet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有僵尸子弹
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateZombieBullets(float dt) {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Bullet b : zombieBullets.values()) {
|
||||
if (!b.update(dt, map)) {
|
||||
toRemove.add(b.getId());
|
||||
}
|
||||
}
|
||||
for (int id : toRemove) {
|
||||
zombieBullets.remove(id);
|
||||
removedZombieBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测僵尸子弹与玩家/墙体的碰撞
|
||||
*/
|
||||
private void checkZombieBulletCollisions() {
|
||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||
for (Bullet b : new ArrayList<>(zombieBullets.values())) {
|
||||
boolean hit = false;
|
||||
|
||||
// 检测是否命中玩家
|
||||
for (Player p : new ArrayList<>(players.values())) {
|
||||
if (!p.isAlive()) continue;
|
||||
if (b.hitsEntity(p.getX(), p.getY(), PLAYER_SIZE)) {
|
||||
p.takeDamage(b.getDamage());
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否命中坚果墙体
|
||||
if (!hit) {
|
||||
int gx = (int) Math.floor(b.getX());
|
||||
int gy = (int) Math.floor(b.getY());
|
||||
Wall wall = map.getWall(gx, gy);
|
||||
if (wall instanceof NutWall && !wall.isDestroyed()) {
|
||||
wall.takeDamage(b.getDamage());
|
||||
hit = true;
|
||||
if (wall.isDestroyed()) {
|
||||
map.removeWall(gx, gy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
bulletsToRemove.add(b.getId());
|
||||
}
|
||||
}
|
||||
for (int id : bulletsToRemove) {
|
||||
zombieBullets.remove(id);
|
||||
removedZombieBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理僵尸死亡
|
||||
*
|
||||
* - 分裂僵尸分裂成多个小僵尸
|
||||
* - 增加分数
|
||||
* - 可能掉落物品
|
||||
*
|
||||
* @param z 被击杀的僵尸
|
||||
*/
|
||||
private void onZombieKilled(Zombie z) {
|
||||
if (z.isSplitter()) {
|
||||
int splitCount = z.getTemplate().getMinSplit() +
|
||||
random.nextInt(z.getTemplate().getMaxSplit() - z.getTemplate().getMinSplit() + 1);
|
||||
for (int i = 0; i < splitCount; i++) {
|
||||
float offsetX = (random.nextFloat() - 0.5f) * 1.0f;
|
||||
float offsetY = (random.nextFloat() - 0.5f) * 1.0f;
|
||||
Zombie splitZombie = new Zombie(nextZombieId++,
|
||||
z.getX() + offsetX, z.getY() + offsetY, "normal");
|
||||
zombies.put(splitZombie.getId(), splitZombie);
|
||||
}
|
||||
score += 20;
|
||||
} else {
|
||||
score += z.isElite() ? 50 : 10;
|
||||
}
|
||||
if (random.nextFloat() < ZOMBIE_LOOT_DROP_CHANCE) {
|
||||
String lootType = random.nextFloat() < 0.5f ? LOOT_TYPE_AMMO : LOOT_TYPE_HEALTH;
|
||||
Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType);
|
||||
loots.put(loot.getId(), loot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有玩家子弹
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateBullets(float dt) {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Bullet b : bullets.values()) {
|
||||
if (!b.update(dt, map)) {
|
||||
if (b.isExplosive()) {
|
||||
createExplosion(b.getX(), b.getY(), b.getExplosionRadius(), b.getOwnerId());
|
||||
}
|
||||
toRemove.add(b.getId());
|
||||
}
|
||||
}
|
||||
for (int id : toRemove) {
|
||||
bullets.remove(id);
|
||||
removedBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测玩家子弹与僵尸/墙体的碰撞
|
||||
*/
|
||||
private void checkBulletCollisions() {
|
||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||
for (Bullet b : new ArrayList<>(bullets.values())) {
|
||||
if (b.isGrenade()) continue;
|
||||
|
||||
// 检测是否命中僵尸
|
||||
boolean hit = false;
|
||||
for (Zombie z : new ArrayList<>(zombies.values())) {
|
||||
if (!z.isAlive()) continue;
|
||||
if (b.hitsEntity(z.getX(), z.getY(), ZOMBIE_SIZE)) {
|
||||
z.takeDamage(b.getDamage());
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否命中坚果墙体
|
||||
if (!hit) {
|
||||
int gx = (int) Math.floor(b.getX());
|
||||
int gy = (int) Math.floor(b.getY());
|
||||
Wall wall = map.getWall(gx, gy);
|
||||
if (wall instanceof NutWall && !wall.isDestroyed()) {
|
||||
wall.takeDamage(b.getDamage());
|
||||
hit = true;
|
||||
if (wall.isDestroyed()) {
|
||||
map.removeWall(gx, gy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
bulletsToRemove.add(b.getId());
|
||||
}
|
||||
}
|
||||
for (int id : bulletsToRemove) {
|
||||
bullets.remove(id);
|
||||
removedBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建爆炸效果
|
||||
*
|
||||
* 对范围内的僵尸造成伤害
|
||||
*
|
||||
* @param x 爆炸中心X坐标
|
||||
* @param y 爆炸中心Y坐标
|
||||
* @param radius 爆炸半径
|
||||
* @param ownerId 爆炸发起者ID
|
||||
*/
|
||||
private void createExplosion(float x, float y, float radius, String ownerId) {
|
||||
Map<String, Object> exp = new LinkedHashMap<>();
|
||||
exp.put("x", x);
|
||||
exp.put("y", y);
|
||||
exp.put("radius", radius);
|
||||
explosions.add(exp);
|
||||
|
||||
for (Zombie z : new ArrayList<>(zombies.values())) {
|
||||
float dist = z.distanceTo(x, y);
|
||||
if (dist < radius) {
|
||||
z.takeDamage(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测僵尸近战攻击
|
||||
*/
|
||||
private void checkZombieAttacks() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (Zombie z : zombies.values()) {
|
||||
if (!z.isAlive()) continue;
|
||||
for (Player p : players.values()) {
|
||||
if (!p.isAlive()) continue;
|
||||
float dist = z.distanceTo(p.getX(), p.getY());
|
||||
if (dist < 1.0f && z.canAttack(now)) {
|
||||
p.takeDamage(z.getTemplate().getDamage());
|
||||
z.attack(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测掉落物收集
|
||||
*/
|
||||
private void checkLootCollection() {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Loot loot : loots.values()) {
|
||||
for (Player p : players.values()) {
|
||||
if (!p.isAlive()) continue;
|
||||
if (loot.isCollectedBy(p.getX(), p.getY())) {
|
||||
if (loot.getType().equals(LOOT_TYPE_HEALTH)) {
|
||||
p.heal(LOOT_HEALTH_AMOUNT);
|
||||
} else {
|
||||
p.refillRandomWeapon();
|
||||
}
|
||||
toRemove.add(loot.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int id : toRemove) {
|
||||
loots.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测玩家重生
|
||||
*/
|
||||
private void checkPlayerRespawn() {
|
||||
boolean hasAlivePlayer = false;
|
||||
for (Player p : players.values()) {
|
||||
if (p.isAlive()) {
|
||||
hasAlivePlayer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (Player p : players.values()) {
|
||||
if (p.isWaitingForRespawn()) {
|
||||
p.updateRespawnTimer(TICK_INTERVAL);
|
||||
if (hasAlivePlayer && p.canRespawn()) {
|
||||
List<int[]> spawnPoints = map.getSpawnPoints();
|
||||
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
p.respawn(wx, wy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家开火(无蓄力)
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
* @return 新创建的子弹ID列表
|
||||
*/
|
||||
public List<Integer> fireWeapon(Player player, float aimX, float aimY) {
|
||||
return fireWeapon(player, aimX, aimY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家开火(支持蓄力)
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
* @param chargePercent 蓄力百分比(0-1),影响手榴弹投掷距离
|
||||
* @return 新创建的子弹ID列表
|
||||
*/
|
||||
public List<Integer> fireWeapon(Player player, float aimX, float aimY, float chargePercent) {
|
||||
List<Integer> newBulletIds = new ArrayList<>();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (!player.canFire(now) || !player.hasAmmo()) return newBulletIds;
|
||||
|
||||
player.setAngle(aimX, aimY);
|
||||
player.fire(now);
|
||||
|
||||
if (player.isChargeable()) {
|
||||
float startX = player.getX();
|
||||
float startY = player.getY();
|
||||
|
||||
float minDist = 3.0f;
|
||||
float maxDist = 15.0f;
|
||||
float dist = minDist + (maxDist - minDist) * chargePercent;
|
||||
|
||||
float dx = aimX - startX;
|
||||
float dy = aimY - startY;
|
||||
float targetDist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
float targetX, targetY;
|
||||
if (targetDist < 0.1f) {
|
||||
targetX = startX + minDist;
|
||||
targetY = startY;
|
||||
} else {
|
||||
float scale = Math.min(dist, targetDist) / targetDist;
|
||||
targetX = startX + dx * scale;
|
||||
targetY = startY + dy * scale;
|
||||
}
|
||||
|
||||
targetX = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetX));
|
||||
targetY = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetY));
|
||||
|
||||
float flightDuration = 0.8f + chargePercent * 0.7f;
|
||||
|
||||
Bullet bullet = new Bullet(nextBulletId++, startX, startY, targetX, targetY,
|
||||
flightDuration, player.getDamage(), player.getId(), 3.0f);
|
||||
bullets.put(bullet.getId(), bullet);
|
||||
newBulletIds.add(bullet.getId());
|
||||
} else {
|
||||
int pellets = player.getPelletCount();
|
||||
float spread = player.getSpread();
|
||||
float range = player.getWeaponIndex() == 1 ? 25 : (player.getWeaponIndex() == 2 ? 12 : 30);
|
||||
|
||||
for (int i = 0; i < pellets; i++) {
|
||||
float angle = player.getAngle();
|
||||
if (spread > 0) {
|
||||
angle += (random.nextFloat() - 0.5f) * spread * 2;
|
||||
}
|
||||
|
||||
float startX = player.getX() + (float) Math.sin(angle) * 0.5f;
|
||||
float startY = player.getY() + (float) Math.cos(angle) * 0.5f;
|
||||
|
||||
float speed = player.getBulletSpeed();
|
||||
int damage = player.getDamage();
|
||||
|
||||
Bullet bullet = new Bullet(nextBulletId++, startX, startY, angle,
|
||||
speed, damage, player.getId(), player.getWeaponIndex(), range);
|
||||
bullets.put(bullet.getId(), bullet);
|
||||
newBulletIds.add(bullet.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return newBulletIds;
|
||||
}
|
||||
|
||||
/** 获取地图数据 */
|
||||
public int[][] getMapData() {
|
||||
return map.getCells();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建游戏状态数据
|
||||
*
|
||||
* 将当前游戏世界的所有状态打包成Map格式,用于网络传输
|
||||
*
|
||||
* @param forPlayerId 目标玩家ID(用于发送该玩家的弹药信息)
|
||||
* @return 游戏状态Map
|
||||
*/
|
||||
public Map<String, Object> buildGameState(String forPlayerId) {
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
|
||||
List<Map<String, Object>> playerStates = new ArrayList<>();
|
||||
for (Player p : players.values()) {
|
||||
playerStates.add(p.toStateMap());
|
||||
}
|
||||
state.put("players", playerStates);
|
||||
|
||||
List<Map<String, Object>> zombieStates = new ArrayList<>();
|
||||
for (Zombie z : zombies.values()) {
|
||||
zombieStates.add(z.toStateMap());
|
||||
}
|
||||
state.put("zombies", zombieStates);
|
||||
|
||||
List<Map<String, Object>> bulletStates = new ArrayList<>();
|
||||
for (Bullet b : bullets.values()) {
|
||||
bulletStates.add(b.toStateMap());
|
||||
}
|
||||
state.put("bullets", bulletStates);
|
||||
|
||||
List<Map<String, Object>> zombieBulletStates = new ArrayList<>();
|
||||
for (Bullet b : zombieBullets.values()) {
|
||||
zombieBulletStates.add(b.toStateMap());
|
||||
}
|
||||
state.put("zombieBullets", zombieBulletStates);
|
||||
|
||||
List<Map<String, Object>> lootStates = new ArrayList<>();
|
||||
for (Loot l : loots.values()) {
|
||||
lootStates.add(l.toStateMap());
|
||||
}
|
||||
state.put("loots", lootStates);
|
||||
|
||||
state.put("explosions", new ArrayList<>(explosions));
|
||||
state.put("removedBullets", new ArrayList<>(removedBullets));
|
||||
state.put("nutWalls", map.getNutWallStates());
|
||||
state.put("gameTime", gameTime);
|
||||
state.put("waveNumber", waveNumber);
|
||||
state.put("score", score);
|
||||
|
||||
Player forPlayer = players.get(forPlayerId);
|
||||
if (forPlayer != null) {
|
||||
Map<String, Object> ammoMap = new LinkedHashMap<>();
|
||||
int weaponCount = TemplateManager.getInstance().getAllWeaponTemplates().size();
|
||||
for (int i = 0; i < weaponCount; i++) {
|
||||
String weaponId = TemplateManager.getInstance().getWeaponId(i);
|
||||
float ammo = forPlayer.getAmmo()[i];
|
||||
ammoMap.put(weaponId, ammo == Integer.MAX_VALUE ? -1 : (int) ammo);
|
||||
}
|
||||
state.put("ammo", ammoMap);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/**
|
||||
* 掉落物类
|
||||
*
|
||||
* 管理僵尸死亡后掉落的物品,包括弹药和生命值补给。
|
||||
*/
|
||||
@Getter
|
||||
public class Loot {
|
||||
private final int id;
|
||||
private final float x, y;
|
||||
private final String type;
|
||||
private final long spawnTime;
|
||||
|
||||
public Loot(int id, float x, float y) {
|
||||
this(id, x, y, Constants.LOOT_TYPE_AMMO);
|
||||
}
|
||||
|
||||
public Loot(int id, float x, float y, String type) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.type = type;
|
||||
this.spawnTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isCollectedBy(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return Math.sqrt(dx * dx + dy * dy) < 0.8f;
|
||||
}
|
||||
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("x", x);
|
||||
map.put("y", y);
|
||||
map.put("type", type);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import com.zombie.game.template.TemplateManager;
|
||||
import com.zombie.game.template.WeaponTemplate;
|
||||
import com.zombie.game.template.PlayerTemplate;
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/**
|
||||
* 玩家类
|
||||
*
|
||||
* 管理玩家状态,包括:
|
||||
* - 位置、朝向、生命值
|
||||
* - 武器和弹药
|
||||
* - 移动和射击
|
||||
* - 重生机制
|
||||
*/
|
||||
@Getter
|
||||
public class Player {
|
||||
private String id;
|
||||
private String name;
|
||||
private float x, y;
|
||||
private float angle;
|
||||
private float health;
|
||||
private int weaponIndex;
|
||||
private boolean ready;
|
||||
private long lastAttackTime;
|
||||
private long lastDamageTime;
|
||||
private float[] ammo;
|
||||
private boolean firing;
|
||||
private float grenadeChargeStart;
|
||||
private boolean chargingGrenade;
|
||||
private int lastProcessedSeq;
|
||||
private float respawnTimer;
|
||||
private boolean waitingForRespawn;
|
||||
|
||||
private static final List<WeaponTemplate> WEAPON_TEMPLATES;
|
||||
static {
|
||||
List<WeaponTemplate> list = new ArrayList<>(TemplateManager.getInstance().getAllWeaponTemplates());
|
||||
WEAPON_TEMPLATES = Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
private final PlayerTemplate playerTemplate;
|
||||
|
||||
public Player(String id, String name, float x, float y) {
|
||||
this.playerTemplate = TemplateManager.getInstance().getPlayerTemplate();
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.angle = 0;
|
||||
this.health = playerTemplate.getMaxHealth();
|
||||
this.weaponIndex = 0;
|
||||
this.ready = false;
|
||||
this.lastAttackTime = 0;
|
||||
this.lastDamageTime = 0;
|
||||
this.ammo = new float[WEAPON_TEMPLATES.size()];
|
||||
for (int i = 0; i < WEAPON_TEMPLATES.size(); i++) {
|
||||
this.ammo[i] = WEAPON_TEMPLATES.get(i).getMaxAmmo();
|
||||
}
|
||||
this.firing = false;
|
||||
this.grenadeChargeStart = 0;
|
||||
this.chargingGrenade = false;
|
||||
this.lastProcessedSeq = 0;
|
||||
this.respawnTimer = 0;
|
||||
this.waitingForRespawn = false;
|
||||
}
|
||||
|
||||
public void setReady(boolean ready) { this.ready = ready; }
|
||||
public void setWeaponIndex(int idx) {
|
||||
this.weaponIndex = Math.max(0, Math.min(WEAPON_TEMPLATES.size() - 1, idx));
|
||||
}
|
||||
|
||||
public void setPosition(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public void applyMovement(float dx, float dy, GameMap map) {
|
||||
float speed = playerTemplate.getSpeed() * TICK_INTERVAL;
|
||||
float newX = x + dx * speed;
|
||||
float newY = y + dy * speed;
|
||||
|
||||
if (map.isWalkable(newX, y, playerTemplate.getSize())) {
|
||||
x = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newX));
|
||||
}
|
||||
if (map.isWalkable(x, newY, playerTemplate.getSize())) {
|
||||
y = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newY));
|
||||
}
|
||||
}
|
||||
|
||||
public void setAngle(float aimX, float aimY) {
|
||||
this.angle = (float) Math.atan2(aimX - x, aimY - y);
|
||||
}
|
||||
|
||||
public void takeDamage(float damage) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastDamageTime < playerTemplate.getInvulnerableTime() * 1000) return;
|
||||
this.health -= damage;
|
||||
if (this.health < 0) this.health = 0;
|
||||
if (this.health <= 0) {
|
||||
startRespawnTimer();
|
||||
}
|
||||
}
|
||||
|
||||
public void startRespawnTimer() {
|
||||
this.waitingForRespawn = true;
|
||||
this.respawnTimer = playerTemplate.getRespawnTime();
|
||||
}
|
||||
|
||||
public void updateRespawnTimer(float dt) {
|
||||
if (waitingForRespawn && respawnTimer > 0) {
|
||||
respawnTimer -= dt;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canRespawn() {
|
||||
return waitingForRespawn && respawnTimer <= 0;
|
||||
}
|
||||
|
||||
public void respawn(float newX, float newY) {
|
||||
this.health = playerTemplate.getMaxHealth();
|
||||
this.x = newX;
|
||||
this.y = newY;
|
||||
this.waitingForRespawn = false;
|
||||
this.respawnTimer = 0;
|
||||
this.lastDamageTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isWaitingForRespawn() {
|
||||
return waitingForRespawn;
|
||||
}
|
||||
|
||||
public float getRespawnTimer() {
|
||||
return respawnTimer;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
public float distanceTo(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
public boolean canFire(long now) {
|
||||
WeaponTemplate weapon = WEAPON_TEMPLATES.get(weaponIndex);
|
||||
return now - lastAttackTime >= weapon.getFireRate();
|
||||
}
|
||||
|
||||
public void fire(long now) {
|
||||
lastAttackTime = now;
|
||||
if (weaponIndex != 0 && ammo[weaponIndex] > 0) {
|
||||
ammo[weaponIndex]--;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAmmo() {
|
||||
if (weaponIndex == 0) return true;
|
||||
return ammo[weaponIndex] > 0;
|
||||
}
|
||||
|
||||
public void refillRandomWeapon() {
|
||||
Random rand = new Random();
|
||||
int idx = rand.nextInt(WEAPON_TEMPLATES.size() - 1) + 1;
|
||||
ammo[idx] = WEAPON_TEMPLATES.get(idx).getMaxAmmo();
|
||||
}
|
||||
|
||||
public void heal(float amount) {
|
||||
this.health = Math.min(playerTemplate.getMaxHealth(), this.health + amount);
|
||||
}
|
||||
|
||||
public void setFiring(boolean firing) { this.firing = firing; }
|
||||
public void setLastProcessedSeq(int seq) { this.lastProcessedSeq = seq; }
|
||||
|
||||
public void startGrenadeCharge() {
|
||||
if (!chargingGrenade) {
|
||||
chargingGrenade = true;
|
||||
grenadeChargeStart = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
public float getGrenadeChargePercent() {
|
||||
if (!chargingGrenade) return 0;
|
||||
float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f;
|
||||
return Math.min(1.0f, elapsed);
|
||||
}
|
||||
|
||||
public void stopGrenadeCharge() {
|
||||
chargingGrenade = false;
|
||||
}
|
||||
|
||||
public long getFireRate() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).getFireRate();
|
||||
}
|
||||
|
||||
public int getDamage() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).getDamage();
|
||||
}
|
||||
|
||||
public float getBulletSpeed() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).getBulletSpeed();
|
||||
}
|
||||
|
||||
public int getPelletCount() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).getPelletCount();
|
||||
}
|
||||
|
||||
public float getSpread() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).getSpread();
|
||||
}
|
||||
|
||||
public boolean isChargeable() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).isChargeable();
|
||||
}
|
||||
|
||||
public boolean isExplosive() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).isExplosive();
|
||||
}
|
||||
|
||||
public float getExplosionRadius() {
|
||||
return WEAPON_TEMPLATES.get(weaponIndex).getExplosionRadius();
|
||||
}
|
||||
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("x", x);
|
||||
map.put("y", y);
|
||||
map.put("angle", angle);
|
||||
map.put("health", health);
|
||||
map.put("weaponIndex", weaponIndex);
|
||||
map.put("lastProcessedSeq", lastProcessedSeq);
|
||||
map.put("waitingForRespawn", waitingForRespawn);
|
||||
map.put("respawnTimer", respawnTimer);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
25
backend/src/main/java/com/zombie/game/model/PlayerInfo.java
Normal file
25
backend/src/main/java/com/zombie/game/model/PlayerInfo.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 玩家大厅信息
|
||||
*
|
||||
* 仅用于房间大厅阶段的玩家信息,不包含游戏内状态。
|
||||
*/
|
||||
@Getter
|
||||
public class PlayerInfo {
|
||||
private final String id;
|
||||
private final String name;
|
||||
private boolean ready;
|
||||
|
||||
public PlayerInfo(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
public void setReady(boolean ready) {
|
||||
this.ready = ready;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import static com.zombie.game.model.Constants.*;
|
||||
public class Room {
|
||||
private String id;
|
||||
private String hostId;
|
||||
private Map<String, Player> players;
|
||||
private Map<String, PlayerInfo> players;
|
||||
private boolean gameStarted;
|
||||
private final int maxPlayers = 4;
|
||||
|
||||
@@ -27,15 +27,13 @@ public class Room {
|
||||
this.id = id;
|
||||
this.hostId = hostId;
|
||||
this.players = new LinkedHashMap<>();
|
||||
Player host = new Player(hostId, hostName, 0, 0);
|
||||
players.put(hostId, host);
|
||||
players.put(hostId, new PlayerInfo(hostId, hostName));
|
||||
}
|
||||
|
||||
public boolean addPlayer(String playerId, String playerName) {
|
||||
if (players.size() >= maxPlayers) return false;
|
||||
if (players.containsKey(playerId)) return false;
|
||||
Player player = new Player(playerId, playerName, 0, 0);
|
||||
players.put(playerId, player);
|
||||
players.put(playerId, new PlayerInfo(playerId, playerName));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -43,11 +41,11 @@ public class Room {
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
public Player getPlayer(String playerId) {
|
||||
public PlayerInfo getPlayer(String playerId) {
|
||||
return players.get(playerId);
|
||||
}
|
||||
|
||||
public Collection<Player> getPlayers() {
|
||||
public Collection<PlayerInfo> getPlayers() {
|
||||
return players.values();
|
||||
}
|
||||
|
||||
@@ -60,18 +58,12 @@ public class Room {
|
||||
}
|
||||
|
||||
public boolean allReady() {
|
||||
for (Player p : players.values()) {
|
||||
for (PlayerInfo p : players.values()) {
|
||||
if (!p.getId().equals(hostId) && !p.isReady()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将房间状态转换为Map格式,用于网络传输
|
||||
*
|
||||
* @param playerId 目标玩家ID
|
||||
* @return 包含房间状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap(String playerId) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("roomId", id);
|
||||
@@ -81,7 +73,7 @@ public class Room {
|
||||
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (Player p : players.values()) {
|
||||
for (PlayerInfo p : players.values()) {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
@@ -93,15 +85,11 @@ public class Room {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将房间信息转换为房间列表格式,用于大厅显示
|
||||
*
|
||||
* @return 包含房间信息的Map
|
||||
*/
|
||||
public Map<String, Object> toRoomListMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("hostName", players.get(hostId) != null ? players.get(hostId).getName() : "Unknown");
|
||||
PlayerInfo host = players.get(hostId);
|
||||
map.put("hostName", host != null ? host.getName() : "Unknown");
|
||||
map.put("playerCount", players.size());
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import com.zombie.game.template.TemplateManager;
|
||||
import com.zombie.game.template.ZombieTemplate;
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/**
|
||||
* 僵尸类
|
||||
*
|
||||
* 管理僵尸状态和行为,包括:
|
||||
* - 位置、朝向、生命值
|
||||
* - 移动和寻路(基于流场导航)
|
||||
* - 近战和远程攻击
|
||||
* - 基于模板配置的属性
|
||||
*/
|
||||
@Getter
|
||||
public class Zombie {
|
||||
private int id;
|
||||
private float x, y;
|
||||
private float angle;
|
||||
private float health;
|
||||
private float maxHealth;
|
||||
private float speed;
|
||||
private long lastAttackTime;
|
||||
private long lastRangedAttackTime;
|
||||
private float targetX, targetY;
|
||||
private boolean hasTarget;
|
||||
private int reservedGridX, reservedGridY;
|
||||
private boolean reservation;
|
||||
private int attackingWallGridX = -1;
|
||||
private int attackingWallGridY = -1;
|
||||
private boolean attackingWall;
|
||||
|
||||
private final ZombieTemplate template;
|
||||
|
||||
public Zombie(int id, float x, float y, String templateId) {
|
||||
this.template = TemplateManager.getInstance().getZombieTemplate(templateId);
|
||||
if (this.template == null) {
|
||||
throw new IllegalArgumentException("Unknown zombie template: " + templateId);
|
||||
}
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.angle = 0;
|
||||
this.health = template.getBaseHealth();
|
||||
this.maxHealth = template.getBaseHealth();
|
||||
this.speed = template.getBaseSpeed();
|
||||
this.lastAttackTime = 0;
|
||||
this.lastRangedAttackTime = 0;
|
||||
this.targetX = 0;
|
||||
this.targetY = 0;
|
||||
this.hasTarget = false;
|
||||
this.reservedGridX = -1;
|
||||
this.reservedGridY = -1;
|
||||
this.reservation = false;
|
||||
this.attackingWallGridX = -1;
|
||||
this.attackingWallGridY = -1;
|
||||
this.attackingWall = false;
|
||||
}
|
||||
|
||||
public boolean isElite() { return template.isCanRangedAttack(); }
|
||||
public boolean isSplitter() { return template.isCanSplit(); }
|
||||
|
||||
public void takeDamage(float damage) {
|
||||
this.health -= damage;
|
||||
if (this.health < 0) this.health = 0;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
public Wall move(GameMap map, float dt, Collection<Zombie> otherZombies, long now) {
|
||||
if (!map.isFlowFieldValid()) return null;
|
||||
|
||||
int currentGridX = (int) Math.floor(x);
|
||||
int currentGridY = (int) Math.floor(y);
|
||||
|
||||
if (attackingWall && attackingWallGridX >= 0) {
|
||||
float wallCenterX = attackingWallGridX + 0.5f;
|
||||
float wallCenterY = attackingWallGridY + 0.5f;
|
||||
float distToWall = distanceTo(wallCenterX, wallCenterY);
|
||||
|
||||
if (distToWall < 0.8f) {
|
||||
Wall wall = map.getWall(attackingWallGridX, attackingWallGridY);
|
||||
if (wall != null && !wall.isDestroyed()) {
|
||||
return wall;
|
||||
}
|
||||
attackingWall = false;
|
||||
attackingWallGridX = -1;
|
||||
attackingWallGridY = -1;
|
||||
}
|
||||
}
|
||||
|
||||
float centerDist = Float.MAX_VALUE;
|
||||
if (hasTarget) {
|
||||
float dx = targetX - x;
|
||||
float dy = targetY - y;
|
||||
centerDist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
if (!hasTarget || centerDist < 0.15f) {
|
||||
float[] flowDir = map.getFlowDirection(x, y);
|
||||
float dirX = flowDir[0];
|
||||
float dirY = flowDir[1];
|
||||
|
||||
if (dirX == 0 && dirY == 0) return null;
|
||||
|
||||
float len = (float) Math.sqrt(dirX * dirX + dirY * dirY);
|
||||
if (len > 0) {
|
||||
dirX /= len;
|
||||
dirY /= len;
|
||||
}
|
||||
|
||||
int nextGridX = currentGridX + (int) Math.round(dirX);
|
||||
int nextGridY = currentGridY + (int) Math.round(dirY);
|
||||
|
||||
if (map.isNutWall(nextGridX, nextGridY)) {
|
||||
attackingWall = true;
|
||||
attackingWallGridX = nextGridX;
|
||||
attackingWallGridY = nextGridY;
|
||||
reservedGridX = nextGridX;
|
||||
reservedGridY = nextGridY;
|
||||
reservation = true;
|
||||
targetX = nextGridX + 0.5f;
|
||||
targetY = nextGridY + 0.5f;
|
||||
hasTarget = true;
|
||||
} else if (map.isWall(nextGridX, nextGridY)) {
|
||||
nextGridX = currentGridX + (int) Math.signum(dirX);
|
||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||
|
||||
if (map.isWall(nextGridX, nextGridY)) {
|
||||
if (!map.isWall(currentGridX + (int) Math.signum(dirX), currentGridY)) {
|
||||
nextGridX = currentGridX + (int) Math.signum(dirX);
|
||||
nextGridY = currentGridY;
|
||||
} else if (!map.isWall(currentGridX, currentGridY + (int) Math.signum(dirY))) {
|
||||
nextGridX = currentGridX;
|
||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||
} else {
|
||||
reservation = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) {
|
||||
int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies);
|
||||
if (altDirs != null) {
|
||||
nextGridX = altDirs[0];
|
||||
nextGridY = altDirs[1];
|
||||
} else {
|
||||
reservation = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
reservedGridX = nextGridX;
|
||||
reservedGridY = nextGridY;
|
||||
reservation = true;
|
||||
|
||||
targetX = nextGridX + 0.5f;
|
||||
targetY = nextGridY + 0.5f;
|
||||
hasTarget = true;
|
||||
} else {
|
||||
if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) {
|
||||
int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies);
|
||||
if (altDirs != null) {
|
||||
nextGridX = altDirs[0];
|
||||
nextGridY = altDirs[1];
|
||||
} else {
|
||||
reservation = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
reservedGridX = nextGridX;
|
||||
reservedGridY = nextGridY;
|
||||
reservation = true;
|
||||
|
||||
targetX = nextGridX + 0.5f;
|
||||
targetY = nextGridY + 0.5f;
|
||||
hasTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
float dx = targetX - x;
|
||||
float dy = targetY - y;
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 0.01f) {
|
||||
hasTarget = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
float dirX = dx / dist;
|
||||
float dirY = dy / dist;
|
||||
|
||||
float moveX = dirX * speed * dt;
|
||||
float moveY = dirY * speed * dt;
|
||||
|
||||
float newX = x + moveX;
|
||||
float newY = y + moveY;
|
||||
|
||||
boolean canMoveX = map.isWalkable(newX, y, ZOMBIE_SIZE);
|
||||
boolean canMoveY = map.isWalkable(x, newY, ZOMBIE_SIZE);
|
||||
boolean canMoveDiagonal = map.isWalkable(newX, newY, ZOMBIE_SIZE);
|
||||
|
||||
if (moveX != 0 && moveY != 0) {
|
||||
int checkX = (int) Math.floor(newX);
|
||||
int checkY = (int) Math.floor(newY);
|
||||
int checkCurrentX = (int) Math.floor(x);
|
||||
int checkCurrentY = (int) Math.floor(y);
|
||||
|
||||
boolean blockedByCorner = false;
|
||||
if (checkX != checkCurrentX && checkY != checkCurrentY) {
|
||||
boolean wallInX = map.isWall(checkX, checkCurrentY);
|
||||
boolean wallInY = map.isWall(checkCurrentX, checkY);
|
||||
|
||||
if (wallInX || wallInY) {
|
||||
blockedByCorner = true;
|
||||
|
||||
if (!wallInX && canMoveX) {
|
||||
x = newX;
|
||||
canMoveY = map.isWalkable(x, newY, ZOMBIE_SIZE);
|
||||
} else if (!wallInY && canMoveY) {
|
||||
y = newY;
|
||||
canMoveX = map.isWalkable(newX, y, ZOMBIE_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockedByCorner && canMoveDiagonal) {
|
||||
x = newX;
|
||||
y = newY;
|
||||
} else if (!blockedByCorner) {
|
||||
if (canMoveX) x = newX;
|
||||
if (canMoveY) y = newY;
|
||||
}
|
||||
} else {
|
||||
if (canMoveX) x = newX;
|
||||
if (canMoveY) y = newY;
|
||||
}
|
||||
|
||||
float minSeparationDist = ZOMBIE_SIZE;
|
||||
for (Zombie other : otherZombies) {
|
||||
if (other.getId() == this.id) continue;
|
||||
if (!other.isAlive()) continue;
|
||||
|
||||
float ox = other.getX();
|
||||
float oy = other.getY();
|
||||
float sepDx = x - ox;
|
||||
float sepDy = y - oy;
|
||||
float sepDist = (float) Math.sqrt(sepDx * sepDx + sepDy * sepDy);
|
||||
|
||||
if (sepDist < minSeparationDist && sepDist > 0.01f) {
|
||||
float overlap = minSeparationDist - sepDist;
|
||||
float pushX = (sepDx / sepDist) * overlap * 0.5f;
|
||||
float pushY = (sepDy / sepDist) * overlap * 0.5f;
|
||||
|
||||
float pushedX = x + pushX;
|
||||
float pushedY = y + pushY;
|
||||
|
||||
if (map.isWalkable(pushedX, pushedY, ZOMBIE_SIZE)) {
|
||||
x = pushedX;
|
||||
y = pushedY;
|
||||
} else {
|
||||
if (map.isWalkable(x + pushX, y, ZOMBIE_SIZE)) {
|
||||
x = x + pushX;
|
||||
}
|
||||
if (map.isWalkable(x, y + pushY, ZOMBIE_SIZE)) {
|
||||
y = y + pushY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dirX != 0 || dirY != 0) {
|
||||
angle = (float) Math.atan2(dirX, dirY);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> otherZombies) {
|
||||
for (Zombie other : otherZombies) {
|
||||
if (other.getId() == this.id) continue;
|
||||
if (!other.isAlive()) continue;
|
||||
|
||||
int otherGridX = (int) Math.floor(other.getX());
|
||||
int otherGridY = (int) Math.floor(other.getY());
|
||||
|
||||
if (otherGridX == gridX && otherGridY == gridY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other.isReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
|
||||
GameMap map, Collection<Zombie> otherZombies) {
|
||||
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
|
||||
java.util.List<int[]> candidates = new java.util.ArrayList<>();
|
||||
|
||||
for (int[] dir : allDirs) {
|
||||
int nx = currentGridX + dir[0];
|
||||
int ny = currentGridY + dir[1];
|
||||
|
||||
if (map.isWall(nx, ny)) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (map.isWall(currentGridX + dir[0], currentGridY) ||
|
||||
map.isWall(currentGridX, currentGridY + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue;
|
||||
|
||||
float dotProduct = dir[0] * dirX + dir[1] * dirY;
|
||||
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) return null;
|
||||
|
||||
candidates.sort((a, b) -> b[2] - a[2]);
|
||||
|
||||
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
|
||||
}
|
||||
|
||||
public boolean canAttack(long now) {
|
||||
return now - lastAttackTime >= template.getAttackRate() * 1000;
|
||||
}
|
||||
|
||||
public void attack(long now) {
|
||||
lastAttackTime = now;
|
||||
}
|
||||
|
||||
public boolean canRangedAttack(long now) {
|
||||
if (!template.isCanRangedAttack()) return false;
|
||||
return now - lastRangedAttackTime >= template.getAttackRate() * 1000;
|
||||
}
|
||||
|
||||
public void rangedAttack(long now) {
|
||||
lastRangedAttackTime = now;
|
||||
}
|
||||
|
||||
public float distanceTo(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("x", x);
|
||||
map.put("y", y);
|
||||
map.put("angle", angle);
|
||||
map.put("health", health);
|
||||
map.put("isElite", isElite());
|
||||
map.put("isSplitter", isSplitter());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user