diff --git a/pom.xml b/pom.xml index 79a78cc..50a876b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.modularsoft PlayerHeadHunt - 1.1.1 + 1.2.0 UTF-8 @@ -18,7 +18,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.13.0 17 @@ -26,7 +26,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.4.1 + 3.5.3 package @@ -37,7 +37,7 @@ org.apache.hc - org.modularsoft.PlayerHeadHunt.shaded.org.apache.hc + org.modularsoft.PlayerHeadHunt.shaded.org.apache.hc @@ -71,7 +71,7 @@ io.papermc.paper paper-api - 1.20.4-R0.1-SNAPSHOT + 1.21.4-R0.1-SNAPSHOT provided @@ -111,20 +111,20 @@ org.projectlombok lombok - 1.18.30 + 1.18.36 provided org.yaml snakeyaml - 2.0 + 2.3 org.apache.httpcomponents.client5 httpclient5 - 5.2 + 5.4.1 diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/HeadChatController.java b/src/main/java/org/modularsoft/PlayerHeadHunt/HeadChatController.java index d1320e1..0944b7f 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/HeadChatController.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/HeadChatController.java @@ -101,12 +101,22 @@ public void newPlayerJoinsTheHunt(Player player) { } public void playersOwnHeadCountResponse(Player player) { - // Use the instance of HeadQuery to call the method player.sendMessage(plugin.config().getLangHeadCount() .replace("%FOUNDHEADS%", "" + headQuery.foundHeadsCount(player)) .replace("%NUMBEROFHEADS%", "" + plugin.config().getTotalHeads())); } + public void targetPlayerHeadCountResponse(org.bukkit.command.CommandSender sender, String targetName, int count) { + if (count == -1) { + sender.sendMessage(ChatColor.RED + "No data found for player: " + targetName); + return; + } + sender.sendMessage(plugin.config().getLangHeadCount() + .replace("%FOUNDHEADS%", "" + count) + .replace("%NUMBEROFHEADS%", "" + plugin.config().getTotalHeads()) + .replace("You have", targetName + " has")); + } + public void playerClearedTheirHeadsResponse(Player player) { player.sendMessage("All heads have been cleared."); } diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/HeadQuery.java b/src/main/java/org/modularsoft/PlayerHeadHunt/HeadQuery.java index a6e4fa4..100801d 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/HeadQuery.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/HeadQuery.java @@ -12,6 +12,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import java.util.stream.Stream; public class HeadQuery { private final YamlFileManager yamlFileManager; @@ -50,24 +51,28 @@ public int foundHeadsCount(Player player) { public int foundHeadsAlreadyCount(int xCord, int yCord, int zCord) { Map data = yamlFileManager.getData(); - Object headsObject = data.get("heads"); + int count = 0; - // Ensure the "heads" object is a list - if (!(headsObject instanceof List)) { - return 0; // Return 0 if the data is not a list + for (Map.Entry entry : data.entrySet()) { + if (!(entry.getValue() instanceof Map playerData)) continue; + + Object headsObj = playerData.get("headsCollected"); + if (!(headsObj instanceof List heads)) continue; + + for (Object head : heads) { + if (!(head instanceof Map headMap)) continue; + Object hx = headMap.get("x"); + Object hy = headMap.get("y"); + Object hz = headMap.get("z"); + if (hx instanceof Integer && hy instanceof Integer && hz instanceof Integer + && (Integer) hx == xCord && (Integer) hy == yCord && (Integer) hz == zCord) { + count++; + break; // each player counts once per head location + } + } } - List heads = (List) headsObject; - - // Filter and count matching heads - return (int) heads.stream() - .filter(head -> head instanceof Map) - .map(head -> (Map) head) - .filter(head -> - head.get("x") instanceof Integer && head.get("y") instanceof Integer && head.get("z") instanceof Integer && - head.get("x").equals(xCord) && head.get("y").equals(yCord) && head.get("z").equals(zCord) - ) - .count(); + return count; } public boolean clearHeads(Player player) { @@ -108,7 +113,7 @@ public boolean hasAlreadyCollectedHead(Player player, int x, int y, int z) { // Check if the head coordinates already exist in the list return headsCollected.stream().anyMatch(head -> - head.get("x") == x && head.get("y") == y && head.get("z") == z); + Objects.equals(head.get("x"), x) && Objects.equals(head.get("y"), y) && Objects.equals(head.get("z"), z)); } public void insertCollectedHead(Player player, int x, int y, int z) { @@ -163,6 +168,52 @@ public boolean addNewHunter(Player player) { return true; } + public int foundHeadsCountByName(String playerName) { + Map data = yamlFileManager.getData(); + for (Map.Entry entry : data.entrySet()) { + if (!(entry.getValue() instanceof Map playerData)) continue; + String username = (String) playerData.get("username"); + if (!playerName.equalsIgnoreCase(username)) continue; + Object headsObj = playerData.get("headsCollected"); + if (headsObj instanceof List heads) { + return heads.size(); + } + return 0; + } + return -1; // -1 indicates player not found in data + } + + public boolean clearHeadsByName(String playerName) { + Map data = yamlFileManager.getData(); + for (Map.Entry entry : data.entrySet()) { + if (!(entry.getValue() instanceof Map rawPlayerData)) continue; + String username = (String) rawPlayerData.get("username"); + if (!playerName.equalsIgnoreCase(username)) continue; + + Map playerData = (Map) entry.getValue(); + List> headsCollected = (List>) playerData.get("headsCollected"); + if (headsCollected != null) headsCollected.clear(); + playerData.put("headsCollectedCount", 0); + yamlFileManager.save(); + return true; + } + return false; + } + + public int getLoadedPlayerCount() { + return yamlFileManager.getData().size(); + } + + public int getTotalHeadsCollectedAcrossAllPlayers() { + return yamlFileManager.getData().values().stream() + .filter(v -> v instanceof Map) + .mapToInt(v -> { + Object headsObj = ((Map) v).get("headsCollected"); + return (headsObj instanceof List list) ? list.size() : 0; + }) + .sum(); + } + private boolean isPlayerExcluded(UUID uuid, String username) { return luckPerms.getUserManager().loadUser(uuid) .thenApply(user -> { diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/HeadWorldController.java b/src/main/java/org/modularsoft/PlayerHeadHunt/HeadWorldController.java index 4afbb0e..ab72564 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/HeadWorldController.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/HeadWorldController.java @@ -21,18 +21,13 @@ import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; -import org.modularsoft.PlayerHeadHunt.helpers.YamlFileManager; - -import java.io.File; import java.util.*; public class HeadWorldController { private final PlayerHeadHuntMain plugin; - private final YamlFileManager yamlFileManager; public HeadWorldController(PlayerHeadHuntMain plugin) { this.plugin = plugin; - this.yamlFileManager = new YamlFileManager(new File(plugin.getDataFolder(), "player-data.yml")); } public void countHeadsInRegion() { @@ -55,39 +50,11 @@ public void countHeadsInRegion() { } public void playerCollectedHead(Player player, Block block, int x, int y, int z) { - String playerUUID = player.getUniqueId().toString(); - Map data = yamlFileManager.getData(); - Map playerData = (Map) data.get(playerUUID); - - if (playerData == null) { - playerData = new HashMap<>(); - playerData.put("headsCollected", new ArrayList>()); - data.put(playerUUID, playerData); - } - - List> collectedHeads = (List>) playerData.get("headsCollected"); - if (collectedHeads == null) { - collectedHeads = new ArrayList<>(); - playerData.put("headsCollected", collectedHeads); - } - - boolean alreadyCollected = collectedHeads.stream().anyMatch(head -> - head.get("x") == x && head.get("y") == y && head.get("z") == z); - - if (alreadyCollected) { - player.sendMessage(plugin.config().getLangHeadAlreadyFound()); - return; - } - - collectedHeads.add(Map.of("x", x, "y", y, "z", z)); - yamlFileManager.save(); - - // Increment the player's head count + // Record the collection via HeadQuery (single source of truth for player data) plugin.getHeadQuery().insertCollectedHead(player, x, y, z); Material blockType = block.getType(); BlockData blockData = block.getBlockData(); - int headRespawnTimer = plugin.config().getHeadRespawnTimer(); breakBlock(x, y, z); diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/PlayerHeadHuntMain.java b/src/main/java/org/modularsoft/PlayerHeadHunt/PlayerHeadHuntMain.java index 1096f6e..4b95600 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/PlayerHeadHuntMain.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/PlayerHeadHuntMain.java @@ -4,6 +4,7 @@ import org.modularsoft.PlayerHeadHunt.commands.*; import org.modularsoft.PlayerHeadHunt.events.HeadFindEvent; import org.modularsoft.PlayerHeadHunt.events.HeadHatOnHead; +import org.modularsoft.PlayerHeadHunt.events.HeadHunterFlightControl; import org.modularsoft.PlayerHeadHunt.events.HeadHunterOnJoin; import org.bukkit.ChatColor; import org.bukkit.command.ConsoleCommandSender; @@ -27,8 +28,12 @@ public PluginConfig config() { @Override public void onEnable() { - // Generate configuration file + // Write default config.yml if it doesn't exist on disk, then enable + // in-memory fallback to bundled defaults for any key not present in + // the server's config.yml. Do NOT call saveConfig() here — merging + // new structured defaults into an old config corrupts the YAML file. saveDefaultConfig(); + getConfig().options().copyDefaults(true); config = new PluginConfig(this); console = getServer().getConsoleSender(); @@ -40,15 +45,23 @@ public void onEnable() { HeadHatController headHatController = new HeadHatController(this); HeadScoreboardController headScoreboardController = new HeadScoreboardController(this); + // Log loaded player and head statistics + int playerCount = headQuery.getLoadedPlayerCount(); + int totalCollected = headQuery.getTotalHeadsCollectedAcrossAllPlayers(); + console.sendMessage(ChatColor.GREEN + "Loaded " + playerCount + " player profile(s) from storage."); + console.sendMessage(ChatColor.GREEN + "Total heads collected across all players: " + totalCollected); + // Do an initial calculation of the number of heads. This can be // manually recalculated with the relevant command. headWorldController.countHeadsInRegion(); // Plugin Event Register + HeadHunterFlightControl flightControl = new HeadHunterFlightControl(this); PluginManager pluginmanager = getServer().getPluginManager(); pluginmanager.registerEvents(new HeadFindEvent(this, headWorldController, headChatController, headHatController, headScoreboardController, headQuery), this); - pluginmanager.registerEvents(new HeadHunterOnJoin(this, headChatController, headScoreboardController, headQuery), this); + pluginmanager.registerEvents(new HeadHunterOnJoin(this, headChatController, headScoreboardController, headQuery, flightControl), this); pluginmanager.registerEvents(new HeadHatOnHead(), this); + pluginmanager.registerEvents(flightControl, this); // Command Registry Objects.requireNonNull(getCommand("heads")).setExecutor(new heads(this, headChatController)); @@ -69,7 +82,7 @@ public void onEnable() { @Override public void onDisable() { - // Plugin Shutdown Message - console.sendMessage(ChatColor.RED + getDescription().getName() + " is now disabled."); + if (console != null) + console.sendMessage(ChatColor.RED + getDescription().getName() + " is now disabled."); } } diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/PluginConfig.java b/src/main/java/org/modularsoft/PlayerHeadHunt/PluginConfig.java index 2ed8059..6509639 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/PluginConfig.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/PluginConfig.java @@ -16,6 +16,7 @@ public class PluginConfig { @Getter private final boolean milestoneHatFeatureEnabled; @Getter private final boolean milestoneMessageFeatureEnabled; + @Getter private final boolean flightDisabledFeatureEnabled; @Getter private final String headBlock; @Getter private final int headRespawnTimer; @@ -28,6 +29,10 @@ public class PluginConfig { @Getter private final Map headMilestones; + // Raw percentage templates parsed once from config; counts derived at runtime + private record MilestoneTemplate(double percentage, boolean isMajor, Material helmet) {} + private final List milestoneTemplates; + @Getter private final String langDatabaseConnectionError; @Getter private final String langDatabaseConnectionSuccess; @Getter private final String langNotAPlayer; @@ -43,6 +48,8 @@ public class PluginConfig { @Getter private final String langLastHeadFound; @Getter private final String langHeadCount; @Getter private final String langHeadCollectionMilestoneReached; + @Getter private final String langAllHeadsCollected; + @Getter private final String langFlightDisabled; @Getter private final String langLeaderboardNoHeads; @Getter private final String langLeaderboardHeader; @@ -57,6 +64,7 @@ public PluginConfig(PlayerHeadHuntMain plugin) { milestoneHatFeatureEnabled = config.getBoolean("FEATURE.MILESTONEHAT"); milestoneMessageFeatureEnabled = config.getBoolean("FEATURE.MILESTONEMESSAGE"); + flightDisabledFeatureEnabled = config.getBoolean("FEATURE.DISABLEFLIGHT"); headBlock = config.getString("HEAD.HEADBLOCK"); headRespawnTimer = config.getInt("HEAD.RESPAWNTIMER"); @@ -71,40 +79,88 @@ public PluginConfig(PlayerHeadHuntMain plugin) { minorCollectionSound = Sound.valueOf(config.getString("SOUND.MINORCOLLECTIONMILESTONE")); majorCollectionSound = Sound.valueOf(config.getString("SOUND.MAJORCOLLECTIONMILESTONE")); + milestoneTemplates = new ArrayList<>(); + for (Object entry : config.getList("MILESTONES.MINOR", Collections.emptyList())) + parseMilestoneTemplate(entry, false); + for (Object entry : config.getList("MILESTONES.MAJOR", Collections.emptyList())) + parseMilestoneTemplate(entry, true); + headMilestones = new HashMap<>(); - for (Integer minor : config.getIntegerList("MILESTONES.MINOR")) - headMilestones.put(minor, new HeadMileStone(minor, false)); - for (Integer minor : config.getIntegerList("MILESTONES.MAJOR")) - headMilestones.put(minor, new HeadMileStone(minor, true)); - headMilestones.get(config.getInt("MILESTONES.LEATHERHELMET")).setHelmet(Material.LEATHER_HELMET); - headMilestones.get(config.getInt("MILESTONES.CHAINMAILHELMET")).setHelmet(Material.CHAINMAIL_HELMET); - headMilestones.get(config.getInt("MILESTONES.IRONHELMET")).setHelmet(Material.IRON_HELMET); - headMilestones.get(config.getInt("MILESTONES.GOLDENHELMET")).setHelmet(Material.GOLDEN_HELMET); - headMilestones.get(config.getInt("MILESTONES.DIAMONDHELMET")).setHelmet(Material.DIAMOND_HELMET); - headMilestones.get(config.getInt("MILESTONES.NETHERITEHELMET")).setHelmet(Material.NETHERITE_HELMET); - - langDatabaseConnectionError = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.DATABASE.CONNECTIONERROR"))); - langDatabaseConnectionSuccess = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.DATABASE.CONNECTIONSUCCESS"))); - langNotAPlayer = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.COMMAND.NOTAPLAYER"))); - langInsufficientPermissions = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.COMMAND.INSUFFICENTPERMISSIONS"))); - langCommandIncomplete = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.COMMAND.COMMANDINCOMPLETE"))); - langHeadFound = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.HEADFOUND"))); - langHeadAlreadyFound = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.HEADALREADYFOUND"))); - langHeadFirstFinder = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.FIRSTFINDER"))); - langHeadFirstFinderStill = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.FIRSTFINDERSTILL"))); - langHeadNotFirstFinderSingle = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.NOTFIRSTFINDERSINGLE"))); - langHeadNotFirstFinderMultiple = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.NOTFIRSTFINDERMULTIPLE"))); - langFirstHeadFound = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.FIRSTHEADFOUND"))); - langLastHeadFound = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.LASTHEADFOUND"))); - langHeadCount = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.HEADCOUNT"))); - langHeadCollectionMilestoneReached = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.HEAD.HEADCOLLECTIONMILESTONEREACHED"))); - - langLeaderboardNoHeads = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.LEADERBOARD.NOHEADS"))); - langLeaderboardHeader = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.LEADERBOARD.HEADER"))); - langLeaderboardFirstColour = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.LEADERBOARD.FIRSTCOLOUR"))); - langLeaderboardSecondColour = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.LEADERBOARD.SECONDCOLOUR"))); - langLeaderboardThirdColour = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.LEADERBOARD.THIRDCOLOUR"))); - langLeaderboardFormat = ChatColor.translateAlternateColorCodes('&', Objects.requireNonNull(config.getString("LANG.LEADERBOARD.FORMAT"))); + recomputeMilestones(getTotalHeads()); + + langDatabaseConnectionError = lang("LANG.DATABASE.CONNECTIONERROR"); + langDatabaseConnectionSuccess = lang("LANG.DATABASE.CONNECTIONSUCCESS"); + langNotAPlayer = lang("LANG.COMMAND.NOTAPLAYER"); + langInsufficientPermissions = lang("LANG.COMMAND.INSUFFICENTPERMISSIONS"); + langCommandIncomplete = lang("LANG.COMMAND.COMMANDINCOMPLETE"); + langHeadFound = lang("LANG.HEAD.HEADFOUND"); + langHeadAlreadyFound = lang("LANG.HEAD.HEADALREADYFOUND"); + langHeadFirstFinder = lang("LANG.HEAD.FIRSTFINDER"); + langHeadFirstFinderStill = lang("LANG.HEAD.FIRSTFINDERSTILL"); + langHeadNotFirstFinderSingle = lang("LANG.HEAD.NOTFIRSTFINDERSINGLE"); + langHeadNotFirstFinderMultiple = lang("LANG.HEAD.NOTFIRSTFINDERMULTIPLE"); + langFirstHeadFound = lang("LANG.HEAD.FIRSTHEADFOUND"); + langLastHeadFound = lang("LANG.HEAD.LASTHEADFOUND"); + langHeadCount = lang("LANG.HEAD.HEADCOUNT"); + langHeadCollectionMilestoneReached = lang("LANG.HEAD.HEADCOLLECTIONMILESTONEREACHED"); + langAllHeadsCollected = lang("LANG.HEAD.ALLHEADSCOLLECTED"); + langFlightDisabled = lang("LANG.FLIGHT.DISABLED"); + + langLeaderboardNoHeads = lang("LANG.LEADERBOARD.NOHEADS"); + langLeaderboardHeader = lang("LANG.LEADERBOARD.HEADER"); + langLeaderboardFirstColour = lang("LANG.LEADERBOARD.FIRSTCOLOUR"); + langLeaderboardSecondColour = lang("LANG.LEADERBOARD.SECONDCOLOUR"); + langLeaderboardThirdColour = lang("LANG.LEADERBOARD.THIRDCOLOUR"); + langLeaderboardFormat = lang("LANG.LEADERBOARD.FORMAT"); + } + + /** Reads a lang string, falling back to the bundled default if the key is absent or the file is corrupt. */ + private String lang(String path) { + String value = config.getString(path); // falls through to bundled defaults automatically + if (value == null) { + plugin.getLogger().warning("Missing config key: " + path + " — using empty string. Delete config.yml to regenerate."); + return ""; + } + return ChatColor.translateAlternateColorCodes('&', value); + } + + private void parseMilestoneTemplate(Object entry, boolean isMajor) { + double percentage; + Material helmet = null; + + if (entry instanceof Number num) { + percentage = num.doubleValue(); + } else if (entry instanceof Map map) { + Object pctObj = map.get("percentage"); + if (!(pctObj instanceof Number pctNum)) return; + percentage = pctNum.doubleValue(); + Object helmetStr = map.get("helmet"); + if (helmetStr instanceof String name) { + try { + helmet = Material.valueOf(name.toUpperCase()); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Unknown helmet material in milestones config: " + name); + } + } + } else { + return; + } + + milestoneTemplates.add(new MilestoneTemplate(percentage, isMajor, helmet)); + } + + public void recomputeMilestones(int totalHeads) { + headMilestones.clear(); + if (totalHeads <= 0) return; + + for (MilestoneTemplate template : milestoneTemplates) { + int count = Math.max(1, (int) Math.round(template.percentage() / 100.0 * totalHeads)); + HeadMileStone milestone = new HeadMileStone(count, template.isMajor()); + if (template.helmet() != null) milestone.setHelmet(template.helmet()); + headMilestones.put(count, milestone); + } + + plugin.getLogger().info("Milestones recalculated for " + totalHeads + " total heads: " + headMilestones.keySet().stream().sorted().toList()); } public void save() { @@ -113,6 +169,7 @@ public void save() { public void setTotalHeads(int totalHeads) { config.set("HEAD.HEADTOTAL", totalHeads); + recomputeMilestones(totalHeads); } public int getTotalHeads() { diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/commands/debugheadhunt.java b/src/main/java/org/modularsoft/PlayerHeadHunt/commands/debugheadhunt.java index 0f8606b..071db89 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/commands/debugheadhunt.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/commands/debugheadhunt.java @@ -40,19 +40,20 @@ public debugheadhunt(PlayerHeadHuntMain plugin, @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { - if (!sender.hasPermission("playerheadhunt.debug") || !sender.isOp()) { + if (!sender.hasPermission("playerheadhunt.debug") && !sender.isOp()) { sender.sendMessage(plugin.config().getLangInsufficientPermissions()); return true; } if (args.length == 0) { - sender.sendMessage("Usage: /debugheadhunt "); + sender.sendMessage("Usage: /debugheadhunt "); return true; } switch (args[0].toLowerCase()) { case "clearheads" -> { - if (sender instanceof Player player) { + if (sender instanceof Player player && args.length == 1) { + // Player clearing their own heads if (!headQuery.clearHeads(player)) { sender.sendMessage("No heads to clear."); return true; @@ -61,8 +62,22 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @N headHatController.clearHelmet(player); scoreboardController.reloadScoreboard(player, headQuery.foundHeadsCount(player)); sender.sendMessage("Heads cleared successfully."); + } else if (args.length >= 2) { + // Clear heads for a named player (works from console or player) + String targetName = args[1]; + if (!headQuery.clearHeadsByName(targetName)) { + sender.sendMessage("No data found for player: " + targetName); + return true; + } + sender.sendMessage("Heads cleared for player: " + targetName); + // If the target is online, update their scoreboard and helmet too + org.bukkit.entity.Player online = plugin.getServer().getPlayerExact(targetName); + if (online != null) { + headHatController.clearHelmet(online); + scoreboardController.reloadScoreboard(online, 0); + } } else { - sender.sendMessage("The 'clearheads' command can only be executed by a player."); + sender.sendMessage("Usage: /debugheadhunt clearheads [player]"); } } case "countheads" -> { diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/commands/heads.java b/src/main/java/org/modularsoft/PlayerHeadHunt/commands/heads.java index 0b31f53..2dd5268 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/commands/heads.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/commands/heads.java @@ -1,5 +1,6 @@ package org.modularsoft.PlayerHeadHunt.commands; +import org.modularsoft.PlayerHeadHunt.HeadQuery; import org.modularsoft.PlayerHeadHunt.PlayerHeadHuntMain; import org.modularsoft.PlayerHeadHunt.HeadChatController; import org.bukkit.command.Command; @@ -11,14 +12,24 @@ public class heads implements CommandExecutor { private final PlayerHeadHuntMain plugin; private final HeadChatController headChatController; + private final HeadQuery headQuery; public heads(PlayerHeadHuntMain plugin, HeadChatController headChatController) { this.plugin = plugin; this.headChatController = headChatController; + this.headQuery = plugin.getHeadQuery(); } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String s, String[] args) { + if (args.length >= 1) { + // /heads — any sender can look up a specific player + String targetName = args[0]; + int count = headQuery.foundHeadsCountByName(targetName); + headChatController.targetPlayerHeadCountResponse(sender, targetName, count); + return true; + } + if (!(sender instanceof Player player)) { sender.sendMessage(plugin.config().getLangNotAPlayer()); return true; @@ -27,5 +38,4 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @N headChatController.playersOwnHeadCountResponse(player); return true; } - } diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/commands/leaderboard.java b/src/main/java/org/modularsoft/PlayerHeadHunt/commands/leaderboard.java index 93bb5d8..8931bd1 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/commands/leaderboard.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/commands/leaderboard.java @@ -36,10 +36,15 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @N } else { // Handle the command for the console headQuery.getBestHunters(5).thenAccept(bestHunters -> { - // Log the leaderboard to the console - plugin.getServer().getConsoleSender().sendMessage("Top 5 Hunters:"); - for (int i = 0; i < bestHunters.size(); i++) { - plugin.getServer().getConsoleSender().sendMessage((i + 1) + ". " + bestHunters.get(i)); + plugin.getServer().getConsoleSender().sendMessage("=== Top 5 Head Hunters ==="); + if (bestHunters.isEmpty()) { + plugin.getServer().getConsoleSender().sendMessage("No hunters found."); + } else { + for (int i = 0; i < bestHunters.size(); i++) { + HeadQuery.HeadHunter hunter = bestHunters.get(i); + plugin.getServer().getConsoleSender().sendMessage( + (i + 1) + ". " + hunter.name() + " - " + hunter.headsCollected() + " heads"); + } } }); } diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadFindEvent.java b/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadFindEvent.java index 7b8a793..1ca4943 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadFindEvent.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadFindEvent.java @@ -47,10 +47,18 @@ public void onHeadFind(PlayerInteractEvent event) { int y = block.getY(); int z = block.getZ(); + // Check if the player has already collected all available heads + int foundHeadsBeforeCollection = headQuery.foundHeadsCount(player); + int totalHeads = plugin.config().getTotalHeads(); + if (totalHeads > 0 && foundHeadsBeforeCollection >= totalHeads) { + player.sendMessage(plugin.config().getLangAllHeadsCollected()); + return; + } + // Check if the head has already been collected if (headQuery.hasAlreadyCollectedHead(player, x, y, z)) { // Send the "head already found" message and stop further processing - headChatController.headFoundResponse(player, true, headQuery.foundHeadsCount(player), x, y, z); + headChatController.headFoundResponse(player, true, foundHeadsBeforeCollection, x, y, z); return; // Ensure no further processing occurs } diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterFlightControl.java b/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterFlightControl.java new file mode 100644 index 0000000..7e5d7a1 --- /dev/null +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterFlightControl.java @@ -0,0 +1,48 @@ +package org.modularsoft.PlayerHeadHunt.events; + +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerToggleFlightEvent; +import org.modularsoft.PlayerHeadHunt.PlayerHeadHuntMain; + +public class HeadHunterFlightControl implements Listener { + private final PlayerHeadHuntMain plugin; + + public HeadHunterFlightControl(PlayerHeadHuntMain plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onFlightToggle(PlayerToggleFlightEvent event) { + if (!plugin.config().isFlightDisabledFeatureEnabled()) return; + if (!event.isFlying()) return; // allow toggling flight off + + Player player = event.getPlayer(); + if (isExempt(player)) return; + + event.setCancelled(true); + player.setAllowFlight(false); + player.sendMessage(plugin.config().getLangFlightDisabled()); + } + + /** + * Strips flight from a player, overriding any previous grant. + * Safe to call at any time (join, teleport, etc.). + */ + public void enforceFlight(Player player) { + if (!plugin.config().isFlightDisabledFeatureEnabled()) return; + if (isExempt(player)) return; + player.setFlying(false); + player.setAllowFlight(false); + } + + private boolean isExempt(Player player) { + return player.isOp() + || player.hasPermission("playerheadhunt.flight.exempt") + || player.getGameMode() == GameMode.CREATIVE + || player.getGameMode() == GameMode.SPECTATOR; + } +} diff --git a/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterOnJoin.java b/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterOnJoin.java index 074ecf7..b8f5898 100644 --- a/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterOnJoin.java +++ b/src/main/java/org/modularsoft/PlayerHeadHunt/events/HeadHunterOnJoin.java @@ -8,21 +8,25 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; +import org.modularsoft.PlayerHeadHunt.events.HeadHunterFlightControl; public class HeadHunterOnJoin implements Listener { private final PlayerHeadHuntMain plugin; private final HeadChatController headChatController; private final HeadScoreboardController headScoreboardController; - private final HeadQuery headQuery; // Add HeadQuery instance + private final HeadQuery headQuery; + private final HeadHunterFlightControl flightControl; public HeadHunterOnJoin(PlayerHeadHuntMain plugin, HeadChatController headChatController, HeadScoreboardController headScoreboardController, - HeadQuery headQuery) { + HeadQuery headQuery, + HeadHunterFlightControl flightControl) { this.plugin = plugin; this.headChatController = headChatController; this.headScoreboardController = headScoreboardController; - this.headQuery = headQuery; // Initialize HeadQuery + this.headQuery = headQuery; + this.flightControl = flightControl; } @EventHandler @@ -30,8 +34,8 @@ public void onHeadHunterJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); String username = player.getName(); - // Use the instance of HeadQuery to call the method headScoreboardController.reloadScoreboard(player, headQuery.foundHeadsCount(player)); + flightControl.enforceFlight(player); if (headQuery.addNewHunter(player)) { // New player joined diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 8b039b3..41e1731 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -11,6 +11,7 @@ FEATURE: MILESTONEHAT: TRUE MILESTONEMESSAGE: TRUE LEADERBOARDDAILYWEBHOOK: TRUE + DISABLEFLIGHT: TRUE DISCORD: WEBHOOKURL: "https://discord.com/api/webhooks/000000000000000000/000000000000000000" HOUR: 7 @@ -19,7 +20,7 @@ HEAD: HEADTOTAL: HEADBLOCK: PLAYER_HEAD RESPAWNTIMER: 1200 # Default is 1200 (1 minute) - SKINSMAX: 9 + SKINSMAX: 10 SKINS: 0: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOGM1MGFlZTg4MDEzZThmYWY0MjdlMTlmM2I4OTgyOGI4NmJiZjAzZGQyZjE3YzRjNzYwZDFkZGUyMmRlMyJ9fX0=" 1: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzc2NTk1ZWZmY2M1NjI3ZTg1YjE0YzljODgyNDY3MWI1ZWMyOTY1NjU5YzhjNDE3ODQ5YTY2Nzg3OGZhNDkwIn19fQ==" @@ -37,29 +38,33 @@ SOUND: MINORCOLLECTIONMILESTONE: "ENTITY_PLAYER_LEVELUP" MAJORCOLLECTIONMILESTONE: "UI_TOAST_CHALLENGE_COMPLETE" MILESTONES: + # Milestone counts are derived automatically from the total egg count. + # Each entry is either a plain percentage (e.g. 8.0 = 8% of total) or a + # map with a 'percentage' and optional 'helmet' (any Bukkit Material name). + # MINOR uses the minor sound; MAJOR uses the major sound and a broadcast. MINOR: - - 10 - - 25 - - 50 - - 75 - - 100 - - 200 - - 300 - - 400 - - 500 - - 600 + - 1.6 + - 4.0 + - percentage: 8.0 + helmet: LEATHER_HELMET + - percentage: 12.0 + helmet: COPPER_HELMET + - percentage: 16.0 + helmet: CHAINMAIL_HELMET + - 32.0 + - 48.0 + - 64.0 + - 80.0 + - 96.0 MAJOR: - - 156 - - 314 - - 468 - - 625 - # These need to match up 1 to 1 with a milestone above - LEATHERHELMET: 25 - CHAINMAILHELMET: 100 - IRONHELMET: 156 - GOLDENHELMET: 314 - DIAMONDHELMET: 468 - NETHERITEHELMET: 625 # Should equal the head count for maximum results. + - percentage: 25.0 + helmet: IRON_HELMET + - percentage: 50.0 + helmet: GOLDEN_HELMET + - percentage: 75.0 + helmet: DIAMOND_HELMET + - percentage: 100.0 + helmet: NETHERITE_HELMET LANG: DATABASE: CONNECTIONERROR: "&cA database error has occurred, please contact an Administrator." @@ -78,6 +83,9 @@ LANG: FIRSTHEADFOUND: "&e&l%PLAYER% &5has found their first egg!" LASTHEADFOUND: "&e&l%PLAYER% &5has found all &e&l%NUMBEROFHEADS% &r&5eggs!" HEADCOUNT: "&eYou have found &l&6%FOUNDHEADS%/%NUMBEROFHEADS% &r&eheads." + ALLHEADSCOLLECTED: "&eYou have already collected all available eggs!" + FLIGHT: + DISABLED: "&cFlight is disabled during the Head Hunt." # Use %PLAYER% to display players name, use %NUMBEROFHEADS% to display number of eggs. HEADCOLLECTIONMILESTONEREACHED: "&e&l%PLAYER% &5has collected &e&l%NUMBEROFHEADS% &r&5eggs!" LEADERBOARD: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b31a5c0..0998832 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,13 +3,13 @@ name: PlayerHeadHunt description: An Player Head Hunt minigame. version: 1.2.0 author: ModularSoft -api-version: 1.19 +api-version: 1.21 depend: [WorldEdit] commands: heads: - description: Grab the amount of heads you have. - usage: /heads + description: Grab the amount of heads you have, or check another player's count. + usage: /heads [player] aliases: [eggs, presents] leaderboard: description: Show the 5 best heads hunters on the Server. @@ -17,10 +17,13 @@ commands: aliases: [lb] debugheadhunt: description: Debug command for developers. - usage: /debugheadhunt + usage: /debugheadhunt aliases: [ dhh ] permissions: playerheadhunt.leaderboard.exempt: description: Allows the player to be exempt from the leaderboard. default: false + playerheadhunt.flight.exempt: + description: Allows the player to fly even when FEATURE.DISABLEFLIGHT is enabled. + default: false