1
This commit is contained in:
@@ -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,14 +42,14 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -108,7 +108,7 @@ public class Bullet {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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<>();
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,14 +187,19 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -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());
|
||||||
@@ -267,8 +282,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -343,22 +358,21 @@ public class GameWorld {
|
|||||||
*/
|
*/
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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()));
|
||||||
@@ -551,15 +565,7 @@ 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();
|
||||||
|
|
||||||
@@ -581,8 +587,8 @@ public class GameWorld {
|
|||||||
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;
|
||||||
|
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,9 +283,6 @@ 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;
|
||||||
@@ -333,11 +302,6 @@ public class Zombie {
|
|||||||
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}};
|
||||||
@@ -370,63 +334,29 @@ public class Zombie {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/main/resources/data/players.json
Normal file
9
backend/src/main/resources/data/players.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"base": {
|
||||||
|
"speed": 5.0,
|
||||||
|
"maxHealth": 100,
|
||||||
|
"invulnerableTime": 0.5,
|
||||||
|
"respawnTime": 30.0,
|
||||||
|
"size": 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/main/resources/data/weapons.json
Normal file
60
backend/src/main/resources/data/weapons.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
52
backend/src/main/resources/data/zombies.json
Normal file
52
backend/src/main/resources/data/zombies.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user