灾难系统
概述
灾难系统是灾难玩法的核心,负责生成各种灾难事件来挑战玩家。系统采用工厂模式,通过灾难管理器创建和管理灾难实例,支持 30+ 种不同类型的灾难。
核心类
Disaster(灾难基类)
路径:cn.enderrealm.disaster.game.disasters.Disaster
所有灾难类型的抽象基类,定义了灾难的基本接口和通用功能。
java
public abstract class Disaster {
protected final disaster plugin; // 插件实例
protected final Room room; // 房间实例
protected final World world; // 游戏世界
protected final Set<Player> players; // 房间内玩家
protected final String disasterId; // 灾难ID
protected Location point1; // 地图范围点1
protected Location point2; // 地图范围点2
private String disasterDisplayValue; // 灾难显示值
private boolean disasterExpired; // 灾难是否过期
}抽象方法
java
// 启动灾难效果
public abstract void start();
// 停止灾难效果
public abstract void stop();
// 获取灾难描述
public String getDescription(Player player) {
return plugin.getLanguageManager().getText("disaster-" + disasterId + "-desc", player);
}
// 获取互斥灾难ID集合(默认为空)
public Set<String> getMutuallyExclusiveDisasterIds() {
return Collections.emptySet();
}通用工具方法
java
// 从地图配置加载范围
protected void loadScope() {
// 尝试从 worlds 目录加载
File worldsFolder = new File(plugin.getDataFolder(), "worlds" + File.separator + mapName);
// ...
// 如果失败,使用默认范围(世界出生点 ±50 格)
}
// 获取地图范围内的随机位置
protected Location getRandomLocation() {
double minX = Math.min(point1.getX(), point2.getX());
double maxX = Math.max(point1.getX(), point2.getX());
// ...
return new Location(world, x, y, z);
}
// 获取存活玩家列表
protected List<Player> getSurvivingPlayers() {
List<Player> survivors = new ArrayList<>();
for (Player player : players) {
if (!player.hasMetadata("eliminated")) {
survivors.add(player);
}
}
return survivors;
}
// 获取当前游戏剩余时间
protected int getRemainingGameTimeSeconds() {
GameManager gameManager = plugin.getRoomManager().getGameManager(room);
return gameManager != null ? gameManager.getRemainingGameTimeSeconds() : Integer.MAX_VALUE;
}DisasterManager(灾难管理器)
路径:cn.enderrealm.disaster.game.disasters.DisasterManager
灾难管理器负责创建、管理和销毁灾难实例。
java
public class DisasterManager {
private final disaster plugin;
// 房间ID -> (灾难ID -> 灾难实例)
private final Map<String, Map<String, Disaster>> roomDisasterInstances;
// 房间ID -> 灾难ID列表
private final Map<String, List<String>> roomDisasterMap;
}核心方法
java
// 创建灾难实例
public Disaster createDisaster(String disasterId, Room room) {
String roomId = room.getWorld().getName();
// 检查是否已有同类型灾难
if (hasDisasterInRoom(roomId, disasterId)) {
Disaster oldDisaster = getDisasterInstance(roomId, disasterId);
if (oldDisaster != null) {
oldDisaster.stop();
// 移除旧灾难
}
}
// 根据灾难ID创建实例
Disaster disaster;
switch (disasterId) {
case "zombie_apocalypse":
disaster = new ZombieApocalypseDisaster(plugin, room, disasterId);
break;
case "meteor_shower":
disaster = new MeteorShowerDisaster(plugin, room, disasterId);
break;
// ... 其他灾难类型
default:
return null;
}
// 注册灾难实例
roomDisasterInstances.computeIfAbsent(roomId, k -> new HashMap<>()).put(disasterId, disaster);
roomDisasterMap.computeIfAbsent(roomId, k -> new ArrayList<>()).add(disasterId);
// 向玩家展示灾难描述
for (Player player : room.getPlayers()) {
player.sendMessage(plugin.getLanguageManager().getText("disaster-" + disasterId, player) +
" - " + disaster.getDescription(player));
}
// 启动灾难效果
disaster.start();
return disaster;
}
// 停止指定灾难
public void stopDisaster(String disasterId) {
for (String roomId : new ArrayList<>(roomDisasterInstances.keySet())) {
Map<String, Disaster> disasterMap = roomDisasterInstances.get(roomId);
if (disasterMap.containsKey(disasterId)) {
Disaster disaster = disasterMap.get(disasterId);
disaster.stop();
disasterMap.remove(disasterId);
// ...
}
}
}灾难类型
主要灾难(Main Disasters)
主要灾难是游戏中的主要威胁,每次灾难触发时 100% 会选择一个主要灾难。
| 灾难ID | 中文名称 | 描述 |
|---|---|---|
zombie_apocalypse | 僵尸启示录 | 僵尸群正在入侵!小心它们的围攻和破坏! |
meteor_shower | 流星雨 | 天空中的火球正在坠落!注意躲避! |
lightning | 闪电 | 雷电即将劈下!远离黄色粒子区域! |
flood | 洪水 | 洪水正在上涨!尽快前往高处! |
dragon | 龙灾 | 末影龙入侵!它们会从空中攻击你! |
tnt_bomber | TNT轰炸机 | 敌机正在投弹!远离爆炸范围! |
anvil_rain | 铁砧雨 | 铁砧正在坠落!被砸中即死! |
wither_storm | 凋零风暴 | 凋零出现!它会追杀玩家,被它命中将获得凋零腐败效果! |
death_cube | 死亡立方 | 死亡立方正在形成!远离灰色玻璃立方体,它们会突然破碎! |
blizzard | 暴风雪 | 暴风雪来袭!保持温暖以避免冻伤!靠近其他玩家或光源可以提高寒冷值! |
two_dimensional_foil | 二向箔 | 维度坍缩进行中... 三维空间正在向二维平面压缩。坍缩完成后,所有空间将被压缩为虚无... |
slime_king | 史莱姆之王 | 史莱姆之王已出现!躲避他危险的行为! |
tornado | 龙卷风 | 龙卷风已形成!它会移动并破坏所经过的方块!远离它的路径! |
nether_invasion | 下界入侵 | 下界入侵开始!下界生物正在入侵这个世界! |
alien_abduction | 外星人 | 外星人正在绑架玩家!随机玩家将被抓走。 |
raid_omen | 袭击之兆 | 掠夺者正在入侵!敌人会源源不断地出现! |
sinkhole | 地陷 | 地面正在塌陷!远离塌陷区域! |
migration | 动物大迁徙 | 兽群正在迁徙!避开它们的行进路线! |
次要灾难(Minor Disasters)
次要灾难是辅助性灾难,每次主要灾难触发后有 30% 概率追加一个次要灾难。
| 灾难ID | 中文名称 | 描述 |
|---|---|---|
acid_rain | 酸雨 | 有毒的雨水正在落下!寻找掩体躲避!雨水会腐蚀木质建筑! |
hot_potato | 烫手山芋 | 不要成为最后持有烫手山芋的人! |
werewolf | 狼人 | 有人变成了狼人!它可以攻击其他玩家! |
purge | 大清洗 | 所有规则已解除!玩家可以互相攻击! |
vampire | 吸血鬼 | 玩家们变成了吸血鬼!攻击其他玩家将回复自己的生命值,并有几率使对方流血! |
block_collapse | 方块塌陷 | 脚下的方块变得不稳定!你脚下的方块将会随时坍塌! |
nuclear_bomb | 核弹 | 核弹倒计时已开始!在爆炸前找到掩体!爆炸将造成大范围的破坏和辐射! |
traffic_light | 红绿灯 | 红灯停,绿灯行——违反交通规则是要付出代价的。 |
disco | 舞!舞!舞! | 音乐响起,舞池已就绪——停下来的人会付出代价。 |
floor_is_lava | 地板是岩浆 | 地板正在变成岩浆!玩家脚下方块会变成岩浆。 |
hacker | 黑客 | 一名黑客正在入侵!他开着暴力外挂来虐杀玩家! |
herobrine_says | 你说我做 | Herobrine正在发号施令。按令行事无赏,违令必受惩罚。 |
blackout | 停电 | 眼前一黑,什么都看不见了!保持冷静,等待灯光恢复! |
shackled | 脚镣 | 无形锁链缠住双脚,一分钟内禁止跳跃。 |
灾难实现示例
僵尸启示录(ZombieApocalypseDisaster)
java
public class ZombieApocalypseDisaster extends Disaster implements Listener {
private static final String ZOMBIE_APOCALYPSE_ROOM_METADATA_KEY = "zombie_apocalypse_room_id";
private final Random random;
private BukkitTask zombieSpawnTask;
@Override
public void start() {
// 立即生成第一轮僵尸
spawnZombieWave();
// 每 30 秒生成一轮僵尸
zombieSpawnTask = new BukkitRunnable() {
@Override
public void run() {
spawnZombieWave();
}
}.runTaskTimer(plugin, 30 * 20L, 30 * 20L);
}
@Override
public void stop() {
if (zombieSpawnTask != null) {
zombieSpawnTask.cancel();
zombieSpawnTask = null;
}
HandlerList.unregisterAll(this);
}
private void spawnZombieWave() {
List<Player> survivors = getSurvivingPlayers();
if (survivors.isEmpty()) return;
// 为每个存活玩家生成 1-2 只僵尸
for (Player player : survivors) {
int zombieCount = 1 + random.nextInt(2);
for (int i = 0; i < zombieCount; i++) {
spawnZombieNearPlayer(player);
}
}
}
private void spawnZombieNearPlayer(Player player) {
Location playerLoc = player.getLocation();
// 在玩家周围 10 格范围内随机位置
double offsetX = (random.nextDouble() * 20 - 10);
double offsetZ = (random.nextDouble() * 20 - 10);
Location spawnLoc = playerLoc.clone().add(offsetX, 0, offsetZ);
int y = world.getHighestBlockYAt(spawnLoc.getBlockX(), spawnLoc.getBlockZ());
Location spawnLocation = new Location(world, spawnLoc.getX(), y + 1, spawnLoc.getZ());
// 生成僵尸
Zombie zombie = (Zombie) world.spawnEntity(spawnLocation, EntityType.ZOMBIE);
zombie.setMetadata(ZOMBIE_APOCALYPSE_ROOM_METADATA_KEY,
new FixedMetadataValue(plugin, room.getWorld().getName()));
// 设置僵尸属性
zombie.setFireTicks(0); // 防火
// 10% 概率生成小僵尸
if (random.nextDouble() < 0.1) {
zombie.setBaby(true);
}
// 40% 概率穿戴装备
if (random.nextDouble() < 0.4) {
equipZombie(zombie);
}
}
// 僵尸死亡事件处理
@EventHandler
public void onZombieDeath(EntityDeathEvent event) {
if (!(event.getEntity() instanceof Zombie)) return;
Zombie zombie = (Zombie) event.getEntity();
if (!isZombieFromThisDisaster(zombie)) return;
Player killer = zombie.getKiller();
if (killer == null || !room.getPlayers().contains(killer) || killer.hasMetadata("eliminated")) {
return;
}
// 40% 概率掉落神秘药水
if (random.nextDouble() < 0.4) {
event.getDrops().add(MysteriousPotion.create());
}
}
}流星雨(MeteorShowerDisaster)
java
public class MeteorShowerDisaster extends Disaster {
private BukkitTask meteorTask;
private final Random random;
@Override
public void start() {
meteorTask = new BukkitRunnable() {
@Override
public void run() {
spawnMeteors();
}
}.runTaskTimer(plugin, 0L, 10 * 20L); // 每 10 秒一次
}
private void spawnMeteors() {
List<Player> survivors = getSurvivingPlayers();
if (survivors.isEmpty()) return;
// 随机选择 2-3 名玩家
int playerCount = 2 + random.nextInt(2);
playerCount = Math.min(playerCount, survivors.size());
List<Player> selectedPlayers = new ArrayList<>(survivors);
Collections.shuffle(selectedPlayers);
for (int i = 0; i < playerCount; i++) {
spawnMeteorAtPlayer(selectedPlayers.get(i));
}
}
private void spawnMeteorAtPlayer(Player player) {
Location playerLoc = player.getLocation();
// 10% 概率直接命中,否则偏移
Location targetLoc;
if (random.nextDouble() < 0.1) {
targetLoc = playerLoc.clone();
} else {
double offsetX = (random.nextDouble() * 20 - 10);
double offsetZ = (random.nextDouble() * 20 - 10);
targetLoc = playerLoc.clone().add(offsetX, 0, offsetZ);
}
// 在玩家上方 30-40 格创建火球
Location meteorStart = targetLoc.clone();
meteorStart.setY(playerLoc.getY() + 30 + random.nextInt(10));
Fireball fireball = (Fireball) world.spawnEntity(meteorStart, EntityType.FIREBALL);
fireball.setYield(2.0f); // 爆炸威力
fireball.setIsIncendiary(true); // 点燃方块
// 设置飞行方向
Vector direction = new Vector(
(random.nextDouble() - 0.5) * 0.3,
-1,
(random.nextDouble() - 0.5) * 0.3
).normalize();
fireball.setDirection(direction);
fireball.setVelocity(direction.multiply(0.5));
// 添加火焰粒子效果
// ...
// 监听爆炸,创建重力方块效果
// ...
}
}灾难选择机制
选择流程
java
// GameManager 中的灾难选择方法
private void selectDisaster() {
Room currentRoom = findCurrentRoom();
if (currentRoom == null) return;
// 1. 从主灾难池中选择
Disaster selectedMainDisaster = trySelectAndStartDisaster(availableMainDisasters, currentRoom);
// 2. 30% 概率从次灾难池中选择
if (selectedMainDisaster != null && random.nextDouble() < 0.3) {
trySelectAndStartDisaster(availableMinorDisasters, currentRoom);
}
}
private Disaster trySelectAndStartDisaster(List<String> disasterPool, Room room) {
if (disasterPool.isEmpty()) return null;
// 随机选择一个灾难ID
String selectedDisasterId = disasterPool.remove(random.nextInt(disasterPool.size()));
// 创建灾难实例
Disaster disaster = disasterManager.createDisaster(selectedDisasterId, room);
if (disaster == null) return null;
// 记录激活的灾难
activeDisastersIds.add(selectedDisasterId);
activeDisasters.add(disaster);
// 应用互斥灾难池移除
applyMutuallyExclusiveDisasterPoolRemoval(disaster, selectedDisasterId);
return disaster;
}互斥机制
某些灾难之间存在互斥关系,不能同时存在:
java
private void applyMutuallyExclusiveDisasterPoolRemoval(Disaster disaster, String selectedDisasterId) {
// 移除自身
removeDisasterIdFromAvailablePools(selectedDisasterId);
// 移除互斥灾难
for (String mutuallyExclusiveDisasterId : disaster.getMutuallyExclusiveDisasterIds()) {
removeDisasterIdFromAvailablePools(mutuallyExclusiveDisasterId);
}
}基岩玩家限制
当房间内存在基岩版玩家时,会自动移除某些灾难:
java
private void applyBedrockPlayerDisasterRestriction() {
for (Player player : players) {
if (Easy4FormAPI.isBedrockPlayer(player)) {
removeDisasterIdFromAvailablePools("tornado"); // 移除龙卷风
return;
}
}
}配置示例
yaml
# 灾难配置
disasterCountdownTime: 12 # 灾难爆发倒计时(秒)
disasterInterval: 60 # 灾难间隔时间(秒)
# 主要灾难列表
mainDisasters:
- zombie_apocalypse # 僵尸启示录
- meteor_shower # 流星雨
- lightning # 闪电
- flood # 洪水
- dragon # 龙灾
- tnt_bomber # TNT轰炸机
- anvil_rain # 铁砧雨
- wither_storm # 凋零风暴
- death_cube # 死亡立方
- blizzard # 暴风雪
- two_dimensional_foil # 二向箔
- slime_king # 史莱姆之王
- tornado # 龙卷风
- nether_invasion # 下界入侵
- alien_abduction # 外星人
- raid_omen # 袭击之兆
- sinkhole # 地陷
- migration # 动物大迁徙
# 次要灾难列表
minorDisasters:
- acid_rain # 酸雨
- hot_potato # 烫手山芋
- werewolf # 狼人
- purge # 大清洗
- vampire # 吸血鬼
- block_collapse # 方块塌陷
- nuclear_bomb # 核弹
- traffic_light # 红绿灯
- disco # 舞!舞!舞!
- floor_is_lava # 地板是岩浆
- hacker # 黑客
- herobrine_says # 你说我做
- blackout # 停电
- shackled # 脚镣