迁移证esc

This commit is contained in:
wfz
2026-05-02 18:07:11 +08:00
parent 990b31a12a
commit 9fd572c8c4
51 changed files with 3659 additions and 1820 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}