diff --git a/backend/src/main/java/com/zombie/game/model/Bullet.java b/backend/src/main/java/com/zombie/game/model/Bullet.java index e18a419..1d6ff26 100644 --- a/backend/src/main/java/com/zombie/game/model/Bullet.java +++ b/backend/src/main/java/com/zombie/game/model/Bullet.java @@ -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 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; } diff --git a/backend/src/main/java/com/zombie/game/model/Constants.java b/backend/src/main/java/com/zombie/game/model/Constants.java index f0fa99e..e803d8f 100644 --- a/backend/src/main/java/com/zombie/game/model/Constants.java +++ b/backend/src/main/java/com/zombie/game/model/Constants.java @@ -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"; diff --git a/backend/src/main/java/com/zombie/game/model/GameWorld.java b/backend/src/main/java/com/zombie/game/model/GameWorld.java index 19ca980..4c87db1 100644 --- a/backend/src/main/java/com/zombie/game/model/GameWorld.java +++ b/backend/src/main/java/com/zombie/game/model/GameWorld.java @@ -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 removedBullets; private List 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 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 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); } diff --git a/backend/src/main/java/com/zombie/game/model/Player.java b/backend/src/main/java/com/zombie/game/model/Player.java index 0c1a89d..34ef2a2 100644 --- a/backend/src/main/java/com/zombie/game/model/Player.java +++ b/backend/src/main/java/com/zombie/game/model/Player.java @@ -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 WEAPON_TEMPLATES; + static { + List 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 toStateMap() { Map map = new LinkedHashMap<>(); map.put("id", id); diff --git a/backend/src/main/java/com/zombie/game/model/Zombie.java b/backend/src/main/java/com/zombie/game/model/Zombie.java index 7fff99a..7790e97 100644 --- a/backend/src/main/java/com/zombie/game/model/Zombie.java +++ b/backend/src/main/java/com/zombie/game/model/Zombie.java @@ -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 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 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 otherZombies) { int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; - + java.util.List 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 toStateMap() { Map 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; } } diff --git a/backend/src/main/java/com/zombie/game/template/PlayerTemplate.java b/backend/src/main/java/com/zombie/game/template/PlayerTemplate.java new file mode 100644 index 0000000..560a284 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/template/PlayerTemplate.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/template/TemplateManager.java b/backend/src/main/java/com/zombie/game/template/TemplateManager.java new file mode 100644 index 0000000..55b1a9a --- /dev/null +++ b/backend/src/main/java/com/zombie/game/template/TemplateManager.java @@ -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 zombieTemplates = new LinkedHashMap<>(); + private final Map 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 getAllZombieTemplates() { + return zombieTemplates.values(); + } + + public WeaponTemplate getWeaponTemplate(String id) { + return weaponTemplates.get(id); + } + + public Collection 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(); + } +} diff --git a/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java b/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java new file mode 100644 index 0000000..9a9ccb6 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/template/WeaponTemplate.java @@ -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; + } +} diff --git a/backend/src/main/java/com/zombie/game/template/ZombieTemplate.java b/backend/src/main/java/com/zombie/game/template/ZombieTemplate.java new file mode 100644 index 0000000..f4d4386 --- /dev/null +++ b/backend/src/main/java/com/zombie/game/template/ZombieTemplate.java @@ -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; + } +} diff --git a/backend/src/main/resources/data/players.json b/backend/src/main/resources/data/players.json new file mode 100644 index 0000000..d9b08ba --- /dev/null +++ b/backend/src/main/resources/data/players.json @@ -0,0 +1,9 @@ +{ + "base": { + "speed": 5.0, + "maxHealth": 100, + "invulnerableTime": 0.5, + "respawnTime": 30.0, + "size": 0.8 + } +} diff --git a/backend/src/main/resources/data/weapons.json b/backend/src/main/resources/data/weapons.json new file mode 100644 index 0000000..e7241bf --- /dev/null +++ b/backend/src/main/resources/data/weapons.json @@ -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 + } + ] +} diff --git a/backend/src/main/resources/data/zombies.json b/backend/src/main/resources/data/zombies.json new file mode 100644 index 0000000..86521c4 --- /dev/null +++ b/backend/src/main/resources/data/zombies.json @@ -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 + } + ] +}