diff --git a/pom.xml b/pom.xml
index 73d2c98..bd220ef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
nu.nerd
DragonFight
- 1.2.0
+ 1.3.0
Custom dragon fight.
https://github.com/NerdNu/${project.name}
@@ -17,8 +17,17 @@
UTF-8
- 1.8
- 1.8
+
+ 17
+ 17
+ 17
+ true
@@ -32,12 +41,21 @@
org.spigotmc
spigot-api
- 1.15.2-R0.1-SNAPSHOT
+ 1.18.1-R0.1-SNAPSHOT
nu.nerd
BeastMaster
- 2.14.0
+ 2.17.0
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.20
@@ -49,5 +67,13 @@
true
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.9.0
+
+
+
diff --git a/src/main/java/nu/nerd/df/Configuration.java b/src/main/java/nu/nerd/df/Configuration.java
index 3420e3d..6974417 100644
--- a/src/main/java/nu/nerd/df/Configuration.java
+++ b/src/main/java/nu/nerd/df/Configuration.java
@@ -24,13 +24,38 @@ public class Configuration {
*/
public String DEBUG_PREFIX;
+ /**
+ * Current stage number: 0 to 11.
+ *
+ * Stage 0 is before the fight, Stage 1 => first crystal removed and boss
+ * spawned. Stage 10 => final boss spawned. Stage 11: dragon. In Stage N, N
+ * crystals have been removed.
+ */
+ public int STAGE_NUMBER;
+
+ /**
+ * The new value of STAGE_NUMBER, when there is a transition between stages.
+ *
+ * Spawn animations for the dragon and for stage bosses take time. To handle
+ * server restarts during these spawn sequences, we need to record the fact
+ * that it is underway and at least the new stage number. But in this
+ * plugin, we record both as that makes backwards stage transitions feasible
+ * (although not currently implemented).
+ *
+ * When NEW_STAGE_NUMBER != STAGE_NUMBER, the fight is part-way through
+ * transitioning from STAGE_NUMBER to NEW_STAGE_NUMBER. The STAGE_NUMBER
+ * will be set to NEW_STAGE_NUMBER when the transition is complete, in a few
+ * seconds.
+ */
+ public int NEW_STAGE_NUMBER;
+
/**
* Total boss maximum health.
*
* This needs to be preserved across restarts, since a stage fight may have
- * a random number of bosses, some of which may have died. THere is no easy
- * way to work infer the total maximum boss health points in the stage after
- * a restart.
+ * a random number of bosses, some of which may have died. There is no easy
+ * way to infer the total maximum boss health points in the stage after a
+ * restart.
*/
public double TOTAL_BOSS_MAX_HEALTH;
@@ -102,15 +127,18 @@ public Stage getStage(int stageNumber) {
// ------------------------------------------------------------------------
/**
- * Reload the configuration.
+ * Reload the fight state.
+ *
+ * The fight state changes as the plugin runs, so only call this method when
+ * loading the plugin.
*/
- public void reload() {
+ public void reloadFightState() {
DragonFight.PLUGIN.reloadConfig();
FileConfiguration config = DragonFight.PLUGIN.getConfig();
Logger logger = DragonFight.PLUGIN.getLogger();
- LOG_PREFIX = config.getString("settings.log-prefix");
- DEBUG_PREFIX = config.getString("settings.debug-prefix");
+ STAGE_NUMBER = config.getInt("state.stage-number", 0);
+ NEW_STAGE_NUMBER = config.getInt("state.new-stage-number", 0);
TOTAL_BOSS_MAX_HEALTH = config.getDouble("state.total-boss-max-health");
try {
@@ -131,6 +159,22 @@ public void reload() {
logger.warning("Unclaimed dragon prize registered to invalid UUID: " + key);
}
}
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Reload the configuration.
+ *
+ * This method is the counterpart to {@link #reloadFightState}. This method
+ * only loads the settings of the fight that are not modified as the fight
+ * progresses.
+ */
+ public void reloadConfiguration() {
+ DragonFight.PLUGIN.reloadConfig();
+ FileConfiguration config = DragonFight.PLUGIN.getConfig();
+
+ LOG_PREFIX = config.getString("settings.log-prefix");
+ DEBUG_PREFIX = config.getString("settings.debug-prefix");
for (int stageNumber = 1; stageNumber <= 11; ++stageNumber) {
getStage(stageNumber).load(getStageSection(stageNumber));
@@ -147,6 +191,8 @@ public void save() {
config.set("settings.log-prefix", LOG_PREFIX);
config.set("settings.debug-prefix", DEBUG_PREFIX);
+ config.set("state.stage-number", STAGE_NUMBER);
+ config.set("state.new-stage-number", NEW_STAGE_NUMBER);
config.set("state.total-boss-max-health", TOTAL_BOSS_MAX_HEALTH);
config.set("state.fight-owner", (FIGHT_OWNER == null) ? null : FIGHT_OWNER.toString());
diff --git a/src/main/java/nu/nerd/df/DragonFight.java b/src/main/java/nu/nerd/df/DragonFight.java
index 3e19d89..e55ca3b 100644
--- a/src/main/java/nu/nerd/df/DragonFight.java
+++ b/src/main/java/nu/nerd/df/DragonFight.java
@@ -38,7 +38,8 @@ public class DragonFight extends JavaPlugin {
public void onEnable() {
PLUGIN = this;
saveDefaultConfig();
- CONFIG.reload();
+ CONFIG.reloadConfiguration();
+ CONFIG.reloadFightState();
addCommandExecutor(new DFExecutor());
addCommandExecutor(new DragonExecutor());
@@ -61,7 +62,7 @@ public void onDisable() {
// ------------------------------------------------------------------------
/**
* Add the specified CommandExecutor and set it as its own TabCompleter.
- *
+ *
* @param executor the CommandExecutor.
*/
protected void addCommandExecutor(ExecutorBase executor) {
diff --git a/src/main/java/nu/nerd/df/DragonUtil.java b/src/main/java/nu/nerd/df/DragonUtil.java
index eec2598..fc7e445 100644
--- a/src/main/java/nu/nerd/df/DragonUtil.java
+++ b/src/main/java/nu/nerd/df/DragonUtil.java
@@ -16,7 +16,7 @@
// --------------------------------------------------------------------------
/**
* DragonFight utility functions.
- *
+ *
* Note that the DragonFight plugin also references BeastMaster's DragonUtil
* class, so to avoid a qualified name, we name this class differently.
*/
@@ -24,7 +24,7 @@ public class DragonUtil {
// ------------------------------------------------------------------------
/**
* Return the world where the dragon fight occurs.
- *
+ *
* @return the world where the dragon fight occurs.
*/
public static World getFightWorld() {
@@ -35,7 +35,7 @@ public static World getFightWorld() {
/**
* Return true if the specified world is the one where the dragon fight can
* occur.
- *
+ *
* @param world the world.
* @return true if the specified world is the one where the dragon fight can
* occur.
@@ -48,9 +48,9 @@ public static boolean isFightWorld(World world) {
// ------------------------------------------------------------------------
/**
* Remove a specific dragon.
- *
+ *
* Because Entity.remove() would be too easy.
- *
+ *
* @param dragon the ender dragon to remove.
*/
public static void removeDragon(EnderDragon dragon) {
@@ -77,9 +77,9 @@ public static void removeAllDragons() {
/**
* Return true if an entity has the specified scoreboard tag or BeastMaster
* group.
- *
+ *
* @param entity the entity.
- * @param tag the tag or group to look for.
+ * @param tag the tag or group to look for.
* @return true if the entity has the tag or group.
*/
public static boolean hasTagOrGroup(Entity entity, String tag) {
@@ -102,7 +102,7 @@ public static boolean hasTagOrGroup(Entity entity, String tag) {
* Return the magnitude of the X and Z components of location, i.e. the
* distance between loc and the world origin in the XZ plane (ignoring Y
* coordinate).
- *
+ *
* @param loc the location.
* @return sqrt(x^2 + z^2).
*/
@@ -114,11 +114,11 @@ public static double getMagnitude2D(Location loc) {
/**
* Return true if the specified location is on the circle where the centre
* of the obsidian pillars are placed.
- *
+ *
* The pillars about placed in a circle about 40 blocks from (0,0). The
* largest pillars have a radius of 5 blocks, including the centre block, so
* ensure that we block placements up to 6 blocks away (6/40 = 0.15).
- *
+ *
* Note that:
*
* - This method assumes that the world has already been confirmed to be
@@ -129,19 +129,20 @@ public static double getMagnitude2D(Location loc) {
* spawning of crystals on pillars, because when the crystal on the pillar
* spawns, the block underneath it comes back as AIR, not BEDROCK.
*
- *
+ *
* @param loc the location.
* @return if the location is about the right range from the origin of the
* world to be the centre of a pillar.
*/
public static boolean isOnPillarCircle(Location loc) {
- return Math.abs(getMagnitude2D(loc) - DragonUtil.PILLAR_CIRCLE_RADIUS) < 0.15 * DragonUtil.PILLAR_CIRCLE_RADIUS;
+ return loc.getWorld().getName().equals("world_the_end") &&
+ Math.abs(getMagnitude2D(loc) - DragonUtil.PILLAR_CIRCLE_RADIUS) < 0.15 * DragonUtil.PILLAR_CIRCLE_RADIUS;
}
// ------------------------------------------------------------------------
/**
* The World where the fight occurs.
- *
+ *
* It could be configurable, but for now is constant.
*/
private static final String FIGHT_WORLD = "world_the_end";
diff --git a/src/main/java/nu/nerd/df/FightState.java b/src/main/java/nu/nerd/df/FightState.java
index 266db96..ad7d5a1 100644
--- a/src/main/java/nu/nerd/df/FightState.java
+++ b/src/main/java/nu/nerd/df/FightState.java
@@ -7,7 +7,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Set;
-import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -40,6 +39,8 @@
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.event.entity.EnderDragonChangePhaseEvent;
import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityDamageEvent;
@@ -78,6 +79,50 @@
*
* Define the term "arena" to mean the space within the circle of obsidian
* pillars in the end.
+ *
+ * The vanilla/Spigot DragonBattle class has various idiosyncrasies that this
+ * plugin must work around:
+ *
+ *
+ * - The insurmountable problem, which I discovered last (unfortunately), is
+ * that a dragon summoned using the Bukkit API has no AI, and logically, even if
+ * it did, might not have AI tailored to the dragon fight.
+ * - The Bukkit DragonBattle class can lose track of a dragon - perhaps it was
+ * in a chunk that was unloaded when a player logged out - and it will then
+ * spawn a replacement EnderDragon entity. Consequently, you can end up with
+ * multiple dragons.
+ * - In a similar vein, the DragonBattle class will simply respawn a dragon
+ * entity that was remove()d through the Bukkit API. We need to use the
+ * "/minecraft:kill" command to remove dragons in a way that tells DragonBattle
+ * to back off.
+ * - If the CreatureSpawnEvent of the ender dragon is cancelled, the
+ * DragonBattle goes into some weird state where there is no dragon, but there
+ * is a visible dragon boss bar, and ender crystals no longer summon a new
+ * dragon. The server cannot be fixed via the API (e.g.
+ * DragonBattle.setRespawnState(NONE)) or vanilla commands. Therefore, we must
+ * allow the vanilla DragonBattle class to finish spawning the dragon.
+ * - The World.spawn() call that creates the EnderDragon does not assign the
+ * reference to it in DragonBattle until that function returns (duh!), but
+ * World.spawn() calls the function to pass EntitySpawnEvent to this plugin. So
+ * when DragonFight receives an EntitySpawnEvent for the dragon,
+ * DragonBattle.getEnderDragon() will return null until the next game tick. So
+ * when a dragon spawns, we need to wait until the next game tick to see whether
+ * the DragonBattle was the culprit.
+ * - You cannot change the properties of the dragon spawned by DragonBattle in
+ * the EntitySpawnEvent (to make it invulnerable), perhaps because those
+ * properties are modified when World.spawn() returns. You have to wait a
+ * tick.
+ * - You cannot set end crystals (on pillars) invulnerable until the tick
+ * after the DragonBattle spawns the vanilla dragon.
+ *
+ *
+ * EnderDragon SpawnReason possibilities:
+ *
+ * - DEFAULT signifies that the dragon was summoned by ender crystals (vanilla
+ * dragon fight).
+ * - CUSTOM signifies that the dragon was summoned by a plugin.
+ *
+ *
*/
public class FightState implements Listener {
// ------------------------------------------------------------------------
@@ -86,9 +131,14 @@ public class FightState implements Listener {
*/
public void onEnable() {
defineBeastMasterObjects();
- discoverFightState();
- log("Detected stage: " + _stageNumber);
- reconfigureDragonBossBar();
+
+ // Force load chunks in the end, then wait a few seconds and hope that
+ // the entities are loaded. There is no way to force-load entities.
+ preLoadEndChunks();
+ Bukkit.getScheduler().runTaskLater(DragonFight.PLUGIN, () -> {
+ recoverFightState();
+ reconfigureDragonBossBar();
+ }, 100);
Bukkit.getScheduler().scheduleSyncRepeatingTask(DragonFight.PLUGIN, _tracker, TrackerTask.PERIOD_TICKS, TrackerTask.PERIOD_TICKS);
// Every 30 seconds remove extra dragons. YOLO.
@@ -106,32 +156,108 @@ public void onEnable() {
public void onDisable() {
Bukkit.getScheduler().cancelTasks(DragonFight.PLUGIN);
- // If restarting while the dragon is spawning, blow it all away and log
- // it so the admins can refund.
+ // If 4 crystals are placed and there is a restart, I'm not sure if
+ // the dragon will respawn after the restart. Log the situation so an
+ // admin refund is possible.
List spawningCrystals = getDragonSpawnCrystals();
if (spawningCrystals.size() == 4) {
- log("Stopping the fight due to restart.");
+ log("The server is restarting during dragon spawn (4 crystals placed).");
cmdStop(Bukkit.getConsoleSender());
}
}
// ------------------------------------------------------------------------
/**
- * Implement the /dragon info command.
+ * Return true if a dragon fight is currently underway.
*
- * This command is for ordinary players to check the fight status.
+ * @return true if a fight is happening.
*/
- public void cmdPlayerInfo(CommandSender sender) {
- if (_stageNumber == 0 || DragonFight.CONFIG.FIGHT_OWNER == null) {
- sender.sendMessage(ChatColor.DARK_PURPLE + "Nobody is fighting the dragon right now.");
- return;
- } else {
- sender.sendMessage(ChatColor.DARK_PURPLE + "The current fight stage is " +
- ChatColor.LIGHT_PURPLE + _stageNumber + ChatColor.DARK_PURPLE + ".");
- OfflinePlayer fightOwner = Bukkit.getOfflinePlayer(DragonFight.CONFIG.FIGHT_OWNER);
- sender.sendMessage(ChatColor.DARK_PURPLE + "The final drops are owned by " +
- ChatColor.LIGHT_PURPLE + fightOwner.getName() + ChatColor.DARK_PURPLE + ".");
+ public boolean isFightHappening() {
+ return DragonFight.CONFIG.STAGE_NUMBER > 0;
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Return true when the stage number is changing to
+ * {@link Configuration#NEW_STAGE_NUMBER}.
+ *
+ * @return true when the stage number is changing.
+ */
+ public boolean isStageNumberChanging() {
+ return getStageNumber() != getNewStageNumber();
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Finish the change of stage number to
+ * {@link Configuration#NEW_STAGE_NUMBER}.
+ */
+ public void finishStageNumberChange() {
+ setStageNumber(getNewStageNumber());
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Immediately change to the specified stage number.
+ *
+ * {@link Configuration#NEW_STAGE_NUMBER} is also immediately set to the new
+ * stage number to signify the transition is complete.
+ *
+ * @param stageNumber the new stage number.
+ */
+ public void immediatelyChangeStageNumber(int stageNumber) {
+ setStageNumber(stageNumber);
+ setNewStageNumber(stageNumber);
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Get the current fight stage number.
+ *
+ * @return 0 for no fight, or 1 to 11 for the various fight stages.
+ */
+ public int getStageNumber() {
+ return DragonFight.CONFIG.STAGE_NUMBER;
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Set the fight stage number.
+ *
+ * @param stageNumber the new stage number.
+ */
+ public void setStageNumber(int stageNumber) {
+ if (stageNumber < 0 || stageNumber > 11) {
+ throw new IllegalArgumentException("invalid stage number: " + stageNumber);
}
+ DragonFight.CONFIG.STAGE_NUMBER = stageNumber;
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Get the new fight stage number.
+ *
+ * The stage number changes to this new value after mob spawn animations.
+ *
+ * @return 0 for no fight, or 1 to 11 for the various fight stages.
+ */
+ public int getNewStageNumber() {
+ return DragonFight.CONFIG.NEW_STAGE_NUMBER;
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Set the new fight stage number.
+ *
+ * The stage number changes to this new value after mob spawn animations.
+ *
+ * @param stageNumber the new stage number.
+ */
+ public void setNewStageNumber(int stageNumber) {
+ if (stageNumber < 0 || stageNumber > 11) {
+ throw new IllegalArgumentException("invalid stage number: " + stageNumber);
+ }
+ DragonFight.CONFIG.NEW_STAGE_NUMBER = stageNumber;
}
// ------------------------------------------------------------------------
@@ -145,8 +271,12 @@ public void cmdPlayerInfo(CommandSender sender) {
*/
public void cmdInfo(CommandSender sender) {
sender.sendMessage(ChatColor.DARK_PURPLE + "The current fight stage is " +
- ChatColor.LIGHT_PURPLE + _stageNumber + ChatColor.DARK_PURPLE + ".");
- if (_stageNumber == 0) {
+ ChatColor.LIGHT_PURPLE + getStageNumber() + ChatColor.DARK_PURPLE + ".");
+ if (isStageNumberChanging()) {
+ sender.sendMessage(ChatColor.DARK_PURPLE + "The fight stage is changing to " +
+ ChatColor.LIGHT_PURPLE + DragonFight.CONFIG.NEW_STAGE_NUMBER + ChatColor.DARK_PURPLE + ".");
+ }
+ if (!isFightHappening()) {
return;
}
@@ -159,7 +289,7 @@ public void cmdInfo(CommandSender sender) {
ChatColor.LIGHT_PURPLE + fightOwner.getName() + ChatColor.DARK_PURPLE + ".");
}
- if (_stageNumber >= 1 && _stageNumber <= 10) {
+ if (getStageNumber() >= 1 && getStageNumber() <= 10) {
sender.sendMessage(ChatColor.DARK_PURPLE + "The current total boss health is " +
ChatColor.LIGHT_PURPLE + String.format("%.1f", getTotalBossHealth()) +
ChatColor.DARK_PURPLE + " out of " +
@@ -232,14 +362,16 @@ public void cmdStop(CommandSender sender) {
sender.sendMessage(ChatColor.DARK_PURPLE + "Removed pillar crystals: " + ChatColor.LIGHT_PURPLE + _crystals.size());
_crystals.clear();
- cleanUp(sender);
- _stageNumber = 0;
+ cleanUpMobsAndProjectiles(sender);
// Immediately hide the boss bar, rather than waiting for update.
if (_bossBar != null) {
_bossBar.setVisible(false);
}
+ // Clear stage numbers prior to config save.
+ immediatelyChangeStageNumber(0);
+
// Nobody owns the drops, even if a dragon randomly spawns for funsies.
DragonFight.CONFIG.FIGHT_OWNER = null;
DragonFight.CONFIG.save();
@@ -262,17 +394,17 @@ public void cmdNextStage(CommandSender sender) {
return;
}
- int nextStage = (_stageNumber + 1) % 12;
+ int nextStage = (getStageNumber() + 1) % 12;
if (nextStage == 0) {
cmdStop(sender);
return;
}
- cleanUp(sender);
+ cleanUpMobsAndProjectiles(sender);
despawnPillarCrystals(1);
// Skip to the next stage.
- startStage(sender, _stageNumber + 1, getBossSpawnLocation());
+ startStage(sender, getStageNumber() + 1, getBossSpawnLocation());
}
// ------------------------------------------------------------------------
@@ -300,65 +432,30 @@ public void cmdSkipToStage(CommandSender sender, int stageNumber) {
return;
}
- if (stageNumber < _stageNumber) {
+ if (stageNumber < getStageNumber()) {
sender.sendMessage(ChatColor.RED + "Skipping backwards is not currently supported.");
return;
}
- if (stageNumber == _stageNumber) {
+ if (stageNumber == getStageNumber()) {
sender.sendMessage(ChatColor.DARK_PURPLE + "You're already in stage " +
ChatColor.LIGHT_PURPLE + stageNumber + ChatColor.DARK_PURPLE + ".");
return;
}
- // So from here on, stageNumber > _stageNumber.
+ // So from here on, stageNumber > getStageNumber().
World fightWorld = DragonUtil.getFightWorld();
DragonBattle battle = fightWorld.getEnderDragonBattle();
if (battle.getEnderDragon() == null) {
sender.sendMessage(ChatColor.RED + "You have to spawn the dragon first, using end crystals.");
return;
- } else {
- // Going from stage > 0 to a higher number.
- cleanUp(sender);
}
- int skippedStages = stageNumber - _stageNumber;
- despawnPillarCrystals(skippedStages);
- startStage(sender, stageNumber, getBossSpawnLocation());
- }
+ cleanUpMobsAndProjectiles(sender);
- // ------------------------------------------------------------------------
- /**
- * Implement the /dragon prize command.
- *
- * @param sender the command sender, for message sending.
- */
- public void cmdDragonPrize(CommandSender sender) {
- Player player = (Player) sender;
- UUID playerUuid = player.getUniqueId();
-
- int unclaimed = DragonFight.CONFIG.getUnclaimedPrizes(playerUuid);
- if (unclaimed <= 0) {
- sender.sendMessage(ChatColor.DARK_PURPLE + "You don't have any unclaimed dragon prizes.");
- } else {
- List prizes = generatePrizes();
- if (givePrizes(player, prizes)) {
- DragonFight.CONFIG.incUnclaimedPrizes(playerUuid, -1);
- DragonFight.CONFIG.save();
-
- if (--unclaimed > 0) {
- String prizeCount = ChatColor.LIGHT_PURPLE + Integer.toString(unclaimed) +
- ChatColor.DARK_PURPLE + " unclaimed prize" + (unclaimed > 1 ? "s" : "");
- sender.sendMessage(ChatColor.DARK_PURPLE + "You still have " + prizeCount + ".");
- }
- } else {
- // The item(s) did not fit.
- String slots = ChatColor.LIGHT_PURPLE + Integer.toString(prizes.size()) +
- ChatColor.DARK_PURPLE + " inventory slot" + (prizes.size() > 1 ? "s" : "");
- player.sendMessage(ChatColor.DARK_PURPLE + "You need at least " + slots + " empty.");
- player.sendMessage(ChatColor.DARK_PURPLE + "Make some room in your inventory and try again.");
- }
- }
+ int skippedStages = stageNumber - getStageNumber();
+ despawnPillarCrystals(skippedStages);
+ startStage(sender, stageNumber, stageNumber == 11 ? null : getBossSpawnLocation());
}
// ------------------------------------------------------------------------
@@ -388,7 +485,7 @@ public static void debug(String message) {
* Clean up mobs and projectiles and message the command sender with tallies
* of entities removed.
*/
- protected void cleanUp(CommandSender sender) {
+ protected void cleanUpMobsAndProjectiles(CommandSender sender) {
int projectileCount = 0;
int bossCount = 0;
int supportCount = 0;
@@ -413,37 +510,74 @@ protected void cleanUp(CommandSender sender) {
sender.sendMessage(ChatColor.DARK_PURPLE + "Removed projectiles: " + ChatColor.LIGHT_PURPLE + projectileCount);
}
+ // ------------------------------------------------------------------------
+ /**
+ * Load relevant end chunks when the plugin loads.
+ *
+ * As of Minecraft 1.17, loading a chunk does not immediately load the
+ * entities therein; they are loaded asynchronously, some time later. There
+ * is no method within the 1.18 Bukkit API to forcibly load those entities
+ * synchronously.
+ *
+ * On startup, we need to add all the end crystals to the {@link #_crystals}
+ * array. So this method starts the ball rolling on loading them and we
+ * check for them a few seconds later in {@link recoverFightState()}.
+ *
+ * References:
+ *
+ * - https://github.com/PaperMC/Paper/issues/5872
+ * - https://hub.spigotmc.org/jira/browse/SPIGOT-6547
+ * - https://hub.spigotmc.org/jira/browse/SPIGOT-6934
+ *
+ */
+ protected void preLoadEndChunks() {
+ World fightWorld = DragonUtil.getFightWorld();
+
+ // Preload chunks to ensure we find the crystals.
+ int chunkRange = (int) Math.ceil(TRACKED_RADIUS / 16);
+ for (int x = -chunkRange; x <= chunkRange; ++x) {
+ for (int z = -chunkRange; z <= chunkRange; ++z) {
+ Chunk chunk = fightWorld.getChunkAt(x, z);
+ chunk.addPluginChunkTicket(DragonFight.PLUGIN);
+ chunk.setForceLoaded(true);
+ chunk.load();
+ }
+ }
+ }
+
// ------------------------------------------------------------------------
/**
* When the plugin initialises, infer the state of the fight, including the
* stage number, based on the presence of crystals, the dragon and bosses.
+ * Then reconcile the inferred stage number with that saved in the config.
*
* Find the ender crystals that are part of the current dragon fight.
*
* There is no ChunkLoadEvent for spawn chunks, so we need to scan loaded
* chunks on startup.
*/
- protected void discoverFightState() {
+ protected void recoverFightState() {
World fightWorld = DragonUtil.getFightWorld();
DragonBattle battle = fightWorld.getEnderDragonBattle();
+ if (battle.getEnderDragon() != null) {
+ log("Dragon " + battle.getEnderDragon().getUniqueId() + " exists.");
+ } else {
+ log("No dragon exists.");
+ }
+
// Preload chunks to ensure we find the crystals.
int chunkRange = (int) Math.ceil(TRACKED_RADIUS / 16);
for (int x = -chunkRange; x <= chunkRange; ++x) {
for (int z = -chunkRange; z <= chunkRange; ++z) {
Chunk chunk = fightWorld.getChunkAt(x, z);
- // Keep the chunk loaded until we can get some consistency
- // checking implemented. Even if not the real problem.
- fightWorld.loadChunk(chunk);
-
for (Entity entity : chunk.getEntities()) {
- if (entity instanceof EnderCrystal &&
- entity.getScoreboardTags().contains(PILLAR_CRYSTAL_TAG)) {
- log("Loaded crystal " + entity.getUniqueId() + " at " + Util.formatLocation(entity.getLocation()));
+ if (entity instanceof EnderCrystal && entity.getScoreboardTags().contains(PILLAR_CRYSTAL_TAG)) {
_crystals.add((EnderCrystal) entity);
- // In case the restart happened immediately after the
- // crystal spawned.
+ // In case the restart happened immediately after
+ // the crystal spawned.
entity.setInvulnerable(true);
+
} else if (entity instanceof LivingEntity) {
// Find bosses within the discoverable range.
if (DragonUtil.hasTagOrGroup(entity, BOSS_TAG)) {
@@ -454,43 +588,22 @@ protected void discoverFightState() {
}
}
log("Discovered bosses: " + _bosses.size());
+ log("Discovered crystals: " + _crystals.size());
- if (battle.getEnderDragon() != null) {
- log("Dragon " + battle.getEnderDragon().getUniqueId() + " exists.");
- } else {
- log("No dragon exists.");
+ if (getStageNumber() < 0 || getStageNumber() > 11) {
+ log("Stage number " + getStageNumber() + " is out of bounds. Setting to 0.");
+ immediatelyChangeStageNumber(0);
}
- // Work out what stage we're in.
- // The dragon can randomly despawn. Make a best guess.
- // Eventually will rewrite to load from config.
- if (battle.getEnderDragon() == null && _bosses.isEmpty() && _crystals.isEmpty()) {
- log("No dragon, bosses or pillar crystals. Guess stage 0.");
- _stageNumber = 0;
- } else {
- // We hope that vanilla code respawns the dragon at some point.
- _stageNumber = 10 - _crystals.size();
- log("Intial guess stage " + _stageNumber);
- }
-
- // A restart during the stage start spawn sequence can leave us
- // without bosses.
- if (battle.getEnderDragon() != null && _bosses.isEmpty()) {
- if (_stageNumber == 0) {
- // We have a dragon and 10 pillar crystals. Start stage 1.
- log("Restarting stage 1 spawn sequence after restart.");
- nextStage();
- } else if (_stageNumber < 10) { // _stageNumber 1-9
- // 1 - 9 pillar crystals and no bosses.
- log("Restarting stage " + (_stageNumber + 1) + " spawn sequence after restart.");
- nextStage();
- } else if (_stageNumber == 10) {
- // We have a dragon, 0 pillar crystals and no bosses.
- // The difference between stage 10 and 11 is that in 11 there
- // are no boss mobs.
- // Show titles again. Make dragon vulnerable.
- log("In stage " + 11 + ".");
- startStage11();
+ log("Current stage: " + getStageNumber());
+ if (isStageNumberChanging()) {
+ log("Transitioning from stage " + getStageNumber() + " to stage " + getNewStageNumber());
+ if (getNewStageNumber() != getStageNumber() + 1) {
+ log("Too many skipped stages! Going direct to " + getNewStageNumber() + ".");
+ startStage(null, getNewStageNumber(), getNewStageNumber() == 11 ? null : getBossSpawnLocation());
+ } else {
+ // Normal path... show boss spawn animations.
+ animateNextStage();
}
}
}
@@ -675,11 +788,10 @@ protected void onPlayerLogin(PlayerLoginEvent event) {
*
* Track any mobs that spawn with the boss group.
*
- * Handle end crystals spawns during the pillar-summoning phase only with
- * {@link #onPillarCrystalSpawn(EnderCrystal)}.
+ * @param event the event.
*/
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
- protected void onEntitySpawn(EntitySpawnEvent event) {
+ protected void onCreatureSpawn(CreatureSpawnEvent event) {
Location loc = event.getLocation();
World world = loc.getWorld();
if (!DragonUtil.isFightWorld(world)) {
@@ -687,22 +799,38 @@ protected void onEntitySpawn(EntitySpawnEvent event) {
}
Entity entity = event.getEntity();
- if (entity instanceof EnderDragon) {
- // Tag dragon as fight entity so its projectiles inherit that tag.
- entity.getScoreboardTags().add(ENTITY_TAG);
- onDragonSpawn((EnderDragon) entity);
- }
- // Track the bosses.
- if (entity instanceof LivingEntity && DragonUtil.hasTagOrGroup(entity, BOSS_TAG)) {
+ // Track the bosses. These might be dragons, but are not THE dragon.
+ if (DragonUtil.hasTagOrGroup(entity, BOSS_TAG)) {
LivingEntity boss = (LivingEntity) entity;
MobType bossMobType = BeastMaster.getMobType(boss);
log("Boss spawned: " + bossMobType.getId());
_bosses.add(boss);
DragonFight.CONFIG.TOTAL_BOSS_MAX_HEALTH += boss.getMaxHealth();
+ } else {
+ // Pass vanilla (DragonBattle) dragon spawns (not plugin spawns) to
+ // onDragonSpawn().
+ SpawnReason reason = event.getSpawnReason();
+ if (reason == SpawnReason.DEFAULT && entity instanceof EnderDragon) {
+ onDragonSpawn((EnderDragon) entity, reason);
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Handle end crystals spawns during the pillar-summoning phase only with
+ * {@link #onPillarCrystalSpawn(EnderCrystal)}.
+ */
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
+ protected void onEntitySpawn(EntitySpawnEvent event) {
+ Location loc = event.getLocation();
+ World world = loc.getWorld();
+ if (!DragonUtil.isFightWorld(world)) {
+ return;
}
- DragonBattle battle = world.getEnderDragonBattle();
+ Entity entity = event.getEntity();
if (entity instanceof EnderCrystal) {
// Make the summoning crystals glowing and invulnerable to prevent
// the player from initiating the fight and then destroying the
@@ -714,6 +842,7 @@ protected void onEntitySpawn(EntitySpawnEvent event) {
}
// Register and protect crystals spawned on the pillars.
+ DragonBattle battle = world.getEnderDragonBattle();
if (battle.getRespawnPhase() == RespawnPhase.SUMMONING_PILLARS &&
DragonUtil.isOnPillarCircle(entity.getLocation())) {
onPillarCrystalSpawn((EnderCrystal) entity);
@@ -781,7 +910,12 @@ protected void onProjectileLaunch(ProjectileLaunchEvent event) {
// ------------------------------------------------------------------------
/**
* Protect the End Crystals on the pillars and the four crystals that spawn
- * the dragon.
+ * the dragon, and protect the dragon in stages 1 to 10.
+ *
+ * You might think that you could just call setInvulnerable(true) on the
+ * dragon to make it invulnerable. Perhaps in a periodic task like the
+ * {@link FightState#Tracker}. You would be wrong; having setInvulnerable()
+ * work as advertised is just wishful thinking.
*
* I had some problems with setInvulnerable(); it doesn't just work
* on the 10 pillar crystals. If you think you can just set them
@@ -806,7 +940,12 @@ public void onEntityDamageEarly(EntityDamageEvent event) {
return;
}
- if (entity instanceof EnderCrystal) {
+ if (entity instanceof EnderDragon && getStageNumber() < 11) {
+ DragonBattle battle = DragonUtil.getFightWorld().getEnderDragonBattle();
+ if (entity == battle.getEnderDragon()) {
+ event.setCancelled(true);
+ }
+ } else if (entity instanceof EnderCrystal) {
if (_crystals.contains(entity) ||
DragonUtil.hasTagOrGroup(entity, PILLAR_CRYSTAL_TAG) ||
DragonUtil.hasTagOrGroup(entity, SPAWNING_CRYSTAL_TAG)) {
@@ -911,8 +1050,6 @@ protected void onEntityDeath(EntityDeathEvent event) {
if (event.getEntity() instanceof EnderDragon) {
onDragonDeath((EnderDragon) event.getEntity());
- // TODO: clean up associated entities?
- _stageNumber = 0;
return;
}
@@ -923,13 +1060,9 @@ protected void onEntityDeath(EntityDeathEvent event) {
log("Boss died: " + bossMobType.getId());
}
- if (_stageNumber != 0 && bossDied && _bosses.isEmpty()) {
- if (_stageNumber == 10) {
- // Just the dragon in stage 11.
- startStage11();
- } else {
- nextStage();
- }
+ // The fight is going and all the bosses are dead. Next stage!
+ if (isFightHappening() && bossDied && _bosses.isEmpty()) {
+ animateNextStage();
}
}
@@ -1060,35 +1193,32 @@ protected void onPillarCrystalSpawn(EnderCrystal crystal) {
// ------------------------------------------------------------------------
/**
- * When the dragon spawns, schedule the start of phase 1.
+ * This method responds to an ender dragon spawning.
*
- * We need to be careful about badly-timed restarts (as always). Since we
- * detect phases on reload by the absence of crystals, remove the crystal
- * before adding the boss.
+ * At the time this method is called, the dragon entity is not yet
+ * associated with the Bukkit DragonBattle instance, because we're executing
+ * in the context of a CreatureSpawnEvent, which is effectively a
+ * synchronous function call from World.spawn(). We would need to wait for 1
+ * tick if we needed to access the dragon through the DragonBattle's methods
+ * (note the called methods too).
*
- * Experimentation reveals that at the time the dragon spawns, the spawning
- * crystals still exist and the RespawnPhase is NONE.
+ * @param dragon the dragon.
+ * @param reason the reason for spawning.
*/
- protected void onDragonSpawn(EnderDragon dragon) {
- log("Dragon " + dragon.getUniqueId() + " spawned.");
+ protected void onDragonSpawn(EnderDragon dragon, SpawnReason reason) {
+ log("Dragon spawned (" + reason + "): " + dragon.getUniqueId());
+
+ // Tag dragon as fight entity so its projectiles inherit that tag.
+ dragon.getScoreboardTags().add(ENTITY_TAG);
+
+ // Doesn't do anything. See onEntityDamageEarly().
+ dragon.setInvulnerable(true);
// Remove surplus dragons after this one is added to the world.
Bukkit.getScheduler().runTaskLater(DragonFight.PLUGIN, () -> removeSurplusDragons(), 2);
- // debug("Dragon spawned. Spawning crystals: " +
- // getDragonSpawnCrystals());
- // debug("Respawn phase: " +
- if (_crystals.size() == 0) {
- // Observed once in testing. Don't set invulnerable.
- log("Dragon spawned but there were no ender crystals?!");
- } else {
- // The dragon is invulnerable for stages 1 through 10.
- // NOTE: creative mode trumps invulnerability.
- dragon.setInvulnerable(true);
- }
-
// Setting the crystals invulnerable before the dragon spawns does not
- // work. But Minecraft prevents them from being da maged.
+ // work. But Minecraft prevents them from being damaged.
// Cannot set them invulnerable this tick either.
for (EnderCrystal crystal : _crystals) {
Bukkit.getScheduler().runTaskLater(DragonFight.PLUGIN,
@@ -1096,22 +1226,8 @@ protected void onDragonSpawn(EnderDragon dragon) {
}
reconfigureDragonBossBar();
- // Since extra dragons can spawn randomly mid-fight, we should only
- // advance to the next stage if there are no bosses.
- if (_stageNumber == 0 && _bosses.isEmpty()) {
- log("Stage 0, no bosses, new dragon.");
- // We may have previously inferred the stage to be 0 based on an
- // absence of the dragon, crystals and bosses. if there are 10
- // crystals and no bosses, we're going from stage 0 to stage 1.
- if (_crystals.size() == 10) {
- log("All 10 crystals, no bosses, new dragon. Go go to stage 1.");
- nextStage();
- } else if (_crystals.size() == 0) {
- log("No crystals, no bosses, new dragon. Must be in stage 11.");
- startStage11();
- }
- } else {
- log("Stay in this stage. Bosses: " + _bosses.size() + ", Crystals: " + _crystals.size());
+ if (getStageNumber() == 0) {
+ animateNextStage();
}
}
@@ -1129,11 +1245,16 @@ protected void onDragonSpawn(EnderDragon dragon) {
protected void onDragonDeath(EnderDragon dragon) {
log("The dragon died.");
- if (_stageNumber != 11) {
+ if (getStageNumber() != 11) {
log("But we're not in stage 11, so it doesn't count.");
return;
}
+ // In stage 11, the dragon died. Signify that the fight is over before
+ // saving the config.
+ immediatelyChangeStageNumber(0);
+ // TODO: clean up associated entities?
+
if (DragonFight.CONFIG.FIGHT_OWNER == null) {
log("But nobody owns the fight, so it doesn't count.");
return;
@@ -1184,7 +1305,7 @@ protected void onDragonDeath(EnderDragon dragon) {
* @param prizes the list of items to give.
* @return true if all prizes could fit; false if they were deferred.
*/
- protected static boolean givePrizes(Player player, List prizes) {
+ public static boolean givePrizes(Player player, List prizes) {
if (prizes.size() == 0) {
log("No dragon drops have been configured!");
return true;
@@ -1223,7 +1344,7 @@ protected static boolean givePrizes(Player player, List prizes) {
*
* @return a list of ItemStacks.
*/
- protected static List generatePrizes() {
+ public static List generatePrizes() {
ArrayList prizes = new ArrayList<>();
DropSet dropSet = BeastMaster.LOOTS.getDropSet("df-dragon-drops");
Drop drop = dropSet.chooseOneDrop(true);
@@ -1304,12 +1425,22 @@ protected void reconfigureDragonBossBar() {
// ------------------------------------------------------------------------
/**
- * Start the next stage.
+ * Start the next boss stage by showing the boss spawn animations.
*
* This method is called to do the boss spawning sequence for stages 1 to
* 10.
*/
- protected void nextStage() {
+ protected void animateNextStage() {
+ setNewStageNumber(getStageNumber() + 1);
+
+ // When going to stage 11 (dragon only) there are no bosses to spawn.
+ // Just call startStage() and return.
+ if (getNewStageNumber() == 11) {
+ startStage(null, getNewStageNumber(), null);
+ return;
+ }
+
+ // From here forward, getNewStageNumber() is between 1 and 10.
// Remove a random crystal. Random order due to hashing UUID.
EnderCrystal replacedCrystal = _crystals.iterator().next();
@@ -1344,11 +1475,11 @@ protected void nextStage() {
playSound(bossSpawnLocation, Sound.BLOCK_BEACON_ACTIVATE);
}, STAGE_START_DELAY * 80 / 100);
- // Remove the crystal and spawn the boss. Only called for stage 1-10.
+ // Remove the crystal and spawn the boss.
Bukkit.getScheduler().scheduleSyncDelayedTask(DragonFight.PLUGIN, () -> {
_crystals.remove(replacedCrystal);
replacedCrystal.remove();
- startStage(null, _stageNumber + 1, bossSpawnLocation);
+ startStage(null, getNewStageNumber(), bossSpawnLocation);
}, STAGE_START_DELAY);
}
@@ -1373,23 +1504,29 @@ protected void despawnPillarCrystals(int count) {
/**
* Begin the specified boss stage (1 to 11 only).
*
+ * In the case of stages 1 through 10, the boss mob is spawned.
+ *
* @param sender the command sender for messages, or null if
* unused.
- * @param stageNumber the stage number from 1 to 10.
+ * @param stageNumber the stage number from 1 to 11.
* @param bossSpawnLocation the location where the bosses are spawned.
*/
protected void startStage(CommandSender sender, int stageNumber, Location bossSpawnLocation) {
+ // The new stage number will (in many but not all cases) have already
+ // been set to stageNumber arg. Ensure that all stage numbers are in
+ // sync.
+ immediatelyChangeStageNumber(stageNumber);
+
if (sender != null) {
sender.sendMessage(ChatColor.DARK_PURPLE + "Starting stage: " + ChatColor.LIGHT_PURPLE + stageNumber);
}
- _stageNumber = stageNumber;
- Stage stage = DragonFight.CONFIG.getStage(_stageNumber);
- log("Beginning stage: " + _stageNumber);
+ Stage stage = DragonFight.CONFIG.getStage(getStageNumber());
+ log("Beginning stage: " + getStageNumber());
// Spawn boss or bosses. Only valid in stages 1 to 10.
- if (_stageNumber >= 1 && _stageNumber <= 10) {
- DragonFight.CONFIG.TOTAL_BOSS_MAX_HEALTH = 0;
+ DragonFight.CONFIG.TOTAL_BOSS_MAX_HEALTH = 0;
+ if (getStageNumber() >= 1 && getStageNumber() <= 10) {
DropResults results = new DropResults();
DropSet dropSet = BeastMaster.LOOTS.getDropSet(stage.getDropSetId());
if (dropSet != null) {
@@ -1412,14 +1549,6 @@ protected void startStage(CommandSender sender, int stageNumber, Location bossSp
stage.announce(nearby);
}
- // ------------------------------------------------------------------------
- /**
- * Show the stage 11 titles and set the dragon vulnerable again.
- */
- protected void startStage11() {
- startStage(null, 11, null);
- }
-
// ------------------------------------------------------------------------
/**
* Choose a random location to spawn the boss with a 3x3x3 volume of air
@@ -1506,7 +1635,7 @@ protected Set getNearbyPlayers() {
* some test bosses.
*/
protected void updateBossBar() {
- if (_stageNumber >= 1 && _stageNumber <= 11) {
+ if (getStageNumber() >= 1 && getStageNumber() <= 11) {
// Vanilla/Bukkit code reuses the dragon boss bar across fights, so
// set its colour across all stages.
Stage stage = DragonFight.CONFIG.getStage(11);
@@ -1514,7 +1643,7 @@ protected void updateBossBar() {
battle.getBossBar().setColor(stage.getBarColor());
}
- if (_bosses.size() == 0 || _stageNumber < 1 || _stageNumber > 10) {
+ if (_bosses.size() == 0 || getStageNumber() < 1 || getStageNumber() > 10) {
if (_bossBar != null) {
_bossBar.setVisible(false);
}
@@ -1522,7 +1651,7 @@ protected void updateBossBar() {
}
// We have a non-zero number of bosses. Stage 1 to 10. Show the bar.
- Stage stage = DragonFight.CONFIG.getStage(_stageNumber);
+ Stage stage = DragonFight.CONFIG.getStage(getStageNumber());
if (_bossBar == null) {
_bossBar = Bukkit.createBossBar("", BarColor.WHITE, BarStyle.SOLID, new BarFlag[0]);
}
@@ -1599,7 +1728,8 @@ protected void removeSurplusDragons() {
}
if (retainedDragon != null) {
- retainedDragon.setInvulnerable(_stageNumber != 11);
+ // Doesn't do anything. See onEntityDamageEarly().
+ retainedDragon.setInvulnerable(getStageNumber() != 11);
}
}
@@ -1688,12 +1818,16 @@ public void run() {
private static final String ENTITY_TAG = "df-entity";
/**
- * Group of summoned boss mobs on spawn.
+ * Group tag of summoned boss mobs on spawn.
+ *
+ * Note that the final boss enderdragon is not allowed to have this tag.
+ * Note also that per-stage boss mobs can be enderdragons, but they are not
+ * the final boss.
*/
private static final String BOSS_TAG = "df-boss";
/**
- * Group of support mobs summoned by bosses.
+ * Group tag of support mobs summoned by bosses.
*/
private static final String SUPPORT_TAG = "df-support";
@@ -1771,16 +1905,6 @@ public void run() {
*/
protected HashSet _bosses = new HashSet<>();
- /**
- * Current stage number.
- *
- * in Stage N, N crystals have been removed.
- *
- * Stage 0 is before the fight, Stage 1 => first crystal removed and boss
- * spawned. Stage 10 => final boss spawned. Stage 11: dragon.
- */
- int _stageNumber;
-
/**
* Tracks mobs to enforce boundaries and update boss bars.
*/
diff --git a/src/main/java/nu/nerd/df/commands/DragonExecutor.java b/src/main/java/nu/nerd/df/commands/DragonExecutor.java
index 0c83e26..e415c0c 100644
--- a/src/main/java/nu/nerd/df/commands/DragonExecutor.java
+++ b/src/main/java/nu/nerd/df/commands/DragonExecutor.java
@@ -1,11 +1,19 @@
package nu.nerd.df.commands;
+import java.util.List;
+import java.util.UUID;
+
+import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
+import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
import nu.nerd.beastmaster.commands.ExecutorBase;
import nu.nerd.df.DragonFight;
+import nu.nerd.df.FightState;
// ----------------------------------------------------------------------------
/**
@@ -32,7 +40,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
}
if (args.length == 1 && args[0].equalsIgnoreCase("info")) {
- DragonFight.FIGHT.cmdPlayerInfo(sender);
+ cmdInfo(sender);
return true;
}
@@ -40,11 +48,69 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
if (!isInGame(sender)) {
return true;
}
- DragonFight.FIGHT.cmdDragonPrize(sender);
+ cmdDragonPrize(sender);
return true;
}
sender.sendMessage(ChatColor.RED + "Invalid arguments. Try /" + command.getName() + " help.");
return true;
}
+
+ // ------------------------------------------------------------------------
+ /**
+ * Implement the /dragon info command.
+ *
+ * This command is for ordinary players to check the fight status.
+ */
+ public void cmdInfo(CommandSender sender) {
+ if (!DragonFight.FIGHT.isFightHappening() && !DragonFight.FIGHT.isStageNumberChanging()) {
+ sender.sendMessage(ChatColor.DARK_PURPLE + "Nobody is fighting the dragon right now.");
+ } else {
+ sender.sendMessage(ChatColor.DARK_PURPLE + "The current fight stage is " +
+ ChatColor.LIGHT_PURPLE + DragonFight.FIGHT.getStageNumber() +
+ ChatColor.DARK_PURPLE + ".");
+ if (DragonFight.CONFIG.FIGHT_OWNER == null) {
+ sender.sendMessage(ChatColor.DARK_PURPLE + "But mysteriously, nobody owns the final drops. :/");
+ } else {
+ OfflinePlayer fightOwner = Bukkit.getOfflinePlayer(DragonFight.CONFIG.FIGHT_OWNER);
+ sender.sendMessage(ChatColor.DARK_PURPLE + "The final drops are owned by " +
+ ChatColor.LIGHT_PURPLE + fightOwner.getName() + ChatColor.DARK_PURPLE + ".");
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ /**
+ * Implement the /dragon prize command.
+ *
+ * @param sender the command sender, for message sending.
+ */
+ public void cmdDragonPrize(CommandSender sender) {
+ Player player = (Player) sender;
+ UUID playerUuid = player.getUniqueId();
+
+ int unclaimed = DragonFight.CONFIG.getUnclaimedPrizes(playerUuid);
+ if (unclaimed <= 0) {
+ sender.sendMessage(ChatColor.DARK_PURPLE + "You don't have any unclaimed dragon prizes.");
+ } else {
+ List prizes = FightState.generatePrizes();
+ if (FightState.givePrizes(player, prizes)) {
+ DragonFight.CONFIG.incUnclaimedPrizes(playerUuid, -1);
+ DragonFight.CONFIG.save();
+
+ if (--unclaimed > 0) {
+ String prizeCount = ChatColor.LIGHT_PURPLE + Integer.toString(unclaimed) +
+ ChatColor.DARK_PURPLE + " unclaimed prize" + (unclaimed > 1 ? "s" : "");
+ sender.sendMessage(ChatColor.DARK_PURPLE + "You still have " + prizeCount + ".");
+ }
+ } else {
+ // The item(s) did not fit.
+ String slots = ChatColor.LIGHT_PURPLE + Integer.toString(prizes.size()) +
+ ChatColor.DARK_PURPLE + " inventory slot" + (prizes.size() > 1 ? "s" : "");
+ player.sendMessage(ChatColor.DARK_PURPLE + "You need at least " + slots + " empty.");
+ player.sendMessage(ChatColor.DARK_PURPLE + "Make some room in your inventory and try again.");
+ }
+ }
+ }
+
} // class DragonExecutor
\ No newline at end of file
diff --git a/src/main/java/nu/nerd/df/commands/DragonFightExecutor.java b/src/main/java/nu/nerd/df/commands/DragonFightExecutor.java
index f459ff9..ceff269 100644
--- a/src/main/java/nu/nerd/df/commands/DragonFightExecutor.java
+++ b/src/main/java/nu/nerd/df/commands/DragonFightExecutor.java
@@ -32,7 +32,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
}
if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
- DragonFight.CONFIG.reload();
+ DragonFight.CONFIG.reloadConfiguration();
sender.sendMessage(ChatColor.DARK_PURPLE + "DragonFight configuration reloaded.");
return true;
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 36af87d..329837a 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -3,6 +3,8 @@ settings:
debug-prefix: '&5[DragonFight &fDBG&5]&f'
state:
+ stage-number: 0
+ new-stage-number: 0
total-boss-max-health: 0
fight-owner:
unclaimed-prizes: {}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 288085d..3239f25 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -5,7 +5,7 @@ authors: []
description: ${project.description}
website: ${project.url}
main: nu.nerd.df.DragonFight
-api-version: 1.15
+api-version: 1.18
depend: [BeastMaster]
permissions: