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 int damage;
private String ownerId;
private String weapon;
private int weaponIndex;
private float range;
private float distanceTraveled;
private boolean explosive;
@@ -31,7 +31,7 @@ public class Bullet {
private boolean isGrenade;
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.x = x;
this.y = y;
@@ -42,14 +42,14 @@ public class Bullet {
this.vz = 0;
this.damage = damage;
this.ownerId = ownerId;
this.weapon = weapon;
this.weaponIndex = weaponIndex;
this.range = range;
this.distanceTraveled = 0;
this.explosive = weapon.equals(Constants.WEAPON_GRENADE);
this.explosionRadius = explosive ? 3.0f : 0;
this.explosive = false;
this.explosionRadius = 0;
this.flightTime = 0;
this.maxFlightTime = 0;
this.isGrenade = weapon.equals(Constants.WEAPON_GRENADE);
this.isGrenade = false;
this.targetX = x + (float) Math.sin(angle) * range;
this.targetY = y + (float) Math.cos(angle) * range;
}
@@ -77,7 +77,7 @@ public class Bullet {
this.targetY = targetY;
this.damage = damage;
this.ownerId = ownerId;
this.weapon = Constants.WEAPON_GRENADE;
this.weaponIndex = -1;
this.range = 0;
this.distanceTraveled = 0;
this.explosive = true;
@@ -108,7 +108,7 @@ public class Bullet {
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;
}
@@ -121,7 +121,7 @@ public class Bullet {
distanceTraveled += (float) Math.sqrt(moveX * moveX + moveY * moveY);
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 gy = (int) Math.floor(y);
@@ -166,7 +166,7 @@ public class Bullet {
} else {
map.put("angle", (float) Math.atan2(vx, vy));
}
map.put("weapon", weapon);
map.put("weaponIndex", weaponIndex);
map.put("ownerId", ownerId);
return map;
}

View File

@@ -3,8 +3,8 @@ package com.zombie.game.model;
/**
* 游戏常量定义类
*
* 集中管理所有游戏平衡性参数、消息类型常量
* 修改此文件中的数值可以调整游戏难度和行为
* 只保留真正的系统常量(不会随游戏平衡性调整而改变的值)
* 所有可配置的游戏平衡参数已迁移到 JSON 模板文件
*/
public class Constants {
@@ -12,93 +12,17 @@ public class Constants {
/** 地图网格尺寸32×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 数) */
public static final int TICK_RATE = 30;
/** 每次 tick 的时间间隔(秒) */
public static final float TICK_INTERVAL = 1.0f / TICK_RATE;
// ==================== 玩家参数 ====================
// ==================== 碰撞体尺寸 ====================
/** 玩家移动速度(格/秒) */
public static final float PLAYER_SPEED = 5.0f;
/** 玩家最大生命值 */
public static final float PLAYER_MAX_HEALTH = 100;
/** 玩家受伤后的无敌时间(秒),防止连续受伤 */
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";
/** 玩家碰撞体大小 */
public static final float PLAYER_SIZE = 0.8f;
/** 僵尸碰撞体大小 */
public static final float ZOMBIE_SIZE = 0.8f;
// ==================== 掉落物类型 ====================
@@ -110,7 +34,6 @@ public class Constants {
public static final float LOOT_HEALTH_AMOUNT = 30;
// ==================== 网络消息类型 ====================
// 客户端与服务器之间通信的消息类型标识
/** 创建房间 */
public static final String MSG_CREATE_ROOM = "create_room";

View File

@@ -1,5 +1,7 @@
package com.zombie.game.model;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter;
import java.util.*;
@@ -40,6 +42,13 @@ public class GameWorld {
private List<Integer> removedBullets;
private List<Integer> removedZombieBullets;
private static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
private static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.2f;
private static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
private static final float ZOMBIE_HEALTH_INCREASE = 20;
private static final float ZOMBIE_SPEED_INCREASE = 0.1f;
private static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
public GameWorld() {
this("/Users/wfz/workspace/zp1/maps/d540209a.json");
}
@@ -60,9 +69,10 @@ public class GameWorld {
this.score = 0;
this.spawnTimer = 0;
this.difficultyTimer = 0;
this.zombieHealth = Constants.ZOMBIE_BASE_HEALTH;
this.zombieSpeed = Constants.ZOMBIE_BASE_SPEED;
this.spawnInterval = Constants.ZOMBIE_SPAWN_INTERVAL_BASE;
ZombieTemplate normal = TemplateManager.getInstance().getZombieTemplate("normal");
this.zombieHealth = normal.getBaseHealth();
this.zombieSpeed = normal.getBaseSpeed();
this.spawnInterval = ZOMBIE_SPAWN_INTERVAL_BASE;
this.random = new Random();
this.explosions = new ArrayList<>();
this.removedBullets = new ArrayList<>();
@@ -127,10 +137,10 @@ public class GameWorld {
updateFlowField();
if (difficultyTimer >= Constants.DIFFICULTY_INCREASE_INTERVAL) {
difficultyTimer -= Constants.DIFFICULTY_INCREASE_INTERVAL;
if (difficultyTimer >= DIFFICULTY_INCREASE_INTERVAL) {
difficultyTimer -= DIFFICULTY_INCREASE_INTERVAL;
waveNumber++;
spawnInterval = Math.max(Constants.ZOMBIE_SPAWN_INTERVAL_MIN,
spawnInterval = Math.max(ZOMBIE_SPAWN_INTERVAL_MIN,
spawnInterval - 0.3f);
}
@@ -177,14 +187,19 @@ public class GameWorld {
float wx = sp[0] + 0.5f;
float wy = sp[1] + 0.5f;
ZombieTemplate elite = TemplateManager.getInstance().getZombieTemplate("elite");
ZombieTemplate splitter = TemplateManager.getInstance().getZombieTemplate("splitter");
float eliteChance = elite.getSpawnWeight();
float splitterChance = splitter.getSpawnWeight();
float roll = random.nextFloat();
Zombie zombie;
if (roll < Constants.ELITE_ZOMBIE_SPAWN_CHANCE) {
zombie = new Zombie(nextZombieId++, wx, wy, Constants.ELITE_ZOMBIE_HEALTH, Constants.ELITE_ZOMBIE_SPEED, true, false);
} else if (roll < Constants.ELITE_ZOMBIE_SPAWN_CHANCE + Constants.SPLITTER_ZOMBIE_SPAWN_CHANCE) {
zombie = new Zombie(nextZombieId++, wx, wy, Constants.SPLITTER_ZOMBIE_HEALTH, Constants.SPLITTER_ZOMBIE_SPEED, false, true);
if (roll < eliteChance) {
zombie = new Zombie(nextZombieId++, wx, wy, "elite");
} else if (roll < eliteChance + splitterChance) {
zombie = new Zombie(nextZombieId++, wx, wy, "splitter");
} else {
zombie = new Zombie(nextZombieId++, wx, wy, zombieHealth, zombieSpeed, false, false);
zombie = new Zombie(nextZombieId++, wx, wy, "normal");
}
zombies.put(zombie.getId(), zombie);
}
@@ -234,7 +249,7 @@ public class GameWorld {
Player nearest = findNearestPlayer(z.getX(), z.getY());
if (nearest != null) {
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);
z.rangedAttack(now);
}
@@ -243,7 +258,7 @@ public class GameWorld {
Wall attackedWall = z.move(map, dt, zombies.values(), now);
if (attackedWall != null && z.canAttack(now)) {
attackedWall.takeDamage(1.0f); // 每次攻击造成1点伤害
attackedWall.takeDamage(1.0f);
z.attack(now);
if (attackedWall.isDestroyed()) {
map.removeWall(attackedWall.getGridX(), attackedWall.getGridY());
@@ -267,8 +282,8 @@ public class GameWorld {
float startY = zombie.getY() + (float) Math.cos(angle) * 0.5f;
Bullet bullet = new Bullet(nextZombieBulletId++, startX, startY, angle,
Constants.ELITE_ZOMBIE_BULLET_SPEED, Constants.ELITE_ZOMBIE_BULLET_DAMAGE,
"zombie_" + zombie.getId(), "zombie_bullet", 15);
zombie.getTemplate().getRangedBulletSpeed(), zombie.getTemplate().getRangedDamage(),
"zombie_" + zombie.getId(), -1, 15);
zombieBullets.put(bullet.getId(), bullet);
}
@@ -301,7 +316,7 @@ public class GameWorld {
// 检测是否命中玩家
for (Player p : new ArrayList<>(players.values())) {
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());
hit = true;
break;
@@ -343,22 +358,21 @@ public class GameWorld {
*/
private void onZombieKilled(Zombie z) {
if (z.isSplitter()) {
int splitCount = Constants.SPLITTER_ZOMBIE_MIN_SPLIT +
random.nextInt(Constants.SPLITTER_ZOMBIE_MAX_SPLIT - Constants.SPLITTER_ZOMBIE_MIN_SPLIT + 1);
int splitCount = z.getTemplate().getMinSplit() +
random.nextInt(z.getTemplate().getMaxSplit() - z.getTemplate().getMinSplit() + 1);
for (int i = 0; i < splitCount; i++) {
float offsetX = (random.nextFloat() - 0.5f) * 1.0f;
float offsetY = (random.nextFloat() - 0.5f) * 1.0f;
Zombie splitZombie = new Zombie(nextZombieId++,
z.getX() + offsetX, z.getY() + offsetY,
zombieHealth, zombieSpeed, false, false);
z.getX() + offsetX, z.getY() + offsetY, "normal");
zombies.put(splitZombie.getId(), splitZombie);
}
score += 20;
} else {
score += z.isElite() ? 50 : 10;
}
if (random.nextFloat() < Constants.ZOMBIE_LOOT_DROP_CHANCE) {
String lootType = random.nextFloat() < 0.5f ? Constants.LOOT_TYPE_AMMO : Constants.LOOT_TYPE_HEALTH;
if (random.nextFloat() < ZOMBIE_LOOT_DROP_CHANCE) {
String lootType = random.nextFloat() < 0.5f ? LOOT_TYPE_AMMO : LOOT_TYPE_HEALTH;
Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType);
loots.put(loot.getId(), loot);
}
@@ -397,7 +411,7 @@ public class GameWorld {
boolean hit = false;
for (Zombie z : new ArrayList<>(zombies.values())) {
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());
hit = true;
break;
@@ -464,7 +478,7 @@ public class GameWorld {
if (!p.isAlive()) continue;
float dist = z.distanceTo(p.getX(), p.getY());
if (dist < 1.0f && z.canAttack(now)) {
p.takeDamage(Constants.ZOMBIE_DAMAGE);
p.takeDamage(z.getTemplate().getDamage());
z.attack(now);
}
}
@@ -480,8 +494,8 @@ public class GameWorld {
for (Player p : players.values()) {
if (!p.isAlive()) continue;
if (loot.isCollectedBy(p.getX(), p.getY())) {
if (loot.getType().equals(Constants.LOOT_TYPE_HEALTH)) {
p.heal(Constants.LOOT_HEALTH_AMOUNT);
if (loot.getType().equals(LOOT_TYPE_HEALTH)) {
p.heal(LOOT_HEALTH_AMOUNT);
} else {
p.refillRandomWeapon();
}
@@ -509,7 +523,7 @@ public class GameWorld {
for (Player p : players.values()) {
if (p.isWaitingForRespawn()) {
p.updateRespawnTimer(Constants.TICK_INTERVAL);
p.updateRespawnTimer(TICK_INTERVAL);
if (hasAlivePlayer && p.canRespawn()) {
List<int[]> spawnPoints = map.getSpawnPoints();
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
@@ -551,15 +565,7 @@ public class GameWorld {
player.setAngle(aimX, aimY);
player.fire(now);
String weapon;
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)) {
if (player.isChargeable()) {
float startX = player.getX();
float startY = player.getY();
@@ -581,8 +587,8 @@ public class GameWorld {
targetY = startY + dy * scale;
}
targetX = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, targetX));
targetY = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, targetY));
targetX = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetX));
targetY = Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, targetY));
float flightDuration = 0.8f + chargePercent * 0.7f;
@@ -593,6 +599,7 @@ public class GameWorld {
} else {
int pellets = player.getPelletCount();
float spread = player.getSpread();
float range = player.getWeaponIndex() == 1 ? 25 : (player.getWeaponIndex() == 2 ? 12 : 30);
for (int i = 0; i < pellets; i++) {
float angle = player.getAngle();
@@ -605,15 +612,9 @@ public class GameWorld {
float speed = player.getBulletSpeed();
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,
speed, damage, player.getId(), weapon, range);
speed, damage, player.getId(), player.getWeaponIndex(), range);
bullets.put(bullet.getId(), bullet);
newBulletIds.add(bullet.getId());
}
@@ -678,10 +679,12 @@ public class GameWorld {
Player forPlayer = players.get(forPlayerId);
if (forPlayer != null) {
Map<String, Object> ammoMap = new LinkedHashMap<>();
ammoMap.put(Constants.WEAPON_PISTOL, forPlayer.getAmmo()[0] == Integer.MAX_VALUE ? -1 : forPlayer.getAmmo()[0]);
ammoMap.put(Constants.WEAPON_MACHINE_GUN, (int) forPlayer.getAmmo()[1]);
ammoMap.put(Constants.WEAPON_SHOTGUN, (int) forPlayer.getAmmo()[2]);
ammoMap.put(Constants.WEAPON_GRENADE, (int) forPlayer.getAmmo()[3]);
int weaponCount = TemplateManager.getInstance().getAllWeaponTemplates().size();
for (int i = 0; i < weaponCount; i++) {
String weaponId = TemplateManager.getInstance().getWeaponId(i);
float ammo = forPlayer.getAmmo()[i];
ammoMap.put(weaponId, ammo == Integer.MAX_VALUE ? -1 : (int) ammo);
}
state.put("ammo", ammoMap);
}

View File

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

View File

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