This commit is contained in:
wfz
2026-04-26 13:35:10 +08:00
parent f1a6f0fd75
commit 990b31a12a
12 changed files with 597 additions and 437 deletions

View File

@@ -20,7 +20,7 @@ public class Bullet {
private float speed; private float speed;
private int damage; private int damage;
private String ownerId; private String ownerId;
private String weapon; private int weaponIndex;
private float range; private float range;
private float distanceTraveled; private float distanceTraveled;
private boolean explosive; private boolean explosive;
@@ -31,7 +31,7 @@ public class Bullet {
private boolean isGrenade; private boolean isGrenade;
public Bullet(int id, float x, float y, float angle, float speed, int damage, public Bullet(int id, float x, float y, float angle, float speed, int damage,
String ownerId, String weapon, float range) { String ownerId, int weaponIndex, float range) {
this.id = id; this.id = id;
this.x = x; this.x = x;
this.y = y; this.y = y;
@@ -42,21 +42,21 @@ public class Bullet {
this.vz = 0; this.vz = 0;
this.damage = damage; this.damage = damage;
this.ownerId = ownerId; this.ownerId = ownerId;
this.weapon = weapon; this.weaponIndex = weaponIndex;
this.range = range; this.range = range;
this.distanceTraveled = 0; this.distanceTraveled = 0;
this.explosive = weapon.equals(Constants.WEAPON_GRENADE); this.explosive = false;
this.explosionRadius = explosive ? 3.0f : 0; this.explosionRadius = 0;
this.flightTime = 0; this.flightTime = 0;
this.maxFlightTime = 0; this.maxFlightTime = 0;
this.isGrenade = weapon.equals(Constants.WEAPON_GRENADE); this.isGrenade = false;
this.targetX = x + (float) Math.sin(angle) * range; this.targetX = x + (float) Math.sin(angle) * range;
this.targetY = y + (float) Math.cos(angle) * range; this.targetY = y + (float) Math.cos(angle) * range;
} }
/** /**
* 构造函数 - 手榴弹 * 构造函数 - 手榴弹
* *
* @param id 子弹ID * @param id 子弹ID
* @param startX 起始X坐标 * @param startX 起始X坐标
* @param startY 起始Y坐标 * @param startY 起始Y坐标
@@ -67,7 +67,7 @@ public class Bullet {
* @param ownerId 发射者ID * @param ownerId 发射者ID
* @param explosionRadius 爆炸半径 * @param explosionRadius 爆炸半径
*/ */
public Bullet(int id, float startX, float startY, float targetX, float targetY, public Bullet(int id, float startX, float startY, float targetX, float targetY,
float flightDuration, int damage, String ownerId, float explosionRadius) { float flightDuration, int damage, String ownerId, float explosionRadius) {
this.id = id; this.id = id;
this.x = startX; this.x = startX;
@@ -77,7 +77,7 @@ public class Bullet {
this.targetY = targetY; this.targetY = targetY;
this.damage = damage; this.damage = damage;
this.ownerId = ownerId; this.ownerId = ownerId;
this.weapon = Constants.WEAPON_GRENADE; this.weaponIndex = -1;
this.range = 0; this.range = 0;
this.distanceTraveled = 0; this.distanceTraveled = 0;
this.explosive = true; this.explosive = true;
@@ -97,21 +97,21 @@ public class Bullet {
public boolean update(float dt, GameMap map) { public boolean update(float dt, GameMap map) {
if (isGrenade) { if (isGrenade) {
flightTime += dt; flightTime += dt;
x += vx * dt; x += vx * dt;
y += vy * dt; y += vy * dt;
float progress = flightTime / maxFlightTime; float progress = flightTime / maxFlightTime;
z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI); z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
if (flightTime >= maxFlightTime || z <= 0.5f && progress > 0.5f) { if (flightTime >= maxFlightTime || z <= 0.5f && progress > 0.5f) {
return false; return false;
} }
if (x < 0 || x >= Constants.GRID_SIZE || y < 0 || y >= Constants.GRID_SIZE) { if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
return false; return false;
} }
return true; return true;
} else { } else {
float moveX = vx * dt; float moveX = vx * dt;
@@ -121,7 +121,7 @@ public class Bullet {
distanceTraveled += (float) Math.sqrt(moveX * moveX + moveY * moveY); distanceTraveled += (float) Math.sqrt(moveX * moveX + moveY * moveY);
if (distanceTraveled >= range) return false; if (distanceTraveled >= range) return false;
if (x < 0 || x >= Constants.GRID_SIZE || y < 0 || y >= Constants.GRID_SIZE) return false; if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return false;
int gx = (int) Math.floor(x); int gx = (int) Math.floor(x);
int gy = (int) Math.floor(y); int gy = (int) Math.floor(y);
@@ -135,7 +135,7 @@ public class Bullet {
/** /**
* 检测子弹是否命中实体 * 检测子弹是否命中实体
* *
* @param ex 实体X坐标 * @param ex 实体X坐标
* @param ey 实体Y坐标 * @param ey 实体Y坐标
* @param size 实体碰撞体大小 * @param size 实体碰撞体大小
@@ -150,7 +150,7 @@ public class Bullet {
/** /**
* 将子弹状态转换为Map格式用于网络传输 * 将子弹状态转换为Map格式用于网络传输
* *
* @return 包含子弹状态的Map * @return 包含子弹状态的Map
*/ */
public Map<String, Object> toStateMap() { public Map<String, Object> toStateMap() {
@@ -166,7 +166,7 @@ public class Bullet {
} else { } else {
map.put("angle", (float) Math.atan2(vx, vy)); map.put("angle", (float) Math.atan2(vx, vy));
} }
map.put("weapon", weapon); map.put("weaponIndex", weaponIndex);
map.put("ownerId", ownerId); map.put("ownerId", ownerId);
return map; return map;
} }

View File

@@ -3,8 +3,8 @@ package com.zombie.game.model;
/** /**
* 游戏常量定义类 * 游戏常量定义类
* *
* 集中管理所有游戏平衡性参数、消息类型常量 * 只保留真正的系统常量(不会随游戏平衡性调整而改变的值)
* 修改此文件中的数值可以调整游戏难度和行为 * 所有可配置的游戏平衡参数已迁移到 JSON 模板文件
*/ */
public class Constants { public class Constants {
@@ -12,93 +12,17 @@ public class Constants {
/** 地图网格尺寸32×32 格子) */ /** 地图网格尺寸32×32 格子) */
public static final int GRID_SIZE = 32; public static final int GRID_SIZE = 32;
/** 玩家碰撞体大小 */
public static final float PLAYER_SIZE = 0.8f;
/** 僵尸碰撞体大小 */
public static final float ZOMBIE_SIZE = 0.8f;
/** 服务器逻辑帧率(每秒 tick 数) */ /** 服务器逻辑帧率(每秒 tick 数) */
public static final int TICK_RATE = 30; public static final int TICK_RATE = 30;
/** 每次 tick 的时间间隔(秒) */ /** 每次 tick 的时间间隔(秒) */
public static final float TICK_INTERVAL = 1.0f / TICK_RATE; public static final float TICK_INTERVAL = 1.0f / TICK_RATE;
// ==================== 玩家参数 ==================== // ==================== 碰撞体尺寸 ====================
/** 玩家移动速度(格/秒) */ /** 玩家碰撞体大小 */
public static final float PLAYER_SPEED = 5.0f; public static final float PLAYER_SIZE = 0.8f;
/** 玩家最大生命值 */ /** 僵尸碰撞体大小 */
public static final float PLAYER_MAX_HEALTH = 100; public static final float ZOMBIE_SIZE = 0.8f;
/** 玩家受伤后的无敌时间(秒),防止连续受伤 */
public static final float PLAYER_INVULNERABLE_TIME = 0.5f;
/** 玩家死亡后的重生等待时间(秒) */
public static final float PLAYER_RESPAWN_TIME = 30.0f;
// ==================== 普通僵尸参数 ====================
/** 普通僵尸基础生命值 */
public static final float ZOMBIE_BASE_HEALTH = 100;
/** 普通僵尸基础移动速度(格/秒) */
public static final float ZOMBIE_BASE_SPEED = 2.0f;
/** 普通僵尸近战伤害 */
public static final float ZOMBIE_DAMAGE = 10;
/** 普通僵尸近战攻击间隔(秒) */
public static final float ZOMBIE_ATTACK_RATE = 1.0f;
/** 僵尸死亡后掉落物品的概率 */
public static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
/** 僵尸生成间隔基础值(秒),难度提升后会逐渐缩短 */
public static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
/** 僵尸生成间隔最小值(秒),防止生成过快 */
public static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.2f;
/** 每次难度提升增加的僵尸生命值 */
public static final float ZOMBIE_HEALTH_INCREASE = 20;
/** 每次难度提升增加的僵尸速度 */
public static final float ZOMBIE_SPEED_INCREASE = 0.1f;
/** 难度提升的时间间隔(秒) */
public static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
// ==================== 精英僵尸参数 ====================
// 精英僵尸:血量高、可远程射击的强力僵尸
/** 精英僵尸生命值 */
public static final float ELITE_ZOMBIE_HEALTH = 800;
/** 精英僵尸移动速度(比普通僵尸稍慢) */
public static final float ELITE_ZOMBIE_SPEED = 1.5f;
/** 精英僵尸近战伤害 */
public static final float ELITE_ZOMBIE_DAMAGE = 20;
/** 精英僵尸远程攻击范围(格) */
public static final float ELITE_ZOMBIE_ATTACK_RANGE = 8.0f;
/** 精英僵尸远程攻击间隔(秒) */
public static final float ELITE_ZOMBIE_ATTACK_RATE = 2.0f;
/** 精英僵尸远程子弹伤害 */
public static final int ELITE_ZOMBIE_BULLET_DAMAGE = 30;
/** 精英僵尸远程子弹飞行速度 */
public static final float ELITE_ZOMBIE_BULLET_SPEED = 6.0f;
/** 精英僵尸的生成概率5% */
public static final float ELITE_ZOMBIE_SPAWN_CHANCE = 0.05f;
// ==================== 分裂僵尸参数 ====================
// 分裂僵尸:被击杀后分裂成多个普通小僵尸
/** 分裂僵尸生命值(较低,但击杀后会分裂) */
public static final float SPLITTER_ZOMBIE_HEALTH = 50;
/** 分裂僵尸移动速度(比普通僵尸快) */
public static final float SPLITTER_ZOMBIE_SPEED = 2.5f;
/** 分裂僵尸死亡后最少分裂数量 */
public static final int SPLITTER_ZOMBIE_MIN_SPLIT = 2;
/** 分裂僵尸死亡后最多分裂数量 */
public static final int SPLITTER_ZOMBIE_MAX_SPLIT = 6;
/** 分裂僵尸的生成概率5% */
public static final float SPLITTER_ZOMBIE_SPAWN_CHANCE = 0.05f;
// ==================== 武器类型 ====================
/** 手枪(初始武器,弹药无限) */
public static final String WEAPON_PISTOL = "pistol";
/** 机枪(高射速,弹药有限) */
public static final String WEAPON_MACHINE_GUN = "machine_gun";
/** 霰弹枪(散射多弹丸,近距离高伤害) */
public static final String WEAPON_SHOTGUN = "shotgun";
/** 手榴弹(可蓄力投掷,爆炸范围伤害) */
public static final String WEAPON_GRENADE = "grenade";
// ==================== 掉落物类型 ==================== // ==================== 掉落物类型 ====================
@@ -110,7 +34,6 @@ public class Constants {
public static final float LOOT_HEALTH_AMOUNT = 30; public static final float LOOT_HEALTH_AMOUNT = 30;
// ==================== 网络消息类型 ==================== // ==================== 网络消息类型 ====================
// 客户端与服务器之间通信的消息类型标识
/** 创建房间 */ /** 创建房间 */
public static final String MSG_CREATE_ROOM = "create_room"; public static final String MSG_CREATE_ROOM = "create_room";

View File

@@ -1,5 +1,7 @@
package com.zombie.game.model; package com.zombie.game.model;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter; import lombok.Getter;
import java.util.*; import java.util.*;
@@ -40,6 +42,13 @@ public class GameWorld {
private List<Integer> removedBullets; private List<Integer> removedBullets;
private List<Integer> removedZombieBullets; 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() { public GameWorld() {
this("/Users/wfz/workspace/zp1/maps/d540209a.json"); this("/Users/wfz/workspace/zp1/maps/d540209a.json");
} }
@@ -60,9 +69,10 @@ public class GameWorld {
this.score = 0; this.score = 0;
this.spawnTimer = 0; this.spawnTimer = 0;
this.difficultyTimer = 0; this.difficultyTimer = 0;
this.zombieHealth = Constants.ZOMBIE_BASE_HEALTH; ZombieTemplate normal = TemplateManager.getInstance().getZombieTemplate("normal");
this.zombieSpeed = Constants.ZOMBIE_BASE_SPEED; this.zombieHealth = normal.getBaseHealth();
this.spawnInterval = Constants.ZOMBIE_SPAWN_INTERVAL_BASE; this.zombieSpeed = normal.getBaseSpeed();
this.spawnInterval = ZOMBIE_SPAWN_INTERVAL_BASE;
this.random = new Random(); this.random = new Random();
this.explosions = new ArrayList<>(); this.explosions = new ArrayList<>();
this.removedBullets = new ArrayList<>(); this.removedBullets = new ArrayList<>();
@@ -86,7 +96,7 @@ public class GameWorld {
/** /**
* 从游戏世界移除玩家 * 从游戏世界移除玩家
* *
* @param playerId 玩家ID * @param playerId 玩家ID
*/ */
public void removePlayer(String playerId) { public void removePlayer(String playerId) {
@@ -106,14 +116,14 @@ public class GameWorld {
/** /**
* 更新游戏世界状态 * 更新游戏世界状态
* *
* 每帧调用,处理: * 每帧调用,处理:
* - 时间流逝和难度提升 * - 时间流逝和难度提升
* - 僵尸生成 * - 僵尸生成
* - 实体移动和碰撞 * - 实体移动和碰撞
* - 掉落物收集 * - 掉落物收集
* - 玩家重生 * - 玩家重生
* *
* @param dt 时间增量(秒) * @param dt 时间增量(秒)
*/ */
public void update(float dt) { public void update(float dt) {
@@ -127,10 +137,10 @@ public class GameWorld {
updateFlowField(); updateFlowField();
if (difficultyTimer >= Constants.DIFFICULTY_INCREASE_INTERVAL) { if (difficultyTimer >= DIFFICULTY_INCREASE_INTERVAL) {
difficultyTimer -= Constants.DIFFICULTY_INCREASE_INTERVAL; difficultyTimer -= DIFFICULTY_INCREASE_INTERVAL;
waveNumber++; waveNumber++;
spawnInterval = Math.max(Constants.ZOMBIE_SPAWN_INTERVAL_MIN, spawnInterval = Math.max(ZOMBIE_SPAWN_INTERVAL_MIN,
spawnInterval - 0.3f); spawnInterval - 0.3f);
} }
@@ -151,7 +161,7 @@ public class GameWorld {
/** /**
* 更新流场导航 * 更新流场导航
* *
* 基于存活玩家的位置更新流场 * 基于存活玩家的位置更新流场
*/ */
private void updateFlowField() { private void updateFlowField() {
@@ -168,7 +178,7 @@ public class GameWorld {
/** /**
* 生成僵尸 * 生成僵尸
* *
* 根据概率生成普通僵尸、精英僵尸或分裂僵尸 * 根据概率生成普通僵尸、精英僵尸或分裂僵尸
*/ */
private void spawnZombie() { private void spawnZombie() {
@@ -177,21 +187,26 @@ public class GameWorld {
float wx = sp[0] + 0.5f; float wx = sp[0] + 0.5f;
float wy = sp[1] + 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(); float roll = random.nextFloat();
Zombie zombie; Zombie zombie;
if (roll < Constants.ELITE_ZOMBIE_SPAWN_CHANCE) { if (roll < eliteChance) {
zombie = new Zombie(nextZombieId++, wx, wy, Constants.ELITE_ZOMBIE_HEALTH, Constants.ELITE_ZOMBIE_SPEED, true, false); zombie = new Zombie(nextZombieId++, wx, wy, "elite");
} else if (roll < Constants.ELITE_ZOMBIE_SPAWN_CHANCE + Constants.SPLITTER_ZOMBIE_SPAWN_CHANCE) { } else if (roll < eliteChance + splitterChance) {
zombie = new Zombie(nextZombieId++, wx, wy, Constants.SPLITTER_ZOMBIE_HEALTH, Constants.SPLITTER_ZOMBIE_SPEED, false, true); zombie = new Zombie(nextZombieId++, wx, wy, "splitter");
} else { } else {
zombie = new Zombie(nextZombieId++, wx, wy, zombieHealth, zombieSpeed, false, false); zombie = new Zombie(nextZombieId++, wx, wy, "normal");
} }
zombies.put(zombie.getId(), zombie); zombies.put(zombie.getId(), zombie);
} }
/** /**
* 查找最近的存活玩家 * 查找最近的存活玩家
* *
* @param x X坐标 * @param x X坐标
* @param y Y坐标 * @param y Y坐标
* @return 最近的玩家,如果没有存活玩家则返回 null * @return 最近的玩家,如果没有存活玩家则返回 null
@@ -234,7 +249,7 @@ public class GameWorld {
Player nearest = findNearestPlayer(z.getX(), z.getY()); Player nearest = findNearestPlayer(z.getX(), z.getY());
if (nearest != null) { if (nearest != null) {
float dist = z.distanceTo(nearest.getX(), nearest.getY()); float dist = z.distanceTo(nearest.getX(), nearest.getY());
if (dist <= Constants.ELITE_ZOMBIE_ATTACK_RANGE && z.canRangedAttack(now)) { if (dist <= z.getTemplate().getRangedRange() && z.canRangedAttack(now)) {
fireZombieBullet(z, nearest); fireZombieBullet(z, nearest);
z.rangedAttack(now); z.rangedAttack(now);
} }
@@ -243,7 +258,7 @@ public class GameWorld {
Wall attackedWall = z.move(map, dt, zombies.values(), now); Wall attackedWall = z.move(map, dt, zombies.values(), now);
if (attackedWall != null && z.canAttack(now)) { if (attackedWall != null && z.canAttack(now)) {
attackedWall.takeDamage(1.0f); // 每次攻击造成1点伤害 attackedWall.takeDamage(1.0f);
z.attack(now); z.attack(now);
if (attackedWall.isDestroyed()) { if (attackedWall.isDestroyed()) {
map.removeWall(attackedWall.getGridX(), attackedWall.getGridY()); map.removeWall(attackedWall.getGridX(), attackedWall.getGridY());
@@ -254,7 +269,7 @@ public class GameWorld {
/** /**
* 精英僵尸发射子弹 * 精英僵尸发射子弹
* *
* @param zombie 发射子弹的僵尸 * @param zombie 发射子弹的僵尸
* @param target 目标玩家 * @param target 目标玩家
*/ */
@@ -267,14 +282,14 @@ public class GameWorld {
float startY = zombie.getY() + (float) Math.cos(angle) * 0.5f; float startY = zombie.getY() + (float) Math.cos(angle) * 0.5f;
Bullet bullet = new Bullet(nextZombieBulletId++, startX, startY, angle, Bullet bullet = new Bullet(nextZombieBulletId++, startX, startY, angle,
Constants.ELITE_ZOMBIE_BULLET_SPEED, Constants.ELITE_ZOMBIE_BULLET_DAMAGE, zombie.getTemplate().getRangedBulletSpeed(), zombie.getTemplate().getRangedDamage(),
"zombie_" + zombie.getId(), "zombie_bullet", 15); "zombie_" + zombie.getId(), -1, 15);
zombieBullets.put(bullet.getId(), bullet); zombieBullets.put(bullet.getId(), bullet);
} }
/** /**
* 更新所有僵尸子弹 * 更新所有僵尸子弹
* *
* @param dt 时间增量(秒) * @param dt 时间增量(秒)
*/ */
private void updateZombieBullets(float dt) { private void updateZombieBullets(float dt) {
@@ -301,7 +316,7 @@ public class GameWorld {
// 检测是否命中玩家 // 检测是否命中玩家
for (Player p : new ArrayList<>(players.values())) { for (Player p : new ArrayList<>(players.values())) {
if (!p.isAlive()) continue; if (!p.isAlive()) continue;
if (b.hitsEntity(p.getX(), p.getY(), Constants.PLAYER_SIZE)) { if (b.hitsEntity(p.getX(), p.getY(), PLAYER_SIZE)) {
p.takeDamage(b.getDamage()); p.takeDamage(b.getDamage());
hit = true; hit = true;
break; break;
@@ -334,31 +349,30 @@ public class GameWorld {
/** /**
* 处理僵尸死亡 * 处理僵尸死亡
* *
* - 分裂僵尸分裂成多个小僵尸 * - 分裂僵尸分裂成多个小僵尸
* - 增加分数 * - 增加分数
* - 可能掉落物品 * - 可能掉落物品
* *
* @param z 被击杀的僵尸 * @param z 被击杀的僵尸
*/ */
private void onZombieKilled(Zombie z) { private void onZombieKilled(Zombie z) {
if (z.isSplitter()) { if (z.isSplitter()) {
int splitCount = Constants.SPLITTER_ZOMBIE_MIN_SPLIT + int splitCount = z.getTemplate().getMinSplit() +
random.nextInt(Constants.SPLITTER_ZOMBIE_MAX_SPLIT - Constants.SPLITTER_ZOMBIE_MIN_SPLIT + 1); random.nextInt(z.getTemplate().getMaxSplit() - z.getTemplate().getMinSplit() + 1);
for (int i = 0; i < splitCount; i++) { for (int i = 0; i < splitCount; i++) {
float offsetX = (random.nextFloat() - 0.5f) * 1.0f; float offsetX = (random.nextFloat() - 0.5f) * 1.0f;
float offsetY = (random.nextFloat() - 0.5f) * 1.0f; float offsetY = (random.nextFloat() - 0.5f) * 1.0f;
Zombie splitZombie = new Zombie(nextZombieId++, Zombie splitZombie = new Zombie(nextZombieId++,
z.getX() + offsetX, z.getY() + offsetY, z.getX() + offsetX, z.getY() + offsetY, "normal");
zombieHealth, zombieSpeed, false, false);
zombies.put(splitZombie.getId(), splitZombie); zombies.put(splitZombie.getId(), splitZombie);
} }
score += 20; score += 20;
} else { } else {
score += z.isElite() ? 50 : 10; score += z.isElite() ? 50 : 10;
} }
if (random.nextFloat() < Constants.ZOMBIE_LOOT_DROP_CHANCE) { if (random.nextFloat() < ZOMBIE_LOOT_DROP_CHANCE) {
String lootType = random.nextFloat() < 0.5f ? Constants.LOOT_TYPE_AMMO : Constants.LOOT_TYPE_HEALTH; String lootType = random.nextFloat() < 0.5f ? LOOT_TYPE_AMMO : LOOT_TYPE_HEALTH;
Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType); Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType);
loots.put(loot.getId(), loot); loots.put(loot.getId(), loot);
} }
@@ -366,7 +380,7 @@ public class GameWorld {
/** /**
* 更新所有玩家子弹 * 更新所有玩家子弹
* *
* @param dt 时间增量(秒) * @param dt 时间增量(秒)
*/ */
private void updateBullets(float dt) { private void updateBullets(float dt) {
@@ -397,7 +411,7 @@ public class GameWorld {
boolean hit = false; boolean hit = false;
for (Zombie z : new ArrayList<>(zombies.values())) { for (Zombie z : new ArrayList<>(zombies.values())) {
if (!z.isAlive()) continue; if (!z.isAlive()) continue;
if (b.hitsEntity(z.getX(), z.getY(), Constants.ZOMBIE_SIZE)) { if (b.hitsEntity(z.getX(), z.getY(), ZOMBIE_SIZE)) {
z.takeDamage(b.getDamage()); z.takeDamage(b.getDamage());
hit = true; hit = true;
break; break;
@@ -430,9 +444,9 @@ public class GameWorld {
/** /**
* 创建爆炸效果 * 创建爆炸效果
* *
* 对范围内的僵尸造成伤害 * 对范围内的僵尸造成伤害
* *
* @param x 爆炸中心X坐标 * @param x 爆炸中心X坐标
* @param y 爆炸中心Y坐标 * @param y 爆炸中心Y坐标
* @param radius 爆炸半径 * @param radius 爆炸半径
@@ -464,7 +478,7 @@ public class GameWorld {
if (!p.isAlive()) continue; if (!p.isAlive()) continue;
float dist = z.distanceTo(p.getX(), p.getY()); float dist = z.distanceTo(p.getX(), p.getY());
if (dist < 1.0f && z.canAttack(now)) { if (dist < 1.0f && z.canAttack(now)) {
p.takeDamage(Constants.ZOMBIE_DAMAGE); p.takeDamage(z.getTemplate().getDamage());
z.attack(now); z.attack(now);
} }
} }
@@ -480,8 +494,8 @@ public class GameWorld {
for (Player p : players.values()) { for (Player p : players.values()) {
if (!p.isAlive()) continue; if (!p.isAlive()) continue;
if (loot.isCollectedBy(p.getX(), p.getY())) { if (loot.isCollectedBy(p.getX(), p.getY())) {
if (loot.getType().equals(Constants.LOOT_TYPE_HEALTH)) { if (loot.getType().equals(LOOT_TYPE_HEALTH)) {
p.heal(Constants.LOOT_HEALTH_AMOUNT); p.heal(LOOT_HEALTH_AMOUNT);
} else { } else {
p.refillRandomWeapon(); p.refillRandomWeapon();
} }
@@ -509,7 +523,7 @@ public class GameWorld {
for (Player p : players.values()) { for (Player p : players.values()) {
if (p.isWaitingForRespawn()) { if (p.isWaitingForRespawn()) {
p.updateRespawnTimer(Constants.TICK_INTERVAL); p.updateRespawnTimer(TICK_INTERVAL);
if (hasAlivePlayer && p.canRespawn()) { if (hasAlivePlayer && p.canRespawn()) {
List<int[]> spawnPoints = map.getSpawnPoints(); List<int[]> spawnPoints = map.getSpawnPoints();
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size())); int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
@@ -523,7 +537,7 @@ public class GameWorld {
/** /**
* 玩家开火(无蓄力) * 玩家开火(无蓄力)
* *
* @param player 玩家 * @param player 玩家
* @param aimX 瞄准X坐标 * @param aimX 瞄准X坐标
* @param aimY 瞄准Y坐标 * @param aimY 瞄准Y坐标
@@ -535,7 +549,7 @@ public class GameWorld {
/** /**
* 玩家开火(支持蓄力) * 玩家开火(支持蓄力)
* *
* @param player 玩家 * @param player 玩家
* @param aimX 瞄准X坐标 * @param aimX 瞄准X坐标
* @param aimY 瞄准Y坐标 * @param aimY 瞄准Y坐标
@@ -551,26 +565,18 @@ public class GameWorld {
player.setAngle(aimX, aimY); player.setAngle(aimX, aimY);
player.fire(now); player.fire(now);
String weapon; if (player.isChargeable()) {
switch (player.getWeaponIndex()) {
case 1: weapon = Constants.WEAPON_MACHINE_GUN; break;
case 2: weapon = Constants.WEAPON_SHOTGUN; break;
case 3: weapon = Constants.WEAPON_GRENADE; break;
default: weapon = Constants.WEAPON_PISTOL;
}
if (weapon.equals(Constants.WEAPON_GRENADE)) {
float startX = player.getX(); float startX = player.getX();
float startY = player.getY(); float startY = player.getY();
float minDist = 3.0f; float minDist = 3.0f;
float maxDist = 15.0f; float maxDist = 15.0f;
float dist = minDist + (maxDist - minDist) * chargePercent; float dist = minDist + (maxDist - minDist) * chargePercent;
float dx = aimX - startX; float dx = aimX - startX;
float dy = aimY - startY; float dy = aimY - startY;
float targetDist = (float) Math.sqrt(dx * dx + dy * dy); float targetDist = (float) Math.sqrt(dx * dx + dy * dy);
float targetX, targetY; float targetX, targetY;
if (targetDist < 0.1f) { if (targetDist < 0.1f) {
targetX = startX + minDist; targetX = startX + minDist;
@@ -580,12 +586,12 @@ public class GameWorld {
targetX = startX + dx * scale; targetX = startX + dx * scale;
targetY = startY + dy * scale; targetY = startY + dy * scale;
} }
targetX = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, targetX)); targetX = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetX));
targetY = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, targetY)); targetY = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetY));
float flightDuration = 0.8f + chargePercent * 0.7f; float flightDuration = 0.8f + chargePercent * 0.7f;
Bullet bullet = new Bullet(nextBulletId++, startX, startY, targetX, targetY, Bullet bullet = new Bullet(nextBulletId++, startX, startY, targetX, targetY,
flightDuration, player.getDamage(), player.getId(), 3.0f); flightDuration, player.getDamage(), player.getId(), 3.0f);
bullets.put(bullet.getId(), bullet); bullets.put(bullet.getId(), bullet);
@@ -593,6 +599,7 @@ public class GameWorld {
} else { } else {
int pellets = player.getPelletCount(); int pellets = player.getPelletCount();
float spread = player.getSpread(); float spread = player.getSpread();
float range = player.getWeaponIndex() == 1 ? 25 : (player.getWeaponIndex() == 2 ? 12 : 30);
for (int i = 0; i < pellets; i++) { for (int i = 0; i < pellets; i++) {
float angle = player.getAngle(); float angle = player.getAngle();
@@ -605,15 +612,9 @@ public class GameWorld {
float speed = player.getBulletSpeed(); float speed = player.getBulletSpeed();
int damage = player.getDamage(); int damage = player.getDamage();
float range;
switch (weapon) {
case Constants.WEAPON_MACHINE_GUN: range = 25; break;
case Constants.WEAPON_SHOTGUN: range = 12; break;
default: range = 30;
}
Bullet bullet = new Bullet(nextBulletId++, startX, startY, angle, Bullet bullet = new Bullet(nextBulletId++, startX, startY, angle,
speed, damage, player.getId(), weapon, range); speed, damage, player.getId(), player.getWeaponIndex(), range);
bullets.put(bullet.getId(), bullet); bullets.put(bullet.getId(), bullet);
newBulletIds.add(bullet.getId()); newBulletIds.add(bullet.getId());
} }
@@ -629,9 +630,9 @@ public class GameWorld {
/** /**
* 构建游戏状态数据 * 构建游戏状态数据
* *
* 将当前游戏世界的所有状态打包成Map格式用于网络传输 * 将当前游戏世界的所有状态打包成Map格式用于网络传输
* *
* @param forPlayerId 目标玩家ID用于发送该玩家的弹药信息 * @param forPlayerId 目标玩家ID用于发送该玩家的弹药信息
* @return 游戏状态Map * @return 游戏状态Map
*/ */
@@ -678,10 +679,12 @@ public class GameWorld {
Player forPlayer = players.get(forPlayerId); Player forPlayer = players.get(forPlayerId);
if (forPlayer != null) { if (forPlayer != null) {
Map<String, Object> ammoMap = new LinkedHashMap<>(); Map<String, Object> ammoMap = new LinkedHashMap<>();
ammoMap.put(Constants.WEAPON_PISTOL, forPlayer.getAmmo()[0] == Integer.MAX_VALUE ? -1 : forPlayer.getAmmo()[0]); int weaponCount = TemplateManager.getInstance().getAllWeaponTemplates().size();
ammoMap.put(Constants.WEAPON_MACHINE_GUN, (int) forPlayer.getAmmo()[1]); for (int i = 0; i < weaponCount; i++) {
ammoMap.put(Constants.WEAPON_SHOTGUN, (int) forPlayer.getAmmo()[2]); String weaponId = TemplateManager.getInstance().getWeaponId(i);
ammoMap.put(Constants.WEAPON_GRENADE, (int) forPlayer.getAmmo()[3]); float ammo = forPlayer.getAmmo()[i];
ammoMap.put(weaponId, ammo == Integer.MAX_VALUE ? -1 : (int) ammo);
}
state.put("ammo", ammoMap); state.put("ammo", ammoMap);
} }

View File

@@ -1,5 +1,8 @@
package com.zombie.game.model; 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 lombok.Getter;
import java.util.*; import java.util.*;
@@ -33,25 +36,30 @@ public class Player {
private float respawnTimer; private float respawnTimer;
private boolean waitingForRespawn; private boolean waitingForRespawn;
private static final String[] WEAPONS = { private static final List<WeaponTemplate> WEAPON_TEMPLATES;
Constants.WEAPON_PISTOL, Constants.WEAPON_MACHINE_GUN, static {
Constants.WEAPON_SHOTGUN, Constants.WEAPON_GRENADE List<WeaponTemplate> list = new ArrayList<>(TemplateManager.getInstance().getAllWeaponTemplates());
}; WEAPON_TEMPLATES = Collections.unmodifiableList(list);
}
private static final int[] MAX_AMMO = {Integer.MAX_VALUE, 100, 20, 10}; private final PlayerTemplate playerTemplate;
public Player(String id, String name, float x, float y) { public Player(String id, String name, float x, float y) {
this.playerTemplate = TemplateManager.getInstance().getPlayerTemplate();
this.id = id; this.id = id;
this.name = name; this.name = name;
this.x = x; this.x = x;
this.y = y; this.y = y;
this.angle = 0; this.angle = 0;
this.health = Constants.PLAYER_MAX_HEALTH; this.health = playerTemplate.getMaxHealth();
this.weaponIndex = 0; this.weaponIndex = 0;
this.ready = false; this.ready = false;
this.lastAttackTime = 0; this.lastAttackTime = 0;
this.lastDamageTime = 0; this.lastDamageTime = 0;
this.ammo = new float[]{Integer.MAX_VALUE, 100, 20, 10}; 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.firing = false;
this.grenadeChargeStart = 0; this.grenadeChargeStart = 0;
this.chargingGrenade = false; this.chargingGrenade = false;
@@ -61,95 +69,59 @@ public class Player {
} }
public void setReady(boolean ready) { this.ready = ready; } public void setReady(boolean ready) { this.ready = ready; }
public void setWeaponIndex(int idx) { this.weaponIndex = Math.max(0, Math.min(3, idx)); } public void setWeaponIndex(int idx) {
this.weaponIndex = Math.max(0, Math.min(WEAPON_TEMPLATES.size() - 1, idx));
}
/**
* 设置玩家位置
*
* @param x 新位置X坐标
* @param y 新位置Y坐标
*/
public void setPosition(float x, float y) { public void setPosition(float x, float y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
/**
* 应用移动输入
*
* @param dx X方向移动量
* @param dy Y方向移动量
* @param map 游戏地图(用于碰撞检测)
*/
public void applyMovement(float dx, float dy, GameMap map) { public void applyMovement(float dx, float dy, GameMap map) {
float speed = Constants.PLAYER_SPEED * Constants.TICK_INTERVAL; float speed = playerTemplate.getSpeed() * TICK_INTERVAL;
float newX = x + dx * speed; float newX = x + dx * speed;
float newY = y + dy * speed; float newY = y + dy * speed;
if (map.isWalkable(newX, y, Constants.PLAYER_SIZE)) { if (map.isWalkable(newX, y, playerTemplate.getSize())) {
x = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, newX)); x = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newX));
} }
if (map.isWalkable(x, newY, Constants.PLAYER_SIZE)) { if (map.isWalkable(x, newY, playerTemplate.getSize())) {
y = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, newY)); y = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newY));
} }
} }
/**
* 设置朝向角度
*
* @param aimX 瞄准点X坐标
* @param aimY 瞄准点Y坐标
*/
public void setAngle(float aimX, float aimY) { public void setAngle(float aimX, float aimY) {
this.angle = (float) Math.atan2(aimX - x, aimY - y); this.angle = (float) Math.atan2(aimX - x, aimY - y);
} }
/**
* 受到伤害
*
* @param damage 伤害值
*/
public void takeDamage(float damage) { public void takeDamage(float damage) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (now - lastDamageTime < Constants.PLAYER_INVULNERABLE_TIME * 1000) return; if (now - lastDamageTime < playerTemplate.getInvulnerableTime() * 1000) return;
this.health -= damage; this.health -= damage;
this.lastDamageTime = now;
if (this.health < 0) this.health = 0; if (this.health < 0) this.health = 0;
if (this.health <= 0) { if (this.health <= 0) {
startRespawnTimer(); startRespawnTimer();
} }
} }
/** 开始重生倒计时 */
public void startRespawnTimer() { public void startRespawnTimer() {
this.waitingForRespawn = true; this.waitingForRespawn = true;
this.respawnTimer = Constants.PLAYER_RESPAWN_TIME; this.respawnTimer = playerTemplate.getRespawnTime();
} }
/**
* 更新重生倒计时
*
* @param dt 时间增量(秒)
*/
public void updateRespawnTimer(float dt) { public void updateRespawnTimer(float dt) {
if (waitingForRespawn && respawnTimer > 0) { if (waitingForRespawn && respawnTimer > 0) {
respawnTimer -= dt; respawnTimer -= dt;
} }
} }
/** 检查是否可以重生 */
public boolean canRespawn() { public boolean canRespawn() {
return waitingForRespawn && respawnTimer <= 0; return waitingForRespawn && respawnTimer <= 0;
} }
/**
* 重生玩家
*
* @param newX 新位置X坐标
* @param newY 新位置Y坐标
*/
public void respawn(float newX, float newY) { public void respawn(float newX, float newY) {
this.health = Constants.PLAYER_MAX_HEALTH; this.health = playerTemplate.getMaxHealth();
this.x = newX; this.x = newX;
this.y = newY; this.y = newY;
this.waitingForRespawn = false; this.waitingForRespawn = false;
@@ -157,88 +129,54 @@ public class Player {
this.lastDamageTime = System.currentTimeMillis(); this.lastDamageTime = System.currentTimeMillis();
} }
/** 是否等待重生 */
public boolean isWaitingForRespawn() { public boolean isWaitingForRespawn() {
return waitingForRespawn; return waitingForRespawn;
} }
/** 获取重生倒计时 */
public float getRespawnTimer() { public float getRespawnTimer() {
return respawnTimer; return respawnTimer;
} }
/** 是否存活 */
public boolean isAlive() { public boolean isAlive() {
return health > 0; return health > 0;
} }
/**
* 计算到指定点的距离
*
* @param px 目标X坐标
* @param py 目标Y坐标
* @return 距离值
*/
public float distanceTo(float px, float py) { public float distanceTo(float px, float py) {
float dx = px - x; float dx = px - x;
float dy = py - y; float dy = py - y;
return (float) Math.sqrt(dx * dx + dy * dy); return (float) Math.sqrt(dx * dx + dy * dy);
} }
/**
* 检查是否可以开火
*
* @param now 当前时间戳
* @return true 表示可以开火
*/
public boolean canFire(long now) { public boolean canFire(long now) {
String weapon = WEAPONS[weaponIndex]; WeaponTemplate weapon = WEAPON_TEMPLATES.get(weaponIndex);
long fireRate = getFireRate(weapon); return now - lastAttackTime >= weapon.getFireRate();
return now - lastAttackTime >= fireRate;
} }
/**
* 执行开火动作
*
* @param now 当前时间戳
*/
public void fire(long now) { public void fire(long now) {
lastAttackTime = now; lastAttackTime = now;
String weapon = WEAPONS[weaponIndex]; if (weaponIndex != 0 && ammo[weaponIndex] > 0) {
int idx = weaponIndex; ammo[weaponIndex]--;
if (idx != 0 && ammo[idx] > 0) {
ammo[idx]--;
} }
} }
/** 检查是否有弹药 */
public boolean hasAmmo() { public boolean hasAmmo() {
if (weaponIndex == 0) return true; if (weaponIndex == 0) return true;
return ammo[weaponIndex] > 0; return ammo[weaponIndex] > 0;
} }
/** 随机补充一个武器的弹药 */
public void refillRandomWeapon() { public void refillRandomWeapon() {
Random rand = new Random(); Random rand = new Random();
int idx = rand.nextInt(3) + 1; int idx = rand.nextInt(WEAPON_TEMPLATES.size() - 1) + 1;
ammo[idx] = MAX_AMMO[idx]; ammo[idx] = WEAPON_TEMPLATES.get(idx).getMaxAmmo();
} }
/**
* 治疗
*
* @param amount 治疗量
*/
public void heal(float amount) { public void heal(float amount) {
this.health = Math.min(Constants.PLAYER_MAX_HEALTH, this.health + amount); this.health = Math.min(playerTemplate.getMaxHealth(), this.health + amount);
} }
/** 设置开火状态 */
public void setFiring(boolean firing) { this.firing = firing; } public void setFiring(boolean firing) { this.firing = firing; }
/** 设置最后处理的输入序列号 */
public void setLastProcessedSeq(int seq) { this.lastProcessedSeq = seq; } public void setLastProcessedSeq(int seq) { this.lastProcessedSeq = seq; }
/** 开始手榴弹蓄力 */
public void startGrenadeCharge() { public void startGrenadeCharge() {
if (!chargingGrenade) { if (!chargingGrenade) {
chargingGrenade = true; chargingGrenade = true;
@@ -246,80 +184,48 @@ public class Player {
} }
} }
/** 获取手榴弹蓄力百分比0-1 */
public float getGrenadeChargePercent() { public float getGrenadeChargePercent() {
if (!chargingGrenade) return 0; if (!chargingGrenade) return 0;
float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f; float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f;
return Math.min(1.0f, elapsed); return Math.min(1.0f, elapsed);
} }
/** 停止手榴弹蓄力 */
public void stopGrenadeCharge() { public void stopGrenadeCharge() {
chargingGrenade = false; chargingGrenade = false;
} }
/** public long getFireRate() {
* 获取武器射速 return WEAPON_TEMPLATES.get(weaponIndex).getFireRate();
*
* @param weapon 武器类型
* @return 射速(毫秒)
*/
private long getFireRate(String weapon) {
switch (weapon) {
case Constants.WEAPON_PISTOL: return 400;
case Constants.WEAPON_MACHINE_GUN: return 100;
case Constants.WEAPON_SHOTGUN: return 800;
case Constants.WEAPON_GRENADE: return 1500;
default: return 400;
}
} }
/** 获取当前武器伤害 */
public int getDamage() { public int getDamage() {
switch (WEAPONS[weaponIndex]) { return WEAPON_TEMPLATES.get(weaponIndex).getDamage();
case Constants.WEAPON_PISTOL: return 50;
case Constants.WEAPON_MACHINE_GUN: return 50;
case Constants.WEAPON_SHOTGUN: return 50;
case Constants.WEAPON_GRENADE: return 120;
default: return 50;
}
} }
/** 获取当前武器子弹速度 */
public float getBulletSpeed() { public float getBulletSpeed() {
switch (WEAPONS[weaponIndex]) { return WEAPON_TEMPLATES.get(weaponIndex).getBulletSpeed();
case Constants.WEAPON_PISTOL: return 20;
case Constants.WEAPON_MACHINE_GUN: return 25;
case Constants.WEAPON_SHOTGUN: return 18;
case Constants.WEAPON_GRENADE: return 12;
default: return 20;
}
} }
/** 获取当前武器弹丸数量(霰弹枪发射多个弹丸) */
public int getPelletCount() { public int getPelletCount() {
return WEAPONS[weaponIndex].equals(Constants.WEAPON_SHOTGUN) ? 10 : 1; return WEAPON_TEMPLATES.get(weaponIndex).getPelletCount();
} }
/** 获取当前武器散射角度 */
public float getSpread() { public float getSpread() {
switch (WEAPONS[weaponIndex]) { return WEAPON_TEMPLATES.get(weaponIndex).getSpread();
case Constants.WEAPON_MACHINE_GUN: return 0.05f;
case Constants.WEAPON_SHOTGUN: return 0.15f;
default: return 0;
}
} }
/** 当前武器是否可蓄力(手榴弹) */
public boolean isChargeable() { public boolean isChargeable() {
return WEAPONS[weaponIndex].equals(Constants.WEAPON_GRENADE); return WEAPON_TEMPLATES.get(weaponIndex).isChargeable();
}
public boolean isExplosive() {
return WEAPON_TEMPLATES.get(weaponIndex).isExplosive();
}
public float getExplosionRadius() {
return WEAPON_TEMPLATES.get(weaponIndex).getExplosionRadius();
} }
/**
* 将玩家状态转换为Map格式用于网络传输
*
* @return 包含玩家状态的Map
*/
public Map<String, Object> toStateMap() { public Map<String, Object> toStateMap() {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id); map.put("id", id);

View File

@@ -1,5 +1,7 @@
package com.zombie.game.model; package com.zombie.game.model;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter; import lombok.Getter;
import java.util.*; import java.util.*;
@@ -12,7 +14,7 @@ import static com.zombie.game.model.Constants.*;
* - 位置、朝向、生命值 * - 位置、朝向、生命值
* - 移动和寻路(基于流场导航) * - 移动和寻路(基于流场导航)
* - 近战和远程攻击 * - 近战和远程攻击
* - 三种类型:普通僵尸、精英僵尸、分裂僵尸 * - 基于模板配置的属性
*/ */
@Getter @Getter
public class Zombie { public class Zombie {
@@ -23,36 +25,30 @@ public class Zombie {
private float maxHealth; private float maxHealth;
private float speed; private float speed;
private long lastAttackTime; private long lastAttackTime;
private boolean isElite;
private boolean isSplitter;
private long lastRangedAttackTime; private long lastRangedAttackTime;
private float targetX, targetY; private float targetX, targetY;
private boolean hasTarget; private boolean hasTarget;
private int reservedGridX, reservedGridY; private int reservedGridX, reservedGridY;
private boolean reservation; // 是否有预留格子 private boolean reservation;
private int attackingWallGridX = -1; // 正在攻击的坚果墙体格子X private int attackingWallGridX = -1;
private int attackingWallGridY = -1; // 正在攻击的坚果墙体格子Y private int attackingWallGridY = -1;
private boolean attackingWall; // 是否正在攻击墙体 private boolean attackingWall;
public Zombie(int id, float x, float y, float health, float speed) { private final ZombieTemplate template;
this(id, x, y, health, speed, false, false);
}
public Zombie(int id, float x, float y, float health, float speed, boolean isElite) { public Zombie(int id, float x, float y, String templateId) {
this(id, x, y, health, speed, isElite, false); this.template = TemplateManager.getInstance().getZombieTemplate(templateId);
} if (this.template == null) {
throw new IllegalArgumentException("Unknown zombie template: " + templateId);
public Zombie(int id, float x, float y, float health, float speed, boolean isElite, boolean isSplitter) { }
this.id = id; this.id = id;
this.x = x; this.x = x;
this.y = y; this.y = y;
this.angle = 0; this.angle = 0;
this.health = health; this.health = template.getBaseHealth();
this.maxHealth = health; this.maxHealth = template.getBaseHealth();
this.speed = speed; this.speed = template.getBaseSpeed();
this.lastAttackTime = 0; this.lastAttackTime = 0;
this.isElite = isElite;
this.isSplitter = isSplitter;
this.lastRangedAttackTime = 0; this.lastRangedAttackTime = 0;
this.targetX = 0; this.targetX = 0;
this.targetY = 0; this.targetY = 0;
@@ -65,55 +61,34 @@ public class Zombie {
this.attackingWall = false; this.attackingWall = false;
} }
/** public boolean isElite() { return template.isCanRangedAttack(); }
* 受到伤害 public boolean isSplitter() { return template.isCanSplit(); }
*
* @param damage 伤害值
*/
public void takeDamage(float damage) { public void takeDamage(float damage) {
this.health -= damage; this.health -= damage;
if (this.health < 0) this.health = 0; if (this.health < 0) this.health = 0;
} }
/** 是否存活 */
public boolean isAlive() { public boolean isAlive() {
return health > 0; return health > 0;
} }
/**
* 移动僵尸
*
* 基于流场导航移动,包含:
* - 路径规划(支持加权障碍物,自动权衡绕道 vs 摧毁)
* - 避免与其他僵尸重叠
* - 墙壁碰撞检测
* - 攻击坚果墙体逻辑
*
* @param map 游戏地图
* @param dt 时间增量(秒)
* @param otherZombies 其他僵尸集合
* @param now 当前时间戳
* @return 如果正在攻击墙体,返回被攻击的墙体对象;否则返回 null
*/
public Wall move(GameMap map, float dt, Collection<Zombie> otherZombies, long now) { public Wall move(GameMap map, float dt, Collection<Zombie> otherZombies, long now) {
if (!map.isFlowFieldValid()) return null; if (!map.isFlowFieldValid()) return null;
int currentGridX = (int) Math.floor(x); int currentGridX = (int) Math.floor(x);
int currentGridY = (int) Math.floor(y); int currentGridY = (int) Math.floor(y);
// 如果正在攻击墙体,检查是否到达攻击位置
if (attackingWall && attackingWallGridX >= 0) { if (attackingWall && attackingWallGridX >= 0) {
float wallCenterX = attackingWallGridX + 0.5f; float wallCenterX = attackingWallGridX + 0.5f;
float wallCenterY = attackingWallGridY + 0.5f; float wallCenterY = attackingWallGridY + 0.5f;
float distToWall = distanceTo(wallCenterX, wallCenterY); float distToWall = distanceTo(wallCenterX, wallCenterY);
if (distToWall < 0.8f) { if (distToWall < 0.8f) {
// 到达攻击位置,返回要攻击的墙体
Wall wall = map.getWall(attackingWallGridX, attackingWallGridY); Wall wall = map.getWall(attackingWallGridX, attackingWallGridY);
if (wall != null && !wall.isDestroyed()) { if (wall != null && !wall.isDestroyed()) {
return wall; return wall;
} }
// 墙体已被破坏或被移除,停止攻击
attackingWall = false; attackingWall = false;
attackingWallGridX = -1; attackingWallGridX = -1;
attackingWallGridY = -1; attackingWallGridY = -1;
@@ -143,10 +118,7 @@ public class Zombie {
int nextGridX = currentGridX + (int) Math.round(dirX); int nextGridX = currentGridX + (int) Math.round(dirX);
int nextGridY = currentGridY + (int) Math.round(dirY); int nextGridY = currentGridY + (int) Math.round(dirY);
// 检查下一个格子是否是坚果墙体
if (map.isNutWall(nextGridX, nextGridY)) { if (map.isNutWall(nextGridX, nextGridY)) {
// 流场指引我们走向坚果,说明摧毁代价比绕道低
// 设置攻击状态
attackingWall = true; attackingWall = true;
attackingWallGridX = nextGridX; attackingWallGridX = nextGridX;
attackingWallGridY = nextGridY; attackingWallGridY = nextGridY;
@@ -231,9 +203,9 @@ public class Zombie {
float newX = x + moveX; float newX = x + moveX;
float newY = y + moveY; float newY = y + moveY;
boolean canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE); boolean canMoveX = map.isWalkable(newX, y, ZOMBIE_SIZE);
boolean canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE); boolean canMoveY = map.isWalkable(x, newY, ZOMBIE_SIZE);
boolean canMoveDiagonal = map.isWalkable(newX, newY, Constants.ZOMBIE_SIZE); boolean canMoveDiagonal = map.isWalkable(newX, newY, ZOMBIE_SIZE);
if (moveX != 0 && moveY != 0) { if (moveX != 0 && moveY != 0) {
int checkX = (int) Math.floor(newX); int checkX = (int) Math.floor(newX);
@@ -251,10 +223,10 @@ public class Zombie {
if (!wallInX && canMoveX) { if (!wallInX && canMoveX) {
x = newX; x = newX;
canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE); canMoveY = map.isWalkable(x, newY, ZOMBIE_SIZE);
} else if (!wallInY && canMoveY) { } else if (!wallInY && canMoveY) {
y = newY; y = newY;
canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE); canMoveX = map.isWalkable(newX, y, ZOMBIE_SIZE);
} }
} }
} }
@@ -271,7 +243,7 @@ public class Zombie {
if (canMoveY) y = newY; if (canMoveY) y = newY;
} }
float minSeparationDist = Constants.ZOMBIE_SIZE; float minSeparationDist = ZOMBIE_SIZE;
for (Zombie other : otherZombies) { for (Zombie other : otherZombies) {
if (other.getId() == this.id) continue; if (other.getId() == this.id) continue;
if (!other.isAlive()) continue; if (!other.isAlive()) continue;
@@ -290,14 +262,14 @@ public class Zombie {
float pushedX = x + pushX; float pushedX = x + pushX;
float pushedY = y + pushY; float pushedY = y + pushY;
if (map.isWalkable(pushedX, pushedY, Constants.ZOMBIE_SIZE)) { if (map.isWalkable(pushedX, pushedY, ZOMBIE_SIZE)) {
x = pushedX; x = pushedX;
y = pushedY; y = pushedY;
} else { } else {
if (map.isWalkable(x + pushX, y, Constants.ZOMBIE_SIZE)) { if (map.isWalkable(x + pushX, y, ZOMBIE_SIZE)) {
x = x + pushX; x = x + pushX;
} }
if (map.isWalkable(x, y + pushY, Constants.ZOMBIE_SIZE)) { if (map.isWalkable(x, y + pushY, ZOMBIE_SIZE)) {
y = y + pushY; y = y + pushY;
} }
} }
@@ -310,123 +282,81 @@ public class Zombie {
return null; return null;
} }
/**
* 检查指定格子是否被其他僵尸占用或预留
*/
private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> otherZombies) { private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> otherZombies) {
for (Zombie other : otherZombies) { for (Zombie other : otherZombies) {
if (other.getId() == this.id) continue; if (other.getId() == this.id) continue;
if (!other.isAlive()) continue; if (!other.isAlive()) continue;
int otherGridX = (int) Math.floor(other.getX()); int otherGridX = (int) Math.floor(other.getX());
int otherGridY = (int) Math.floor(other.getY()); int otherGridY = (int) Math.floor(other.getY());
if (otherGridX == gridX && otherGridY == gridY) { if (otherGridX == gridX && otherGridY == gridY) {
return true; return true;
} }
if (other.isReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) { if (other.isReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
return true; return true;
} }
} }
return false; return false;
} }
/** private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
* 寻找替代移动方向
*
* 当目标格子被占用时,寻找其他可行方向
*/
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
GameMap map, Collection<Zombie> otherZombies) { GameMap map, Collection<Zombie> otherZombies) {
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; 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<>(); java.util.List<int[]> candidates = new java.util.ArrayList<>();
for (int[] dir : allDirs) { for (int[] dir : allDirs) {
int nx = currentGridX + dir[0]; int nx = currentGridX + dir[0];
int ny = currentGridY + dir[1]; int ny = currentGridY + dir[1];
if (map.isWall(nx, ny)) continue; if (map.isWall(nx, ny)) continue;
if (dir[0] != 0 && dir[1] != 0) { if (dir[0] != 0 && dir[1] != 0) {
if (map.isWall(currentGridX + dir[0], currentGridY) || if (map.isWall(currentGridX + dir[0], currentGridY) ||
map.isWall(currentGridX, currentGridY + dir[1])) { map.isWall(currentGridX, currentGridY + dir[1])) {
continue; continue;
} }
} }
if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue; if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue;
float dotProduct = dir[0] * dirX + dir[1] * dirY; float dotProduct = dir[0] * dirX + dir[1] * dirY;
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)}); candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
} }
if (candidates.isEmpty()) return null; if (candidates.isEmpty()) return null;
candidates.sort((a, b) -> b[2] - a[2]); candidates.sort((a, b) -> b[2] - a[2]);
return new int[]{candidates.get(0)[0], candidates.get(0)[1]}; return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
} }
/**
* 检查是否可以近战攻击
*
* @param now 当前时间戳
* @return true 表示可以攻击
*/
public boolean canAttack(long now) { public boolean canAttack(long now) {
return now - lastAttackTime >= Constants.ZOMBIE_ATTACK_RATE * 1000; return now - lastAttackTime >= template.getAttackRate() * 1000;
} }
/**
* 执行近战攻击
*
* @param now 当前时间戳
*/
public void attack(long now) { public void attack(long now) {
lastAttackTime = now; lastAttackTime = now;
} }
/**
* 检查是否可以远程攻击(精英僵尸专用)
*
* @param now 当前时间戳
* @return true 表示可以攻击
*/
public boolean canRangedAttack(long now) { public boolean canRangedAttack(long now) {
if (!isElite) return false; if (!template.isCanRangedAttack()) return false;
return now - lastRangedAttackTime >= Constants.ELITE_ZOMBIE_ATTACK_RATE * 1000; return now - lastRangedAttackTime >= template.getAttackRate() * 1000;
} }
/**
* 执行远程攻击(精英僵尸专用)
*
* @param now 当前时间戳
*/
public void rangedAttack(long now) { public void rangedAttack(long now) {
lastRangedAttackTime = now; lastRangedAttackTime = now;
} }
/**
* 计算到指定点的距离
*
* @param px 目标X坐标
* @param py 目标Y坐标
* @return 距离值
*/
public float distanceTo(float px, float py) { public float distanceTo(float px, float py) {
float dx = px - x; float dx = px - x;
float dy = py - y; float dy = py - y;
return (float) Math.sqrt(dx * dx + dy * dy); return (float) Math.sqrt(dx * dx + dy * dy);
} }
/**
* 将僵尸状态转换为Map格式用于网络传输
*
* @return 包含僵尸状态的Map
*/
public Map<String, Object> toStateMap() { public Map<String, Object> toStateMap() {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("id", id); map.put("id", id);
@@ -434,8 +364,8 @@ public class Zombie {
map.put("y", y); map.put("y", y);
map.put("angle", angle); map.put("angle", angle);
map.put("health", health); map.put("health", health);
map.put("isElite", isElite); map.put("isElite", isElite());
map.put("isSplitter", isSplitter); map.put("isSplitter", isSplitter());
return map; return map;
} }
} }

View File

@@ -0,0 +1,26 @@
package com.zombie.game.template;
import lombok.Getter;
/**
* 玩家基础属性模板
*
* 定义玩家的基础属性,所有属性在加载时确定,运行时只读。
*/
@Getter
public class PlayerTemplate {
private final float speed;
private final float maxHealth;
private final float invulnerableTime;
private final float respawnTime;
private final float size;
public PlayerTemplate(float speed, float maxHealth, float invulnerableTime,
float respawnTime, float size) {
this.speed = speed;
this.maxHealth = maxHealth;
this.invulnerableTime = invulnerableTime;
this.respawnTime = respawnTime;
this.size = size;
}
}

View File

@@ -0,0 +1,163 @@
package com.zombie.game.template;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.util.*;
/**
* 模板管理器
*
* 负责从 JSON 配置文件加载和管理所有游戏模板(僵尸、武器、玩家)。
* 单例模式,在服务器启动时加载一次。
*/
public class TemplateManager {
private static final Logger logger = LoggerFactory.getLogger(TemplateManager.class);
private static TemplateManager instance;
private final Map<String, ZombieTemplate> zombieTemplates = new LinkedHashMap<>();
private final Map<String, WeaponTemplate> weaponTemplates = new LinkedHashMap<>();
private PlayerTemplate playerTemplate;
private TemplateManager() {
loadAll();
}
public static synchronized TemplateManager getInstance() {
if (instance == null) {
instance = new TemplateManager();
}
return instance;
}
/**
* 加载所有模板
*/
private void loadAll() {
loadZombies();
loadWeapons();
loadPlayer();
logger.info("Templates loaded: {} zombies, {} weapons, 1 player base",
zombieTemplates.size(), weaponTemplates.size());
}
private void loadZombies() {
try (InputStream is = getClass().getResourceAsStream("/data/zombies.json")) {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(is);
JsonNode types = root.get("types");
for (JsonNode node : types) {
ZombieTemplate t = new ZombieTemplate(
node.get("id").asText(),
node.get("name").asText(),
(float) node.get("baseHealth").asDouble(),
(float) node.get("baseSpeed").asDouble(),
(float) node.get("damage").asDouble(),
(float) node.get("attackRate").asDouble(),
node.get("canRangedAttack").asBoolean(),
(float) node.get("rangedRange").asDouble(),
node.get("rangedDamage").asInt(),
(float) node.get("rangedBulletSpeed").asDouble(),
node.get("canSplit").asBoolean(),
node.get("minSplit").asInt(),
node.get("maxSplit").asInt(),
(float) node.get("spawnWeight").asDouble()
);
zombieTemplates.put(t.getId(), t);
}
} catch (Exception e) {
throw new RuntimeException("Failed to load zombie templates", e);
}
}
private void loadWeapons() {
try (InputStream is = getClass().getResourceAsStream("/data/weapons.json")) {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(is);
JsonNode types = root.get("types");
for (JsonNode node : types) {
WeaponTemplate t = new WeaponTemplate(
node.get("id").asText(),
node.get("name").asText(),
node.get("damage").asInt(),
node.get("fireRate").asLong(),
node.get("pelletCount").asInt(),
(float) node.get("spread").asDouble(),
(float) node.get("bulletSpeed").asDouble(),
(float) node.get("range").asDouble(),
node.get("maxAmmo").asInt(),
node.get("chargeable").asBoolean(),
node.get("explosive").asBoolean(),
(float) node.get("explosionRadius").asDouble()
);
weaponTemplates.put(t.getId(), t);
}
} catch (Exception e) {
throw new RuntimeException("Failed to load weapon templates", e);
}
}
private void loadPlayer() {
try (InputStream is = getClass().getResourceAsStream("/data/players.json")) {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(is);
JsonNode base = root.get("base");
playerTemplate = new PlayerTemplate(
(float) base.get("speed").asDouble(),
(float) base.get("maxHealth").asDouble(),
(float) base.get("invulnerableTime").asDouble(),
(float) base.get("respawnTime").asDouble(),
(float) base.get("size").asDouble()
);
} catch (Exception e) {
throw new RuntimeException("Failed to load player template", e);
}
}
public ZombieTemplate getZombieTemplate(String id) {
return zombieTemplates.get(id);
}
public Collection<ZombieTemplate> getAllZombieTemplates() {
return zombieTemplates.values();
}
public WeaponTemplate getWeaponTemplate(String id) {
return weaponTemplates.get(id);
}
public Collection<WeaponTemplate> getAllWeaponTemplates() {
return weaponTemplates.values();
}
public PlayerTemplate getPlayerTemplate() {
return playerTemplate;
}
/**
* 获取武器列表的索引位置
*/
public int getWeaponIndex(String id) {
int idx = 0;
for (String key : weaponTemplates.keySet()) {
if (key.equals(id)) return idx;
idx++;
}
return 0;
}
/**
* 根据索引获取武器ID
*/
public String getWeaponId(int index) {
int idx = 0;
for (String key : weaponTemplates.keySet()) {
if (idx == index) return key;
idx++;
}
return weaponTemplates.keySet().iterator().next();
}
}

View File

@@ -0,0 +1,42 @@
package com.zombie.game.template;
import lombok.Getter;
/**
* 武器类型模板
*
* 定义一种武器的基础属性,所有属性在加载时确定,运行时只读。
*/
@Getter
public class WeaponTemplate {
private final String id;
private final String name;
private final int damage;
private final long fireRate;
private final int pelletCount;
private final float spread;
private final float bulletSpeed;
private final float range;
private final int maxAmmo;
private final boolean chargeable;
private final boolean explosive;
private final float explosionRadius;
public WeaponTemplate(String id, String name, int damage, long fireRate,
int pelletCount, float spread, float bulletSpeed,
float range, int maxAmmo, boolean chargeable,
boolean explosive, float explosionRadius) {
this.id = id;
this.name = name;
this.damage = damage;
this.fireRate = fireRate;
this.pelletCount = pelletCount;
this.spread = spread;
this.bulletSpeed = bulletSpeed;
this.range = range;
this.maxAmmo = maxAmmo;
this.chargeable = chargeable;
this.explosive = explosive;
this.explosionRadius = explosionRadius;
}
}

View File

@@ -0,0 +1,46 @@
package com.zombie.game.template;
import lombok.Getter;
/**
* 僵尸类型模板
*
* 定义一种僵尸的基础属性,所有属性在加载时确定,运行时只读。
*/
@Getter
public class ZombieTemplate {
private final String id;
private final String name;
private final float baseHealth;
private final float baseSpeed;
private final float damage;
private final float attackRate;
private final boolean canRangedAttack;
private final float rangedRange;
private final int rangedDamage;
private final float rangedBulletSpeed;
private final boolean canSplit;
private final int minSplit;
private final int maxSplit;
private final float spawnWeight;
public ZombieTemplate(String id, String name, float baseHealth, float baseSpeed,
float damage, float attackRate, boolean canRangedAttack,
float rangedRange, int rangedDamage, float rangedBulletSpeed,
boolean canSplit, int minSplit, int maxSplit, float spawnWeight) {
this.id = id;
this.name = name;
this.baseHealth = baseHealth;
this.baseSpeed = baseSpeed;
this.damage = damage;
this.attackRate = attackRate;
this.canRangedAttack = canRangedAttack;
this.rangedRange = rangedRange;
this.rangedDamage = rangedDamage;
this.rangedBulletSpeed = rangedBulletSpeed;
this.canSplit = canSplit;
this.minSplit = minSplit;
this.maxSplit = maxSplit;
this.spawnWeight = spawnWeight;
}
}

View File

@@ -0,0 +1,9 @@
{
"base": {
"speed": 5.0,
"maxHealth": 100,
"invulnerableTime": 0.5,
"respawnTime": 30.0,
"size": 0.8
}
}

View File

@@ -0,0 +1,60 @@
{
"types": [
{
"id": "pistol",
"name": "手枪",
"damage": 50,
"fireRate": 400,
"pelletCount": 1,
"spread": 0,
"bulletSpeed": 20,
"range": 30,
"maxAmmo": 2147483647,
"chargeable": false,
"explosive": false,
"explosionRadius": 0
},
{
"id": "machine_gun",
"name": "机枪",
"damage": 50,
"fireRate": 100,
"pelletCount": 1,
"spread": 0.05,
"bulletSpeed": 25,
"range": 25,
"maxAmmo": 100,
"chargeable": false,
"explosive": false,
"explosionRadius": 0
},
{
"id": "shotgun",
"name": "霰弹枪",
"damage": 50,
"fireRate": 800,
"pelletCount": 10,
"spread": 0.15,
"bulletSpeed": 18,
"range": 12,
"maxAmmo": 20,
"chargeable": false,
"explosive": false,
"explosionRadius": 0
},
{
"id": "grenade",
"name": "手榴弹",
"damage": 120,
"fireRate": 1500,
"pelletCount": 1,
"spread": 0,
"bulletSpeed": 12,
"range": 15,
"maxAmmo": 10,
"chargeable": true,
"explosive": true,
"explosionRadius": 3
}
]
}

View File

@@ -0,0 +1,52 @@
{
"types": [
{
"id": "normal",
"name": "普通僵尸",
"baseHealth": 100,
"baseSpeed": 2.0,
"damage": 10,
"attackRate": 1.0,
"canRangedAttack": false,
"rangedRange": 0,
"rangedDamage": 0,
"rangedBulletSpeed": 0,
"canSplit": false,
"minSplit": 0,
"maxSplit": 0,
"spawnWeight": 1.0
},
{
"id": "elite",
"name": "精英僵尸",
"baseHealth": 800,
"baseSpeed": 1.5,
"damage": 20,
"attackRate": 2.0,
"canRangedAttack": true,
"rangedRange": 8.0,
"rangedDamage": 30,
"rangedBulletSpeed": 6.0,
"canSplit": false,
"minSplit": 0,
"maxSplit": 0,
"spawnWeight": 0.05
},
{
"id": "splitter",
"name": "分裂僵尸",
"baseHealth": 50,
"baseSpeed": 2.5,
"damage": 10,
"attackRate": 1.0,
"canRangedAttack": false,
"rangedRange": 0,
"rangedDamage": 0,
"rangedBulletSpeed": 0,
"canSplit": true,
"minSplit": 2,
"maxSplit": 6,
"spawnWeight": 0.05
}
]
}