diff --git a/README.md b/README.md index 80bb509..721692d 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ commits to the fight, and defeats every monster the lure draws in. night-only, minimum-height, and terrain rules. - **Lure raids** — an InteractibleFurniture lure opens a join window, then releases a paced wave; defeating the full wave clears the infestation. -- **Committed parties** — leaving the province or losing the whole party fails - the lure, bystanders are warned and damaged, and logged-out players get a - grace period. +- **Committed parties** — a committed player who leaves the province takes + damage until `/lure leave` drops them from the fight. Losing the whole party + fails the lure, bystanders who stay are warned and damaged, and logged-out + players get a grace period. - **Optional spread** — idle infestations can worsen and spread to neighbouring land, including across a single water or sea province. - **Map and persistence** — infestation severity is exported to the diff --git a/src/main/java/net/tfminecraft/infestations/Infestations.java b/src/main/java/net/tfminecraft/infestations/Infestations.java index 9acde4d..2e1c7ad 100644 --- a/src/main/java/net/tfminecraft/infestations/Infestations.java +++ b/src/main/java/net/tfminecraft/infestations/Infestations.java @@ -39,13 +39,8 @@ public void onEnable() { getServer().getPluginManager().registerEvents(infestationManager, this); infestationManager.start(); - var cmd = getCommand("infestation"); - if (cmd != null) { - cmd.setExecutor(commandManager); - cmd.setTabCompleter(commandManager); - } else { - getLogger().severe("Command 'infestation' missing from plugin.yml"); - } + registerCommand("infestation"); + registerCommand("lure"); getLogger().info("Infestations enabled."); } @@ -62,6 +57,16 @@ public InfestationManager getInfestationManager() { return infestationManager; } + private void registerCommand(String name) { + var cmd = getCommand(name); + if (cmd != null) { + cmd.setExecutor(commandManager); + cmd.setTabCompleter(commandManager); + } else { + getLogger().severe("Command '" + name + "' missing from plugin.yml"); + } + } + public boolean reloadAll() { boolean ok = loadConfigs(); if (infestationManager != null) { diff --git a/src/main/java/net/tfminecraft/infestations/command/CommandManager.java b/src/main/java/net/tfminecraft/infestations/command/CommandManager.java index 0ee5d42..040765b 100644 --- a/src/main/java/net/tfminecraft/infestations/command/CommandManager.java +++ b/src/main/java/net/tfminecraft/infestations/command/CommandManager.java @@ -26,6 +26,9 @@ public final class CommandManager implements CommandExecutor, TabCompleter { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (command.getName().equalsIgnoreCase("lure")) { + return handleLure(sender, args); + } if (args.length == 0) { if (!sender.hasPermission("infestations.admin") && !sender.hasPermission("infestations.admin.reload")) { @@ -57,6 +60,21 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + private boolean handleLure(CommandSender sender, String[] args) { + if (args.length != 1 || !args[0].equalsIgnoreCase("leave")) { + sender.sendMessage(Messages.get("lure-usage")); + return true; + } + if (!(sender instanceof Player player)) { + sender.sendMessage(Messages.get("player-only")); + return true; + } + if (!Infestations.plugin.getInfestationManager().leaveLure(player)) { + player.sendMessage(Messages.get("lure-not-in")); + } + return true; + } + private void handleSet(CommandSender sender, String[] args) { if (args.length < 4) { sender.sendMessage(Messages.get("usage")); @@ -155,6 +173,12 @@ private Integer resolveProvince(CommandSender sender, String token) { @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (command.getName().equalsIgnoreCase("lure")) { + if (args.length == 1) { + return prefix(List.of("leave"), args[0]); + } + return List.of(); + } if (args.length == 1) { List options = new ArrayList<>(); if (sender.hasPermission("infestations.admin.reload")) { diff --git a/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java b/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java index 878f3ba..c5fdf8b 100644 --- a/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java +++ b/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java @@ -213,7 +213,7 @@ private void tick() { if (infestation.hasLure()) { LureHologram.tick(infestation); } - tickDeserters(infestation); + tickOutsideCommitted(infestation); } tickSpread(); @@ -560,23 +560,63 @@ private void warnNonJoiners(Infestation infestation) { } } - private void tickDeserters(Infestation infestation) { + private void tickOutsideCommitted(Infestation infestation) { if (tick % 20 != 0 || infestation.getPhase() == LurePhase.NONE) { return; } + String bar = Messages.get("lure-outside-bar"); for (UUID id : List.copyOf(infestation.getCommitted())) { Player player = Bukkit.getPlayer(id); if (player == null || !player.isOnline() || player.isDead()) { continue; } + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + continue; + } if (Provinces.at(player) == infestation.getProvinceId()) { continue; } - failLure(infestation); - return; + actionBar(player, bar); + player.damage(Cache.deserterDamage); + if (tick % 100 == 0) { + player.sendMessage(Messages.get("lure-outside")); + } } } + /** + * Drop a committed player out of every lure. The lure fails only when nobody committed remains. + * + * @return true when the player was committed to at least one lure + */ + public boolean leaveLure(Player player) { + UUID id = player.getUniqueId(); + boolean found = false; + for (Infestation infestation : List.copyOf(byProvince.values())) { + if (!infestation.isCommitted(id)) { + continue; + } + if (!found) { + player.sendMessage(Messages.get("lure-left")); + } + found = true; + boolean inProvince = Provinces.at(player) == infestation.getProvinceId(); + infestation.getCommitted().remove(id); + infestation.getLogoutGraceUntil().remove(id); + infestation.getDeathOnLogin().remove(id); + if (infestation.getPhase() != LurePhase.NONE && !hasLivingOrGrace(infestation)) { + failLure(infestation); + if (!inProvince) { + player.sendMessage(Messages.get("lure-fail")); + } + } + } + if (found) { + save(); + } + return found; + } + private void expireGrace(Infestation infestation, long now) { Iterator> it = infestation.getLogoutGraceUntil().entrySet().iterator(); while (it.hasNext()) { @@ -1044,9 +1084,14 @@ public void onLeaveProvince(PlayerProvinceLeaveEvent event) { if (infestation.getPhase() == LurePhase.NONE) { return; } - if (infestation.isCommitted(event.getPlayer().getUniqueId())) { - failLure(infestation); + Player player = event.getPlayer(); + if (!infestation.isCommitted(player.getUniqueId())) { + return; + } + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + return; } + player.sendMessage(Messages.get("lure-outside")); } @EventHandler diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 994075c..d11ea7b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -30,6 +30,8 @@ lure-spawn-radius: 48 # ambient ring minimum and the lure ring minimum when they are smaller. min-player-distance: 16 logout-grace-seconds: 300 +# Hearts are 2 health. Applied each second to uncommitted players who stay +# after a lure starts, and to committed players who leave the province. deserter-damage: 2 hologram-view-range: 96 diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 6f5cf37..50a7829 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -20,7 +20,12 @@ list-lure-suffix: " #c45c5c[lure]" no-infestation-here: "{prefix}#c45c5cThere is no infestation here. The lure does nothing." lure-already: "{prefix}#c45c5cA lure is already placed in this province." lure-placed: "{prefix}#c9a44aA lure is activating. Leave the province or right-click the lure to join." -lure-join: "{prefix}#7dba7aYou joined the lure. Leave the province and the lure fails." +lure-join: "{prefix}#7dba7aYou joined the lure. Leave the province and you take damage until you run #d4c9ae/lure leave#7dba7a." +lure-outside: "{prefix}#c45c5cYou left the lure's province. Run #d4c9ae/lure leave #c45c5cto stop taking damage." +lure-outside-bar: "#c45c5cRun /lure leave to stop taking damage." +lure-left: "{prefix}#d4c9aeYou left the lure." +lure-not-in: "{prefix}#c45c5cYou are not in a lure." +lure-usage: "{prefix}#d4c9ae/lure leave" lure-already-joined: "{prefix}#d4c9aeYou are already in the lure." lure-highlight: "{prefix}#d4c9aeHighlighted {count} remaining lure mobs for 10s." lure-activating-bar: "#c9a44aLure activating: {seconds}s - leave or right-click to join" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index cf3c295..b708e2e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -11,6 +11,9 @@ commands: usage: /infestation aliases: [infestations] permission: infestations.admin + lure: + description: Leave a lure you have joined + usage: /lure leave permissions: infestations.admin: