1
This commit is contained in:
@@ -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,21 +42,21 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数 - 手榴弹
|
||||
*
|
||||
*
|
||||
* @param id 子弹ID
|
||||
* @param startX 起始X坐标
|
||||
* @param startY 起始Y坐标
|
||||
@@ -67,7 +67,7 @@ public class Bullet {
|
||||
* @param ownerId 发射者ID
|
||||
* @param explosionRadius 爆炸半径
|
||||
*/
|
||||
public Bullet(int id, float startX, float startY, float targetX, float targetY,
|
||||
public Bullet(int id, float startX, float startY, float targetX, float targetY,
|
||||
float flightDuration, int damage, String ownerId, float explosionRadius) {
|
||||
this.id = id;
|
||||
this.x = startX;
|
||||
@@ -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;
|
||||
@@ -97,21 +97,21 @@ public class Bullet {
|
||||
public boolean update(float dt, GameMap map) {
|
||||
if (isGrenade) {
|
||||
flightTime += dt;
|
||||
|
||||
|
||||
x += vx * dt;
|
||||
y += vy * dt;
|
||||
|
||||
|
||||
float progress = flightTime / maxFlightTime;
|
||||
z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
|
||||
|
||||
|
||||
if (flightTime >= maxFlightTime || z <= 0.5f && progress > 0.5f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= Constants.GRID_SIZE || y < 0 || y >= Constants.GRID_SIZE) {
|
||||
|
||||
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
} else {
|
||||
float moveX = vx * dt;
|
||||
@@ -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);
|
||||
@@ -135,7 +135,7 @@ public class Bullet {
|
||||
|
||||
/**
|
||||
* 检测子弹是否命中实体
|
||||
*
|
||||
*
|
||||
* @param ex 实体X坐标
|
||||
* @param ey 实体Y坐标
|
||||
* @param size 实体碰撞体大小
|
||||
@@ -150,7 +150,7 @@ public class Bullet {
|
||||
|
||||
/**
|
||||
* 将子弹状态转换为Map格式,用于网络传输
|
||||
*
|
||||
*
|
||||
* @return 包含子弹状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap() {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<>();
|
||||
@@ -86,7 +96,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 从游戏世界移除玩家
|
||||
*
|
||||
*
|
||||
* @param playerId 玩家ID
|
||||
*/
|
||||
public void removePlayer(String playerId) {
|
||||
@@ -106,14 +116,14 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 更新游戏世界状态
|
||||
*
|
||||
*
|
||||
* 每帧调用,处理:
|
||||
* - 时间流逝和难度提升
|
||||
* - 僵尸生成
|
||||
* - 实体移动和碰撞
|
||||
* - 掉落物收集
|
||||
* - 玩家重生
|
||||
*
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
public void update(float dt) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -151,7 +161,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 更新流场导航
|
||||
*
|
||||
*
|
||||
* 基于存活玩家的位置更新流场
|
||||
*/
|
||||
private void updateFlowField() {
|
||||
@@ -168,7 +178,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 生成僵尸
|
||||
*
|
||||
*
|
||||
* 根据概率生成普通僵尸、精英僵尸或分裂僵尸
|
||||
*/
|
||||
private void spawnZombie() {
|
||||
@@ -177,21 +187,26 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找最近的存活玩家
|
||||
*
|
||||
*
|
||||
* @param x X坐标
|
||||
* @param y Y坐标
|
||||
* @return 最近的玩家,如果没有存活玩家则返回 null
|
||||
@@ -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());
|
||||
@@ -254,7 +269,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 精英僵尸发射子弹
|
||||
*
|
||||
*
|
||||
* @param zombie 发射子弹的僵尸
|
||||
* @param target 目标玩家
|
||||
*/
|
||||
@@ -267,14 +282,14 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有僵尸子弹
|
||||
*
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateZombieBullets(float dt) {
|
||||
@@ -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;
|
||||
@@ -334,31 +349,30 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 处理僵尸死亡
|
||||
*
|
||||
*
|
||||
* - 分裂僵尸分裂成多个小僵尸
|
||||
* - 增加分数
|
||||
* - 可能掉落物品
|
||||
*
|
||||
*
|
||||
* @param z 被击杀的僵尸
|
||||
*/
|
||||
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);
|
||||
Zombie splitZombie = new Zombie(nextZombieId++,
|
||||
z.getX() + offsetX, z.getY() + offsetY, "normal");
|
||||
zombies.put(splitZombie.getId(), splitZombie);
|
||||
}
|
||||
score += 20;
|
||||
} else {
|
||||
score += z.isElite() ? 50 : 10;
|
||||
}
|
||||
if (random.nextFloat() < 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);
|
||||
}
|
||||
@@ -366,7 +380,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 更新所有玩家子弹
|
||||
*
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateBullets(float dt) {
|
||||
@@ -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;
|
||||
@@ -430,9 +444,9 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 创建爆炸效果
|
||||
*
|
||||
*
|
||||
* 对范围内的僵尸造成伤害
|
||||
*
|
||||
*
|
||||
* @param x 爆炸中心X坐标
|
||||
* @param y 爆炸中心Y坐标
|
||||
* @param radius 爆炸半径
|
||||
@@ -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()));
|
||||
@@ -523,7 +537,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 玩家开火(无蓄力)
|
||||
*
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
@@ -535,7 +549,7 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 玩家开火(支持蓄力)
|
||||
*
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
@@ -551,26 +565,18 @@ 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();
|
||||
|
||||
|
||||
float minDist = 3.0f;
|
||||
float maxDist = 15.0f;
|
||||
float dist = minDist + (maxDist - minDist) * chargePercent;
|
||||
|
||||
|
||||
float dx = aimX - startX;
|
||||
float dy = aimY - startY;
|
||||
float targetDist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
|
||||
float targetX, targetY;
|
||||
if (targetDist < 0.1f) {
|
||||
targetX = startX + minDist;
|
||||
@@ -580,12 +586,12 @@ public class GameWorld {
|
||||
targetX = startX + dx * scale;
|
||||
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;
|
||||
|
||||
|
||||
Bullet bullet = new Bullet(nextBulletId++, startX, startY, targetX, targetY,
|
||||
flightDuration, player.getDamage(), player.getId(), 3.0f);
|
||||
bullets.put(bullet.getId(), bullet);
|
||||
@@ -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());
|
||||
}
|
||||
@@ -629,9 +630,9 @@ public class GameWorld {
|
||||
|
||||
/**
|
||||
* 构建游戏状态数据
|
||||
*
|
||||
*
|
||||
* 将当前游戏世界的所有状态打包成Map格式,用于网络传输
|
||||
*
|
||||
*
|
||||
* @param forPlayerId 目标玩家ID(用于发送该玩家的弹药信息)
|
||||
* @return 游戏状态Map
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -310,123 +282,81 @@ 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;
|
||||
if (!other.isAlive()) continue;
|
||||
|
||||
|
||||
int otherGridX = (int) Math.floor(other.getX());
|
||||
int otherGridY = (int) Math.floor(other.getY());
|
||||
|
||||
|
||||
if (otherGridX == gridX && otherGridY == gridY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (other.isReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找替代移动方向
|
||||
*
|
||||
* 当目标格子被占用时,寻找其他可行方向
|
||||
*/
|
||||
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
|
||||
|
||||
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
|
||||
GameMap map, Collection<Zombie> otherZombies) {
|
||||
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
|
||||
|
||||
java.util.List<int[]> candidates = new java.util.ArrayList<>();
|
||||
|
||||
|
||||
for (int[] dir : allDirs) {
|
||||
int nx = currentGridX + dir[0];
|
||||
int ny = currentGridY + dir[1];
|
||||
|
||||
|
||||
if (map.isWall(nx, ny)) continue;
|
||||
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (map.isWall(currentGridX + dir[0], currentGridY) ||
|
||||
if (map.isWall(currentGridX + dir[0], currentGridY) ||
|
||||
map.isWall(currentGridX, currentGridY + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue;
|
||||
|
||||
|
||||
float dotProduct = dir[0] * dirX + dir[1] * dirY;
|
||||
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
|
||||
}
|
||||
|
||||
|
||||
if (candidates.isEmpty()) return null;
|
||||
|
||||
|
||||
candidates.sort((a, b) -> b[2] - a[2]);
|
||||
|
||||
|
||||
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以近战攻击
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user