diff --git a/README.md b/README.md index eb02cea..a09e593 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,116 @@ # Damage Display + ![Logo](./media/logo.png) -A Minecraft Spigot plugin that offers the possibility to keep track of damage dealt and received. +Spigot plugin that shows combat feedback: floating damage/heal numbers and action bar messages. + +Requires **Java 21+** and a **26.2** Spigot-compatible server (`api-version: 26.2`). + +## Features + +- Floating `TextDisplay` holograms on damage (optional heal holograms) +- Action bar for damage dealt and damage taken +- Uses **final damage** (after armor/resistance), not raw damage +- Configurable `&` color formats, TextDisplay style, and rise motion +- Nearby-player gate, concurrent caps, viewer scoping +- `/dd toggle [actionbar|heals]` per-player preferences (YAML) +- GitHub Actions: PR tests + `v*` tag releases + +## Installation + +1. Download the latest release jar (or build with Maven) +2. Put it in your server `plugins/` folder +3. Restart the server +4. Edit `plugins/DamageDisplay/config.yml` and run `/dd reload` + +## Commands + +| Command | Permission | Description | +|---------|------------|-------------| +| `/dd help` | `damagedisplay.command.help` | List commands you can use | +| `/dd info` | `damagedisplay.command.info` | Show plugin version and config keys | +| `/dd toggle` | `damagedisplay.command.toggle` | Toggle action bar (same as `actionbar`) | +| `/dd toggle actionbar` | `damagedisplay.command.toggle` | Toggle action bar messages | +| `/dd toggle heals` | `damagedisplay.command.toggle` | Toggle heal holograms for yourself | +| `/dd reload` | `damagedisplay.command.reload` | Reload config and player prefs | + +Aliases: `/damagedisplay` + +## Permissions + +| Permission | Default | Description | +|------------|---------|-------------| +| `damagedisplay.enabled` | true | See action bars and holograms | +| `damagedisplay.command.toggle` | true | Use `/dd toggle` | +| `damagedisplay.command.help` | op | Use `/dd help` | +| `damagedisplay.command.info` | op | Use `/dd info` | +| `damagedisplay.command.reload` | op | Use `/dd reload` | + +## Configuration + +`plugins/DamageDisplay/config.yml` (defaults below). Colors use legacy `&` codes. Placeholders: `{damage}`, `{cause}`, `{amount}`. + +```yaml +actionbar: + damageDealt: + enabled: true + format: "&e★ Damage Dealt: &e{damage}" + damageReceived: + enabled: true + format: "&c☠ Damage Taken: &c{damage}&7 | &7{cause}" + +displayText: + enabled: true + lingertime: 10 + nearbyRange: 16 + onlyFromPlayers: false + maxTotal: 64 + maxPerChunk: 8 + viewerScoped: true + format: "&c{damage}" + critFormat: "&6✴{damage}" + billboard: VERTICAL + shadow: true + seeThrough: false + background: true + textOpacity: 255 + scale: 1.0 + motion: + enabled: true + riseBlocks: 0.6 + +heals: + enabled: false + format: "&a+{amount}" +``` + +Player toggles are stored in `plugins/DamageDisplay/player-prefs.yml`. An old `cache.dat` is migrated automatically on first load. + +## Build -# Features -![Damage Example Showcase](./media/damage_01.png) - - Show a [TextDisplay](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/TextDisplay.html) whenever an entity takes damage - - Send out ActionBar messages to players in regard to damage that has been taken. - - Display an ActionBar message whenever a player has dealt an amount of damage to an entity. +```bash +mvn clean package +``` -# Installation -1. Download the latest version of the DamageDisplay plugin -2. Place the downloaded `.jar` file inside of your server's plugins directory -3. Restart your server +Jar: `target/DamageDisplay-.jar` -# Usage -Whenever a player is going to take or deal an amount of damage, an ActionBar message is automatically going to be -shown to the player in question. There is also another case when an entity takes an amount of damage, that damage will -be displayed to anyone looking as a TextDisplay floating near the position that the damage has been taken at. +Tests: `mvn test` -# Permissions +## Releases -### damagedisplay.enabled -- Description: Enables the ActionBar messages for the player -- Default: True +Push a tag on `master`: -### damagedisplay.command.info -- Description: Enables the ActionBar messages for the player -- Default: True +```bash +git tag v1.2.0 +git push origin v1.2.0 +``` -### damagedisplay.command.toggle -- Description: Enables the ActionBar messages for the player -- Default: True +CI runs tests, sets the Maven version from the tag, and publishes the jar on GitHub Releases. -### damagedisplay.command.reload -- Description: Grants permission to /dd reload which will reload the configuration file, so you won't have to reload it -- Default: False +## Support -### damagedisplay.command.help -- Description: Grants permission to /dd help -- Default: False +Bugs and PRs: [GitHub repository](https://github.com/JustKato/DamageDisplay). -# Support & Contribution -In regard to possible bugs that might show up, please open an issue on the [Github Repository](https://github.com/JustKato/DamageDisplay). -Contributions are more than welcome! Fork the repository, make your changes and submit a pull request. +## License -# License -This project is licensed under the [MIT License](./LICENSE) \ No newline at end of file +[MIT License](./LICENSE) diff --git a/pom.xml b/pom.xml index 52eda0e..620a646 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ UTF-8 5.11.4 5.20.0 + 4.21.0 https://danlegt.com @@ -50,6 +51,10 @@ org.bstats com.danlegt.util + + net.kyori + com.danlegt.adventure + @@ -94,6 +99,18 @@ 3.0.2 compile + + net.kyori + adventure-api + ${adventure.version} + compile + + + net.kyori + adventure-text-serializer-legacy + ${adventure.version} + compile + org.junit.jupiter junit-jupiter diff --git a/src/main/java/com/danlegt/damagedisplay/Commands/CommandManager.java b/src/main/java/com/danlegt/damagedisplay/Commands/CommandManager.java index 71a2d16..7904357 100644 --- a/src/main/java/com/danlegt/damagedisplay/Commands/CommandManager.java +++ b/src/main/java/com/danlegt/damagedisplay/Commands/CommandManager.java @@ -1,31 +1,34 @@ package com.danlegt.damagedisplay.Commands; -import com.danlegt.damagedisplay.DamageDisplay; -import org.bukkit.ChatColor; +import com.danlegt.damagedisplay.Services.Messages; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import java.util.ArrayList; import java.util.List; -public class CommandManager implements CommandExecutor { +public class CommandManager implements CommandExecutor, TabCompleter { - public static List registeredCommands = List.of( - new InfoCommand(), - new ToggleCommand(), - new HelpCommand(), - new ReloadCommand() + public static final List registeredCommands = List.of( + new InfoCommand(), + new ToggleCommand(), + new HelpCommand(), + new ReloadCommand() ); + private static final List TOGGLE_OPTIONS = List.of("actionbar", "heals"); + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length == 0) { - sender.sendMessage(ChatColor.RED + "Usage: /dd "); + sender.sendMessage(Messages.toLegacy("&cUsage: /dd ")); return true; } - for ( var cmd: registeredCommands ) { - if ( cmd.getCommandLabel().equalsIgnoreCase(args[0]) ) { + for (var cmd : registeredCommands) { + if (cmd.getCommandLabel().equalsIgnoreCase(args[0])) { return cmd.handleCommand(sender, command, label, args); } } @@ -33,4 +36,32 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return false; } + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + String prefix = args[0].toLowerCase(); + List completions = new ArrayList<>(); + for (DDCommand cmd : registeredCommands) { + String cmdLabel = cmd.getCommandLabel(); + if (cmdLabel.startsWith(prefix) && sender.hasPermission(cmd.getPermission())) { + completions.add(cmdLabel); + } + } + return completions; + } + + if (args.length == 2 && args[0].equalsIgnoreCase("toggle") + && sender.hasPermission("damagedisplay.command.toggle")) { + String prefix = args[1].toLowerCase(); + List completions = new ArrayList<>(); + for (String option : TOGGLE_OPTIONS) { + if (option.startsWith(prefix)) { + completions.add(option); + } + } + return completions; + } + + return List.of(); + } } diff --git a/src/main/java/com/danlegt/damagedisplay/Commands/DDCommand.java b/src/main/java/com/danlegt/damagedisplay/Commands/DDCommand.java index bcac36e..5a18143 100644 --- a/src/main/java/com/danlegt/damagedisplay/Commands/DDCommand.java +++ b/src/main/java/com/danlegt/damagedisplay/Commands/DDCommand.java @@ -5,10 +5,12 @@ public interface DDCommand { - public String getCommandLabel(); + String getCommandLabel(); - public boolean handleCommand(CommandSender sender, Command command, String label, String[] args); + String getPermission(); - public String getDescription(); + boolean handleCommand(CommandSender sender, Command command, String label, String[] args); + + String getDescription(); } diff --git a/src/main/java/com/danlegt/damagedisplay/Commands/HelpCommand.java b/src/main/java/com/danlegt/damagedisplay/Commands/HelpCommand.java index d63c589..bec048b 100644 --- a/src/main/java/com/danlegt/damagedisplay/Commands/HelpCommand.java +++ b/src/main/java/com/danlegt/damagedisplay/Commands/HelpCommand.java @@ -1,6 +1,6 @@ package com.danlegt.damagedisplay.Commands; -import org.bukkit.ChatColor; +import com.danlegt.damagedisplay.Services.Messages; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -10,18 +10,25 @@ public String getCommandLabel() { return "help"; } + @Override + public String getPermission() { + return "damagedisplay.command.help"; + } + @Override public boolean handleCommand(CommandSender sender, Command command, String label, String[] args) { - if ( !sender.hasPermission("damagedisplay.command.help") ) { - sender.sendMessage(ChatColor.RED + "You do not have permission to use this command."); + if (!sender.hasPermission(getPermission())) { + sender.sendMessage(Messages.toLegacy("&cYou do not have permission to use this command.")); return true; } - sender.sendMessage(ChatColor.GRAY + "===================="); - CommandManager.registeredCommands.forEach( cmd -> { - sender.sendMessage(ChatColor.GOLD + "/dd " + cmd.getCommandLabel() + ChatColor.GRAY + " - " + ChatColor.RESET + cmd.getDescription()); + sender.sendMessage(Messages.toLegacy("&8====================")); + CommandManager.registeredCommands.forEach(cmd -> { + if (sender.hasPermission(cmd.getPermission())) { + sender.sendMessage(Messages.toLegacy("&6/dd " + cmd.getCommandLabel() + " &7- &f" + cmd.getDescription())); + } }); - sender.sendMessage(ChatColor.GRAY + "===================="); + sender.sendMessage(Messages.toLegacy("&8====================")); return true; } diff --git a/src/main/java/com/danlegt/damagedisplay/Commands/InfoCommand.java b/src/main/java/com/danlegt/damagedisplay/Commands/InfoCommand.java index 21b68cb..2ad7ba7 100644 --- a/src/main/java/com/danlegt/damagedisplay/Commands/InfoCommand.java +++ b/src/main/java/com/danlegt/damagedisplay/Commands/InfoCommand.java @@ -1,42 +1,42 @@ package com.danlegt.damagedisplay.Commands; import com.danlegt.damagedisplay.DamageDisplay; -import org.bukkit.ChatColor; +import com.danlegt.damagedisplay.Services.Messages; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; -import java.util.List; -import java.util.Objects; - -public class InfoCommand implements DDCommand{ +public class InfoCommand implements DDCommand { @Override public String getCommandLabel() { return "info"; } + @Override + public String getPermission() { + return "damagedisplay.command.info"; + } + @Override public boolean handleCommand(CommandSender sender, Command command, String label, String[] args) { - if ( !sender.hasPermission("damagedisplay.command.info") ) { - sender.sendMessage(ChatColor.RED + "You do not have permission to use this command."); + if (!sender.hasPermission(getPermission())) { + sender.sendMessage(Messages.toLegacy("&cYou do not have permission to use this command.")); return true; } FileConfiguration config = DamageDisplay.me.getConfig(); - sender.sendMessage(ChatColor.GREEN + "DamageDisplay (" + ChatColor.AQUA + DamageDisplay.me.getDescription().getVersion() + ChatColor.GREEN + ")"); + sender.sendMessage(Messages.toLegacy( + "&aDamageDisplay (&b" + DamageDisplay.me.getDescription().getVersion() + "&a)" + )); for (String key : config.getKeys(true)) { - var val = config.get(key); - if ( val == null ) { - val = ""; + Object val = config.get(key); + if (val == null) { + continue; } - if ( val instanceof Boolean v) { - val = v ? "True" : "False"; - } - - if ( val instanceof String || val instanceof Integer || val instanceof Float || val instanceof Double ) { - sender.sendMessage(ChatColor.GOLD + key + ChatColor.GRAY + ": " + ChatColor.WHITE + config.get(key)); + if (val instanceof Boolean || val instanceof String || val instanceof Number) { + sender.sendMessage(Messages.toLegacy("&6" + key + "&7: &f" + val)); } } @@ -47,5 +47,4 @@ public boolean handleCommand(CommandSender sender, Command command, String label public String getDescription() { return "Retrieve information about the plugin"; } - } diff --git a/src/main/java/com/danlegt/damagedisplay/Commands/ReloadCommand.java b/src/main/java/com/danlegt/damagedisplay/Commands/ReloadCommand.java index bd1f824..71cf414 100644 --- a/src/main/java/com/danlegt/damagedisplay/Commands/ReloadCommand.java +++ b/src/main/java/com/danlegt/damagedisplay/Commands/ReloadCommand.java @@ -1,7 +1,7 @@ package com.danlegt.damagedisplay.Commands; import com.danlegt.damagedisplay.DamageDisplay; -import org.bukkit.ChatColor; +import com.danlegt.damagedisplay.Services.Messages; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -11,18 +11,23 @@ public String getCommandLabel() { return "reload"; } + @Override + public String getPermission() { + return "damagedisplay.command.reload"; + } + @Override public boolean handleCommand(CommandSender sender, Command command, String label, String[] args) { - if ( !sender.hasPermission("damagedisplay.command.reload") ) { - sender.sendMessage(ChatColor.RED + "You do not have permission to use this command."); + if (!sender.hasPermission(getPermission())) { + sender.sendMessage(Messages.toLegacy("&cYou do not have permission to use this command.")); return true; } DamageDisplay.me.reloadConfig(); DamageDisplay.me.getSettings().reload(); - ToggleCommand.loadToggleCache(); + DamageDisplay.me.getPlayerPrefs().load(); - sender.sendMessage(ChatColor.GREEN + "Configuration reloaded."); + sender.sendMessage(Messages.toLegacy("&aConfiguration reloaded.")); return true; } diff --git a/src/main/java/com/danlegt/damagedisplay/Commands/ToggleCommand.java b/src/main/java/com/danlegt/damagedisplay/Commands/ToggleCommand.java index 5b8d9aa..8c003ea 100644 --- a/src/main/java/com/danlegt/damagedisplay/Commands/ToggleCommand.java +++ b/src/main/java/com/danlegt/damagedisplay/Commands/ToggleCommand.java @@ -1,87 +1,60 @@ package com.danlegt.damagedisplay.Commands; import com.danlegt.damagedisplay.DamageDisplay; -import org.bukkit.ChatColor; +import com.danlegt.damagedisplay.Services.Messages; +import com.danlegt.damagedisplay.Services.PlayerPrefs; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import java.io.*; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.logging.Level; - public class ToggleCommand implements DDCommand { - public static Set disabledPlayers = new HashSet<>(); - // The file that we store all the player toggle command statuses to - private static final String CACHE_FILE = "cache.dat"; - @Override public String getCommandLabel() { return "toggle"; } + @Override + public String getPermission() { + return "damagedisplay.command.toggle"; + } + @Override public boolean handleCommand(CommandSender sender, Command command, String label, String[] args) { - if ( !sender.hasPermission("damagedisplay.command.toggle") ) { - sender.sendMessage(ChatColor.RED + "You do not have permission to use this command."); + if (!sender.hasPermission(getPermission())) { + sender.sendMessage(Messages.toLegacy("&cYou do not have permission to use this command.")); return true; } - if ( !(sender instanceof Player player) ) { - sender.sendMessage(ChatColor.RED + "Only players can use this command."); + if (!(sender instanceof Player player)) { + sender.sendMessage(Messages.toLegacy("&cOnly players can use this command.")); return true; } - UUID playerUUID = player.getUniqueId(); - if ( ToggleCommand.disabledPlayers.contains(playerUUID) ) { - sender.sendMessage("Action bar messages toggled " + ChatColor.GREEN + "ON"); - ToggleCommand.disabledPlayers.remove(playerUUID); - } else { - sender.sendMessage("Action bar messages toggled " + ChatColor.RED + "OFF"); - ToggleCommand.disabledPlayers.add(playerUUID); - } + String target = args.length >= 2 ? args[1].toLowerCase() : "actionbar"; + PlayerPrefs prefs = DamageDisplay.me.getPlayerPrefs(); - // Update the toggles list - saveToggleCache(); + switch (target) { + case "actionbar" -> { + boolean enabled = prefs.toggleActionBar(player.getUniqueId()); + sender.sendMessage(Messages.toLegacy( + enabled ? "&7Action bar messages toggled &aON" : "&7Action bar messages toggled &cOFF" + )); + } + case "heals" -> { + boolean enabled = prefs.toggleHeals(player.getUniqueId()); + sender.sendMessage(Messages.toLegacy( + enabled ? "&7Heal displays toggled &aON" : "&7Heal displays toggled &cOFF" + )); + } + default -> sender.sendMessage(Messages.toLegacy("&cUsage: /dd toggle [actionbar|heals]")); + } return true; } @Override public String getDescription() { - return "Toggle the actionbar message you receive whenever dealing/taking damage"; + return "Toggle action bar messages or heal displays (/dd toggle [actionbar|heals])"; } - - private static void saveToggleCache() { - File dataFolder = DamageDisplay.me.getDataFolder(); - if (!dataFolder.exists()) { - dataFolder.mkdirs(); - } - File file = new File(dataFolder, CACHE_FILE); - - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) { - oos.writeObject(new ArrayList<>(disabledPlayers)); - } catch (IOException e) { - e.printStackTrace(); - DamageDisplay.me.getLogger().log(Level.WARNING, "Could not save player toggle command cache: " + e.getMessage()); - } - } - - public static void loadToggleCache() { - File file = new File(DamageDisplay.me.getDataFolder(), CACHE_FILE); - if (file.exists()) { - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - disabledPlayers = new HashSet<>((List) ois.readObject()); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - DamageDisplay.me.getLogger().log(Level.WARNING, "Could not load player toggle command cache: " + e.getMessage()); - } - } - } - } diff --git a/src/main/java/com/danlegt/damagedisplay/DamageDisplay.java b/src/main/java/com/danlegt/damagedisplay/DamageDisplay.java index 7a7d4e4..0974538 100644 --- a/src/main/java/com/danlegt/damagedisplay/DamageDisplay.java +++ b/src/main/java/com/danlegt/damagedisplay/DamageDisplay.java @@ -1,11 +1,12 @@ package com.danlegt.damagedisplay; import com.danlegt.damagedisplay.Commands.CommandManager; -import com.danlegt.damagedisplay.Commands.ToggleCommand; import com.danlegt.damagedisplay.Event.EventManager; import com.danlegt.damagedisplay.Services.DisplayTracker; +import com.danlegt.damagedisplay.Services.PlayerPrefs; import com.danlegt.damagedisplay.Services.PluginSettings; import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; import org.bukkit.plugin.java.JavaPlugin; public final class DamageDisplay extends JavaPlugin { @@ -15,25 +16,23 @@ public final class DamageDisplay extends JavaPlugin { private final PluginSettings settings = new PluginSettings(); private final DisplayTracker displayTracker = new DisplayTracker(); + private final PlayerPrefs playerPrefs = new PlayerPrefs(); @Override public void onEnable() { - // Singleton for Services me = this; - // Register Listeners new EventManager(me).registerEvents(); - // bStats Metrics - RegisterMetrics(); - - // Save the default settings if they are not saved this.saveDefaultConfig(); this.settings.reload(); - this.getCommand("dd").setExecutor(new CommandManager()); + this.playerPrefs.load(); + + CommandManager commandManager = new CommandManager(); + this.getCommand("dd").setExecutor(commandManager); + this.getCommand("dd").setTabCompleter(commandManager); - // Load the toggle command cache - ToggleCommand.loadToggleCache(); + registerMetrics(); } @Override @@ -49,8 +48,16 @@ public DisplayTracker getDisplayTracker() { return displayTracker; } - private void RegisterMetrics() { - new Metrics(this, bStatsID); + public PlayerPrefs getPlayerPrefs() { + return playerPrefs; } + private void registerMetrics() { + Metrics metrics = new Metrics(this, bStatsID); + metrics.addCustomChart(new SimplePie("displayText_enabled", () -> Boolean.toString(settings.isDisplayTextEnabled()))); + metrics.addCustomChart(new SimplePie("heals_enabled", () -> Boolean.toString(settings.isHealsEnabled()))); + metrics.addCustomChart(new SimplePie("viewerScoped", () -> Boolean.toString(settings.isViewerScoped()))); + metrics.addCustomChart(new SimplePie("motion_enabled", () -> Boolean.toString(settings.isMotionEnabled()))); + metrics.addCustomChart(new SimplePie("onlyFromPlayers", () -> Boolean.toString(settings.isOnlyFromPlayers()))); + } } diff --git a/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnDamageHandler.java b/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnDamageHandler.java index c99bdad..75d7ca3 100644 --- a/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnDamageHandler.java +++ b/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnDamageHandler.java @@ -1,136 +1,108 @@ package com.danlegt.damagedisplay.Event.EventListeners; -import com.danlegt.damagedisplay.Commands.ToggleCommand; import com.danlegt.damagedisplay.DamageDisplay; import com.danlegt.damagedisplay.Services.ActionBar; -import com.danlegt.damagedisplay.Services.DisplayTracker; +import com.danlegt.damagedisplay.Services.HologramService; +import com.danlegt.damagedisplay.Services.Messages; +import com.danlegt.damagedisplay.Services.PlayerPrefs; import com.danlegt.damagedisplay.Services.PluginSettings; -import net.md_5.bungee.api.ChatColor; import org.bukkit.Location; -import org.bukkit.entity.*; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.Vector; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.ThreadLocalRandom; +import java.util.Map; public class OnDamageHandler implements Listener { - @EventHandler(priority = EventPriority.MONITOR) + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerDamage(EntityDamageByEntityEvent e) { - if (e.isCancelled()) return; - PluginSettings settings = DamageDisplay.me.getSettings(); - if (!settings.isActionBarDamageDealtEnabled()) return; + PlayerPrefs prefs = DamageDisplay.me.getPlayerPrefs(); + if (!settings.isActionBarDamageDealtEnabled()) { + return; + } + + double damage = e.getFinalDamage(); + String formatted = Messages.format(settings.getActionBarDamageDealtFormat(), Map.of( + "damage", Messages.roundDamage(damage) + )); if (e.getDamager().getType() == EntityType.PLAYER) { Player p = (Player) e.getDamager(); - - if (ToggleCommand.disabledPlayers.contains(p.getUniqueId())) return; - - if (!p.hasPermission("damagedisplay.enabled")) + if (!prefs.isActionBarEnabled(p.getUniqueId())) { return; - - ActionBar.sendToPlayer(p, "★ Damage Dealt: " + ChatColor.YELLOW + Math.round(e.getDamage() * 100.0) / 100.0); + } + if (!p.hasPermission("damagedisplay.enabled")) { + return; + } + ActionBar.sendToPlayer(p, formatted); } - if (e.getDamager() instanceof Projectile a) { - if (a.getShooter() instanceof Player p) { - if (!p.hasPermission("damagedisplay.enabled")) - return; - - if (ToggleCommand.disabledPlayers.contains(p.getUniqueId())) return; - - ActionBar.sendToPlayer(p, "★ Damage Dealt: " + ChatColor.YELLOW + Math.round(e.getDamage() * 100.0) / 100.0); + if (e.getDamager() instanceof Projectile projectile && projectile.getShooter() instanceof Player p) { + if (!p.hasPermission("damagedisplay.enabled")) { + return; + } + if (!prefs.isActionBarEnabled(p.getUniqueId())) { + return; } + ActionBar.sendToPlayer(p, formatted); } } - @EventHandler(priority = EventPriority.MONITOR) + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEnvironmentDamage(EntityDamageEvent e) { - if (e.isCancelled()) return; - PluginSettings settings = DamageDisplay.me.getSettings(); - var receivedDamage = (Math.round(e.getDamage() * 100.0) / 100.0); + PlayerPrefs prefs = DamageDisplay.me.getPlayerPrefs(); + double damage = e.getFinalDamage(); + String damageText = Messages.roundDamage(damage); if (e.getEntity().getType() == EntityType.PLAYER && settings.isActionBarDamageReceivedEnabled()) { Player p = (Player) e.getEntity(); - if (!p.hasPermission("damagedisplay.enabled")) - return; - - if (ToggleCommand.disabledPlayers.contains(p.getUniqueId())) return; - - ActionBar.sendToPlayer(p, "☠ Damage Taken: " + ChatColor.RED + receivedDamage + ChatColor.GRAY + " | " + ActionBar.parseStringToPretty(e.getCause())); + if (p.hasPermission("damagedisplay.enabled") && prefs.isActionBarEnabled(p.getUniqueId())) { + String formatted = Messages.format(settings.getActionBarDamageReceivedFormat(), Map.of( + "damage", damageText, + "cause", ActionBar.parseStringToPretty(e.getCause()) + )); + ActionBar.sendToPlayer(p, formatted); + } } if (!settings.isDisplayTextEnabled()) { return; } - Location entityLoc = e.getEntity().getLocation(); - DisplayTracker tracker = DamageDisplay.me.getDisplayTracker(); - if (!tracker.canSpawn(entityLoc.getChunk(), settings)) { - return; - } - if (settings.isOnlyFromPlayers() && !isPlayerCaused(e)) { return; } - double nearbyRange = settings.getNearbyRange(); - Collection nearbyPlayers = Collections.emptyList(); - if (nearbyRange > 0) { - nearbyPlayers = getNearbyPlayers(entityLoc, nearbyRange); - if (nearbyPlayers.isEmpty()) { - return; - } - } - - ThreadLocalRandom rand = ThreadLocalRandom.current(); - String damagePrefix = ChatColor.RED.toString(); - - if (e instanceof EntityDamageByEntityEvent damageByEntityEvent) { - if (damageByEntityEvent.getDamager() instanceof Player damager) { - if (damager.getFallDistance() > 0.0f && !damager.isOnGround() && !damager.isInWater() && !damager.isInsideVehicle() && !damager.hasPotionEffect(PotionEffectType.BLINDNESS)) { - damagePrefix = ChatColor.GOLD + "✴"; - } - } + Location entityLoc = e.getEntity().getLocation(); + Collection viewers = HologramService.getPermittedNearbyPlayers(entityLoc, settings.getNearbyRange()); + if (viewers.isEmpty()) { + return; } - Location loc = entityLoc.clone(); - loc.add(new Vector(.05 * (rand.nextBoolean() ? -1 : 0), 1.5f, .05 * (rand.nextBoolean() ? -1 : 0))); - - TextDisplay textDisplay = e.getEntity().getWorld().spawn(loc, TextDisplay.class); - textDisplay.setText(damagePrefix + receivedDamage); - textDisplay.setBillboard(Display.Billboard.VERTICAL); - - if (settings.isViewerScoped()) { - textDisplay.setVisibleByDefault(false); - Collection viewers = nearbyRange > 0 - ? nearbyPlayers - : getNearbyPlayers(loc, 16); - for (Player viewer : viewers) { - viewer.showEntity(DamageDisplay.me, textDisplay); - } + boolean crit = false; + if (e instanceof EntityDamageByEntityEvent damageByEntityEvent + && damageByEntityEvent.getDamager() instanceof Player damager) { + crit = damager.getFallDistance() > 0.0f + && !damager.isOnGround() + && !damager.isInWater() + && !damager.isInsideVehicle() + && !damager.hasPotionEffect(PotionEffectType.BLINDNESS); } - tracker.trackAndScheduleRemoval(textDisplay, settings.getLingerTime()); - } - - private static Collection getNearbyPlayers(Location location, double range) { - Collection nearby = location.getWorld().getNearbyEntities(location, range, range, range, entity -> entity instanceof Player); - Collection players = new ArrayList<>(nearby.size()); - for (Entity entity : nearby) { - players.add((Player) entity); - } - return players; + String template = crit ? settings.getCritFormat() : settings.getDisplayFormat(); + String legacy = Messages.formatLegacy(template, Map.of("damage", damageText)); + HologramService.spawn(entityLoc, legacy, viewers, settings); } private static boolean isPlayerCaused(EntityDamageEvent e) { diff --git a/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnHealHandler.java b/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnHealHandler.java new file mode 100644 index 0000000..c94f470 --- /dev/null +++ b/src/main/java/com/danlegt/damagedisplay/Event/EventListeners/OnHealHandler.java @@ -0,0 +1,52 @@ +package com.danlegt.damagedisplay.Event.EventListeners; + +import com.danlegt.damagedisplay.DamageDisplay; +import com.danlegt.damagedisplay.Services.HologramService; +import com.danlegt.damagedisplay.Services.Messages; +import com.danlegt.damagedisplay.Services.PlayerPrefs; +import com.danlegt.damagedisplay.Services.PluginSettings; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityRegainHealthEvent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +public class OnHealHandler implements Listener { + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onHeal(EntityRegainHealthEvent e) { + PluginSettings settings = DamageDisplay.me.getSettings(); + if (!settings.isHealsEnabled()) { + return; + } + + double amount = e.getAmount(); + if (amount <= 0) { + return; + } + + Location loc = e.getEntity().getLocation(); + Collection nearby = HologramService.getPermittedNearbyPlayers(loc, settings.getNearbyRange()); + PlayerPrefs prefs = DamageDisplay.me.getPlayerPrefs(); + + Collection viewers = new ArrayList<>(); + for (Player player : nearby) { + if (prefs.isHealsEnabled(player.getUniqueId())) { + viewers.add(player); + } + } + if (viewers.isEmpty()) { + return; + } + + String legacy = Messages.formatLegacy(settings.getHealFormat(), Map.of( + "amount", Messages.roundDamage(amount) + )); + HologramService.spawn(loc, legacy, viewers, settings, true); + } +} diff --git a/src/main/java/com/danlegt/damagedisplay/Event/EventManager.java b/src/main/java/com/danlegt/damagedisplay/Event/EventManager.java index 2a1c811..7094a0e 100644 --- a/src/main/java/com/danlegt/damagedisplay/Event/EventManager.java +++ b/src/main/java/com/danlegt/damagedisplay/Event/EventManager.java @@ -2,6 +2,7 @@ import com.danlegt.damagedisplay.DamageDisplay; import com.danlegt.damagedisplay.Event.EventListeners.OnDamageHandler; +import com.danlegt.damagedisplay.Event.EventListeners.OnHealHandler; import org.bukkit.event.Listener; import java.util.List; @@ -9,20 +10,17 @@ public class EventManager { private final DamageDisplay plugin; private final List enabledListeners = List.of( - new OnDamageHandler() + new OnDamageHandler(), + new OnHealHandler() ); - public EventManager (DamageDisplay plugin) { + public EventManager(DamageDisplay plugin) { this.plugin = plugin; } - /** - * Register the currently enabled listeners - */ public void registerEvents() { - for ( var ev: enabledListeners ) { + for (var ev : enabledListeners) { plugin.getServer().getPluginManager().registerEvents(ev, plugin); } } - } diff --git a/src/main/java/com/danlegt/damagedisplay/Services/ActionBar.java b/src/main/java/com/danlegt/damagedisplay/Services/ActionBar.java index 506ff9a..9803488 100644 --- a/src/main/java/com/danlegt/damagedisplay/Services/ActionBar.java +++ b/src/main/java/com/danlegt/damagedisplay/Services/ActionBar.java @@ -12,8 +12,9 @@ public class ActionBar { private static final Map PRETTY_CAUSES = buildPrettyCauses(); - public static void sendToPlayer(Player p, String msg) { - p.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(msg)); + public static void sendToPlayer(Player player, String ampersandMessage) { + String legacy = Messages.toLegacy(ampersandMessage); + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(legacy)); } public static String parseStringToPretty(EntityDamageEvent.DamageCause cause) { @@ -28,11 +29,10 @@ public static String parseStringToPretty(String input) { for (int i = 0; i < words.length; i++) { words[i] = words[i].toLowerCase(); words[i] = words[i].substring(0, 1).toUpperCase() + words[i].substring(1); - output.append(words[i]).append(" "); } - return output.toString(); + return output.toString().trim(); } private static Map buildPrettyCauses() { diff --git a/src/main/java/com/danlegt/damagedisplay/Services/DisplayTracker.java b/src/main/java/com/danlegt/damagedisplay/Services/DisplayTracker.java index 0799e76..7245d14 100644 --- a/src/main/java/com/danlegt/damagedisplay/Services/DisplayTracker.java +++ b/src/main/java/com/danlegt/damagedisplay/Services/DisplayTracker.java @@ -2,8 +2,12 @@ import com.danlegt.damagedisplay.DamageDisplay; import org.bukkit.Chunk; +import org.bukkit.Location; import org.bukkit.entity.TextDisplay; import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.Transformation; +import org.joml.AxisAngle4f; +import org.joml.Vector3f; import java.util.HashMap; import java.util.Iterator; @@ -39,8 +43,55 @@ public void track(TextDisplay display) { perChunkCounts.merge(key, 1, Integer::sum); } - public void trackAndScheduleRemoval(TextDisplay display, int lingerTime) { + public void applyStyle(TextDisplay display, PluginSettings settings) { + display.setBillboard(settings.getBillboard()); + display.setShadowed(settings.isShadow()); + display.setSeeThrough(settings.isSeeThrough()); + display.setDefaultBackground(settings.isBackground()); + display.setTextOpacity(settings.getTextOpacity()); + + float scale = settings.getScale(); + display.setTransformation(new Transformation( + new Vector3f(), + new AxisAngle4f(), + new Vector3f(scale, scale, scale), + new AxisAngle4f() + )); + } + + public void trackAndScheduleRemoval(TextDisplay display, PluginSettings settings) { track(display); + int lingerTime = settings.getLingerTime(); + + if (settings.isMotionEnabled() && settings.getRiseBlocks() > 0 && lingerTime > 1) { + Location start = display.getLocation().clone(); + double rise = settings.getRiseBlocks(); + + new BukkitRunnable() { + private int tick = 0; + + @Override + public void run() { + if (!display.isValid()) { + cancel(); + remove(display); + return; + } + + tick++; + if (tick >= lingerTime) { + cancel(); + remove(display); + return; + } + + double progress = (double) tick / lingerTime; + Location next = start.clone().add(0, rise * progress, 0); + display.teleport(next); + } + }.runTaskTimer(DamageDisplay.me, 1L, 1L); + return; + } new BukkitRunnable() { @Override diff --git a/src/main/java/com/danlegt/damagedisplay/Services/HologramService.java b/src/main/java/com/danlegt/damagedisplay/Services/HologramService.java new file mode 100644 index 0000000..64b5f4d --- /dev/null +++ b/src/main/java/com/danlegt/damagedisplay/Services/HologramService.java @@ -0,0 +1,72 @@ +package com.danlegt.damagedisplay.Services; + +import com.danlegt.damagedisplay.DamageDisplay; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.entity.TextDisplay; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public final class HologramService { + + private HologramService() { + } + + public static Collection getPermittedNearbyPlayers(Location location, double range) { + Collection nearby; + if (range > 0) { + nearby = location.getWorld().getNearbyEntities(location, range, range, range, entity -> entity instanceof Player); + } else { + nearby = new ArrayList<>(location.getWorld().getPlayers()); + } + + List players = new ArrayList<>(); + for (Entity entity : nearby) { + Player player = (Player) entity; + if (player.hasPermission("damagedisplay.enabled")) { + players.add(player); + } + } + return players; + } + + public static TextDisplay spawn(Location at, String legacyText, Collection viewers, PluginSettings settings) { + return spawn(at, legacyText, viewers, settings, false); + } + + public static TextDisplay spawn(Location at, String legacyText, Collection viewers, PluginSettings settings, boolean forceViewerScoped) { + DisplayTracker tracker = DamageDisplay.me.getDisplayTracker(); + if (!tracker.canSpawn(at.getChunk(), settings)) { + return null; + } + if (viewers.isEmpty()) { + return null; + } + + ThreadLocalRandom rand = ThreadLocalRandom.current(); + Location loc = at.clone().add(new Vector( + 0.05 * (rand.nextBoolean() ? -1 : 0), + 1.5, + 0.05 * (rand.nextBoolean() ? -1 : 0) + )); + + TextDisplay textDisplay = loc.getWorld().spawn(loc, TextDisplay.class); + textDisplay.setText(legacyText); + tracker.applyStyle(textDisplay, settings); + + if (forceViewerScoped || settings.isViewerScoped()) { + textDisplay.setVisibleByDefault(false); + for (Player viewer : viewers) { + viewer.showEntity(DamageDisplay.me, textDisplay); + } + } + + tracker.trackAndScheduleRemoval(textDisplay, settings); + return textDisplay; + } +} diff --git a/src/main/java/com/danlegt/damagedisplay/Services/Messages.java b/src/main/java/com/danlegt/damagedisplay/Services/Messages.java new file mode 100644 index 0000000..7ffe1d8 --- /dev/null +++ b/src/main/java/com/danlegt/damagedisplay/Services/Messages.java @@ -0,0 +1,46 @@ +package com.danlegt.damagedisplay.Services; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.Map; + +public final class Messages { + + private static final LegacyComponentSerializer AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); + private static final LegacyComponentSerializer SECTION = LegacyComponentSerializer.legacySection(); + + private Messages() { + } + + public static Component parse(String ampersandText) { + if (ampersandText == null || ampersandText.isEmpty()) { + return Component.empty(); + } + return AMPERSAND.deserialize(ampersandText); + } + + public static String toLegacy(Component component) { + return SECTION.serialize(component); + } + + public static String toLegacy(String ampersandText) { + return toLegacy(parse(ampersandText)); + } + + public static String format(String template, Map placeholders) { + String result = template == null ? "" : template; + for (Map.Entry entry : placeholders.entrySet()) { + result = result.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return result; + } + + public static String formatLegacy(String template, Map placeholders) { + return toLegacy(format(template, placeholders)); + } + + public static String roundDamage(double value) { + return Double.toString(Math.round(value * 100.0) / 100.0); + } +} diff --git a/src/main/java/com/danlegt/damagedisplay/Services/PlayerPrefs.java b/src/main/java/com/danlegt/damagedisplay/Services/PlayerPrefs.java new file mode 100644 index 0000000..dd9a676 --- /dev/null +++ b/src/main/java/com/danlegt/damagedisplay/Services/PlayerPrefs.java @@ -0,0 +1,143 @@ +package com.danlegt.damagedisplay.Services; + +import com.danlegt.damagedisplay.DamageDisplay; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; + +public class PlayerPrefs { + + private static final String PREFS_FILE = "player-prefs.yml"; + private static final String LEGACY_CACHE = "cache.dat"; + + private final Map actionBarEnabled = new HashMap<>(); + private final Map healsEnabled = new HashMap<>(); + + public boolean isActionBarEnabled(UUID playerId) { + return actionBarEnabled.getOrDefault(playerId, true); + } + + public boolean isHealsEnabled(UUID playerId) { + return healsEnabled.getOrDefault(playerId, true); + } + + public boolean toggleActionBar(UUID playerId) { + boolean next = !isActionBarEnabled(playerId); + actionBarEnabled.put(playerId, next); + save(); + return next; + } + + public boolean toggleHeals(UUID playerId) { + boolean next = !isHealsEnabled(playerId); + healsEnabled.put(playerId, next); + save(); + return next; + } + + public void load() { + actionBarEnabled.clear(); + healsEnabled.clear(); + + File dataFolder = DamageDisplay.me.getDataFolder(); + if (!dataFolder.exists()) { + dataFolder.mkdirs(); + } + + migrateLegacyCache(dataFolder); + + File prefsFile = new File(dataFolder, PREFS_FILE); + if (!prefsFile.exists()) { + return; + } + + FileConfiguration config = YamlConfiguration.loadConfiguration(prefsFile); + ConfigurationSection players = config.getConfigurationSection("players"); + if (players == null) { + return; + } + + for (String key : players.getKeys(false)) { + try { + UUID id = UUID.fromString(key); + ConfigurationSection section = players.getConfigurationSection(key); + if (section == null) { + continue; + } + if (section.contains("actionbar")) { + actionBarEnabled.put(id, section.getBoolean("actionbar")); + } + if (section.contains("heals")) { + healsEnabled.put(id, section.getBoolean("heals")); + } + } catch (IllegalArgumentException ignored) { + DamageDisplay.me.getLogger().log(Level.WARNING, "Invalid UUID in player-prefs.yml: " + key); + } + } + } + + public void save() { + File dataFolder = DamageDisplay.me.getDataFolder(); + if (!dataFolder.exists()) { + dataFolder.mkdirs(); + } + + YamlConfiguration config = new YamlConfiguration(); + Set ids = new HashSet<>(); + ids.addAll(actionBarEnabled.keySet()); + ids.addAll(healsEnabled.keySet()); + + for (UUID id : ids) { + String path = "players." + id; + if (actionBarEnabled.containsKey(id)) { + config.set(path + ".actionbar", actionBarEnabled.get(id)); + } + if (healsEnabled.containsKey(id)) { + config.set(path + ".heals", healsEnabled.get(id)); + } + } + + try { + config.save(new File(dataFolder, PREFS_FILE)); + } catch (IOException e) { + DamageDisplay.me.getLogger().log(Level.WARNING, "Could not save player-prefs.yml: " + e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private void migrateLegacyCache(File dataFolder) { + File legacy = new File(dataFolder, LEGACY_CACHE); + if (!legacy.exists()) { + return; + } + + try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(legacy))) { + List disabled = (List) ois.readObject(); + for (UUID id : disabled) { + actionBarEnabled.put(id, false); + } + save(); + + File backup = new File(dataFolder, LEGACY_CACHE + ".bak"); + if (!legacy.renameTo(backup)) { + DamageDisplay.me.getLogger().log(Level.WARNING, "Migrated cache.dat but could not rename it to cache.dat.bak"); + } else { + DamageDisplay.me.getLogger().info("Migrated cache.dat to player-prefs.yml"); + } + } catch (IOException | ClassNotFoundException e) { + DamageDisplay.me.getLogger().log(Level.WARNING, "Could not migrate cache.dat: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/danlegt/damagedisplay/Services/PluginSettings.java b/src/main/java/com/danlegt/damagedisplay/Services/PluginSettings.java index ea143e6..4e17f10 100644 --- a/src/main/java/com/danlegt/damagedisplay/Services/PluginSettings.java +++ b/src/main/java/com/danlegt/damagedisplay/Services/PluginSettings.java @@ -2,6 +2,7 @@ import com.danlegt.damagedisplay.DamageDisplay; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Display; import java.util.logging.Level; import java.util.logging.Logger; @@ -10,6 +11,9 @@ public class PluginSettings { private boolean actionBarDamageDealtEnabled = true; private boolean actionBarDamageReceivedEnabled = true; + private String actionBarDamageDealtFormat = "&e★ Damage Dealt: &e{damage}"; + private String actionBarDamageReceivedFormat = "&c☠ Damage Taken: &c{damage}&7 | &7{cause}"; + private boolean displayTextEnabled = true; private int lingerTime = 10; private double nearbyRange = 16; @@ -17,6 +21,21 @@ public class PluginSettings { private int maxTotal = 64; private int maxPerChunk = 8; private boolean viewerScoped = true; + private String displayFormat = "&c{damage}"; + private String critFormat = "&6✴{damage}"; + + private Display.Billboard billboard = Display.Billboard.VERTICAL; + private boolean shadow = true; + private boolean seeThrough = false; + private boolean background = true; + private byte textOpacity = (byte) 255; + private float scale = 1.0f; + + private boolean motionEnabled = true; + private double riseBlocks = 0.6; + + private boolean healsEnabled = false; + private String healFormat = "&a+{amount}"; public void reload() { reload(DamageDisplay.me.getConfig(), DamageDisplay.me.getLogger()); @@ -25,6 +44,9 @@ public void reload() { public void reload(FileConfiguration config, Logger logger) { actionBarDamageDealtEnabled = config.getBoolean("actionbar.damageDealt.enabled", true); actionBarDamageReceivedEnabled = config.getBoolean("actionbar.damageReceived.enabled", true); + actionBarDamageDealtFormat = config.getString("actionbar.damageDealt.format", actionBarDamageDealtFormat); + actionBarDamageReceivedFormat = config.getString("actionbar.damageReceived.format", actionBarDamageReceivedFormat); + displayTextEnabled = config.getBoolean("displayText.enabled", true); lingerTime = config.getInt("displayText.lingertime", 10); nearbyRange = config.getDouble("displayText.nearbyRange", 16); @@ -32,6 +54,21 @@ public void reload(FileConfiguration config, Logger logger) { maxTotal = Math.max(0, config.getInt("displayText.maxTotal", 64)); maxPerChunk = Math.max(0, config.getInt("displayText.maxPerChunk", 8)); viewerScoped = config.getBoolean("displayText.viewerScoped", true); + displayFormat = config.getString("displayText.format", displayFormat); + critFormat = config.getString("displayText.critFormat", critFormat); + + billboard = parseBillboard(config.getString("displayText.billboard", "VERTICAL"), logger); + shadow = config.getBoolean("displayText.shadow", true); + seeThrough = config.getBoolean("displayText.seeThrough", false); + background = config.getBoolean("displayText.background", true); + textOpacity = (byte) Math.max(0, Math.min(255, config.getInt("displayText.textOpacity", 255))); + scale = (float) Math.max(0.01, config.getDouble("displayText.scale", 1.0)); + + motionEnabled = config.getBoolean("displayText.motion.enabled", true); + riseBlocks = Math.max(0.0, config.getDouble("displayText.motion.riseBlocks", 0.6)); + + healsEnabled = config.getBoolean("heals.enabled", false); + healFormat = config.getString("heals.format", healFormat); if (lingerTime <= 0) { if (logger != null) { @@ -42,6 +79,17 @@ public void reload(FileConfiguration config, Logger logger) { } } + private static Display.Billboard parseBillboard(String raw, Logger logger) { + try { + return Display.Billboard.valueOf(raw.trim().toUpperCase()); + } catch (IllegalArgumentException | NullPointerException ex) { + if (logger != null) { + logger.log(Level.WARNING, "Invalid displayText.billboard '" + raw + "', using VERTICAL"); + } + return Display.Billboard.VERTICAL; + } + } + public boolean isActionBarDamageDealtEnabled() { return actionBarDamageDealtEnabled; } @@ -50,6 +98,14 @@ public boolean isActionBarDamageReceivedEnabled() { return actionBarDamageReceivedEnabled; } + public String getActionBarDamageDealtFormat() { + return actionBarDamageDealtFormat; + } + + public String getActionBarDamageReceivedFormat() { + return actionBarDamageReceivedFormat; + } + public boolean isDisplayTextEnabled() { return displayTextEnabled; } @@ -77,4 +133,52 @@ public int getMaxPerChunk() { public boolean isViewerScoped() { return viewerScoped; } + + public String getDisplayFormat() { + return displayFormat; + } + + public String getCritFormat() { + return critFormat; + } + + public Display.Billboard getBillboard() { + return billboard; + } + + public boolean isShadow() { + return shadow; + } + + public boolean isSeeThrough() { + return seeThrough; + } + + public boolean isBackground() { + return background; + } + + public byte getTextOpacity() { + return textOpacity; + } + + public float getScale() { + return scale; + } + + public boolean isMotionEnabled() { + return motionEnabled; + } + + public double getRiseBlocks() { + return riseBlocks; + } + + public boolean isHealsEnabled() { + return healsEnabled; + } + + public String getHealFormat() { + return healFormat; + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 8fad8b8..553672a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,16 +1,51 @@ -# The text that is being displayed at the bottom of the player's screen right above the hotbar +############################################################ +# DamageDisplay configuration +# Colors use legacy & codes (&a, &c, &e, &7, &l, ...). +# Placeholders: {damage} {cause} {amount} +############################################################ + +# ---------------------------------------------------------- +# Action bar (above the hotbar) +# ---------------------------------------------------------- actionbar: damageDealt: - enabled: true # If the damage that the player dealt should be shown to the player + enabled: true + format: "&e★ Damage Dealt: &e{damage}" damageReceived: - enabled: true # If the damage that the player has sustained should be shown to the player + enabled: true + format: "&c☠ Damage Taken: &c{damage}&7 | &7{cause}" -# The text that spawns whenever an entity is hit +# ---------------------------------------------------------- +# Floating TextDisplay holograms (damage) +# ---------------------------------------------------------- displayText: - lingertime: 10 # The amount of time in ticks that the text should stay alive for, 20 = 1 second - enabled: true # Whether to display the floating DisplayText instances or not - nearbyRange: 16 # Skip spawn if no player is within this distance; 0 disables the nearby-player gate - onlyFromPlayers: false # Only spawn when the damager (or projectile shooter) is a Player - maxTotal: 64 # Maximum concurrent TextDisplay entities globally - maxPerChunk: 8 # Maximum concurrent TextDisplay entities per chunk - viewerScoped: true # Hide displays by default and only show them to nearby players + enabled: true + lingertime: 10 # ticks (20 = 1 second); minimum 1 + nearbyRange: 16 # skip if no players in range; 0 = disable gate + onlyFromPlayers: false # only when a player (or their projectile) dealt the hit + maxTotal: 64 # global concurrent display cap (0 = unlimited) + maxPerChunk: 8 # per-chunk concurrent cap (0 = unlimited) + viewerScoped: true # hide by default; show only to nearby permitted players + + format: "&c{damage}" + critFormat: "&6✴{damage}" + + # TextDisplay appearance + billboard: VERTICAL # FIXED | VERTICAL | HORIZONTAL | CENTER + shadow: true + seeThrough: false + background: true + textOpacity: 255 # 0-255 + scale: 1.0 + + # Float upward over the linger time + motion: + enabled: true + riseBlocks: 0.6 + +# ---------------------------------------------------------- +# Healing holograms (same style/caps/motion as displayText) +# ---------------------------------------------------------- +heals: + enabled: false + format: "&a+{amount}" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bdf8809..a3bede6 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -14,16 +14,16 @@ commands: permissions: damagedisplay.enabled: default: true - description: "Grants permission to view ActionBar messages to a user" + description: "Grants permission to view ActionBar messages and holograms" damagedisplay.command.info: - default: false - description: "Grants permission to /dd info to show information about the plugin" + default: op + description: "Grants permission to /dd info" damagedisplay.command.toggle: default: true - description: "Grants permission to /dd toggle which enables or disables the ActionBar messages" + description: "Grants permission to /dd toggle [actionbar|heals]" damagedisplay.command.reload: - default: false - description: "Grants permission to /dd reload which will reload the configuration file, so you won't have to reload it" + default: op + description: "Grants permission to /dd reload" damagedisplay.command.help: - default: false + default: op description: "Grants permission to /dd help" \ No newline at end of file diff --git a/src/test/java/com/danlegt/damagedisplay/Commands/CommandManagerTabCompleteTest.java b/src/test/java/com/danlegt/damagedisplay/Commands/CommandManagerTabCompleteTest.java new file mode 100644 index 0000000..8ac6fcb --- /dev/null +++ b/src/test/java/com/danlegt/damagedisplay/Commands/CommandManagerTabCompleteTest.java @@ -0,0 +1,71 @@ +package com.danlegt.damagedisplay.Commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CommandManagerTabCompleteTest { + + @Mock + private CommandSender sender; + + @Mock + private Command command; + + private final CommandManager manager = new CommandManager(); + + @Test + void firstArgCompletesCommandsAllowedByPermission() { + when(sender.hasPermission("damagedisplay.command.toggle")).thenReturn(true); + when(sender.hasPermission("damagedisplay.command.help")).thenReturn(false); + when(sender.hasPermission("damagedisplay.command.info")).thenReturn(false); + when(sender.hasPermission("damagedisplay.command.reload")).thenReturn(false); + + List completions = manager.onTabComplete(sender, command, "dd", new String[]{""}); + + assertEquals(List.of("toggle"), completions); + } + + @Test + void firstArgFiltersByPrefix() { + lenient().when(sender.hasPermission("damagedisplay.command.toggle")).thenReturn(true); + lenient().when(sender.hasPermission("damagedisplay.command.help")).thenReturn(true); + lenient().when(sender.hasPermission("damagedisplay.command.info")).thenReturn(true); + lenient().when(sender.hasPermission("damagedisplay.command.reload")).thenReturn(true); + + List completions = manager.onTabComplete(sender, command, "dd", new String[]{"re"}); + + assertEquals(List.of("reload"), completions); + } + + @Test + void toggleSecondArgCompletesActionbarAndHeals() { + when(sender.hasPermission("damagedisplay.command.toggle")).thenReturn(true); + + List completions = manager.onTabComplete(sender, command, "dd", new String[]{"toggle", ""}); + + assertTrue(completions.contains("actionbar")); + assertTrue(completions.contains("heals")); + assertEquals(2, completions.size()); + } + + @Test + void toggleSecondArgFiltersByPrefix() { + when(sender.hasPermission("damagedisplay.command.toggle")).thenReturn(true); + + List completions = manager.onTabComplete(sender, command, "dd", new String[]{"toggle", "he"}); + + assertEquals(List.of("heals"), completions); + } +} diff --git a/src/test/java/com/danlegt/damagedisplay/Services/ActionBarTest.java b/src/test/java/com/danlegt/damagedisplay/Services/ActionBarTest.java index 5854376..15c8175 100644 --- a/src/test/java/com/danlegt/damagedisplay/Services/ActionBarTest.java +++ b/src/test/java/com/danlegt/damagedisplay/Services/ActionBarTest.java @@ -11,16 +11,16 @@ class ActionBarTest { @Test void parseStringToPrettyFormatsUnderscoredEnumNames() { - assertEquals("Entity Attack ", ActionBar.parseStringToPretty("ENTITY_ATTACK")); - assertEquals("Fall ", ActionBar.parseStringToPretty("FALL")); - assertEquals("Projectile ", ActionBar.parseStringToPretty("PROJECTILE")); + assertEquals("Entity Attack", ActionBar.parseStringToPretty("ENTITY_ATTACK")); + assertEquals("Fall", ActionBar.parseStringToPretty("FALL")); + assertEquals("Projectile", ActionBar.parseStringToPretty("PROJECTILE")); } @Test void parseDamageCauseUsesPrecomputedMap() { - assertEquals("Entity Attack ", ActionBar.parseStringToPretty(EntityDamageEvent.DamageCause.ENTITY_ATTACK)); - assertEquals("Fall ", ActionBar.parseStringToPretty(EntityDamageEvent.DamageCause.FALL)); - assertEquals("Fire Tick ", ActionBar.parseStringToPretty(EntityDamageEvent.DamageCause.FIRE_TICK)); + assertEquals("Entity Attack", ActionBar.parseStringToPretty(EntityDamageEvent.DamageCause.ENTITY_ATTACK)); + assertEquals("Fall", ActionBar.parseStringToPretty(EntityDamageEvent.DamageCause.FALL)); + assertEquals("Fire Tick", ActionBar.parseStringToPretty(EntityDamageEvent.DamageCause.FIRE_TICK)); } @Test diff --git a/src/test/java/com/danlegt/damagedisplay/Services/MessagesTest.java b/src/test/java/com/danlegt/damagedisplay/Services/MessagesTest.java new file mode 100644 index 0000000..b8ea943 --- /dev/null +++ b/src/test/java/com/danlegt/damagedisplay/Services/MessagesTest.java @@ -0,0 +1,35 @@ +package com.danlegt.damagedisplay.Services; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MessagesTest { + + @Test + void formatReplacesPlaceholders() { + String result = Messages.format("&c{damage} &7| &7{cause}", Map.of( + "damage", "3.5", + "cause", "Fall" + )); + assertEquals("&c3.5 &7| &7Fall", result); + } + + @Test + void formatLegacyConvertsAmpersandCodes() { + String legacy = Messages.formatLegacy("&cHello", Map.of()); + assertTrue(legacy.contains("Hello")); + assertFalse(legacy.contains("&c")); + assertTrue(legacy.indexOf('§') >= 0); + } + + @Test + void roundDamageUsesTwoDecimals() { + assertEquals("3.14", Messages.roundDamage(3.14159)); + assertEquals("1.0", Messages.roundDamage(1.001)); + } +} diff --git a/src/test/java/com/danlegt/damagedisplay/Services/PluginSettingsTest.java b/src/test/java/com/danlegt/damagedisplay/Services/PluginSettingsTest.java index 4bb4127..b27fa3d 100644 --- a/src/test/java/com/danlegt/damagedisplay/Services/PluginSettingsTest.java +++ b/src/test/java/com/danlegt/damagedisplay/Services/PluginSettingsTest.java @@ -1,6 +1,7 @@ package com.danlegt.damagedisplay.Services; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Display; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -21,6 +22,7 @@ void reloadAppliesConfiguredValues() { YamlConfiguration config = new YamlConfiguration(); config.set("actionbar.damageDealt.enabled", false); config.set("actionbar.damageReceived.enabled", false); + config.set("actionbar.damageDealt.format", "&eHit {damage}"); config.set("displayText.enabled", false); config.set("displayText.lingertime", 25); config.set("displayText.nearbyRange", 32.5); @@ -28,12 +30,25 @@ void reloadAppliesConfiguredValues() { config.set("displayText.maxTotal", 12); config.set("displayText.maxPerChunk", 3); config.set("displayText.viewerScoped", false); + config.set("displayText.format", "&c{dmg}"); + config.set("displayText.critFormat", "&6CRIT {damage}"); + config.set("displayText.billboard", "CENTER"); + config.set("displayText.shadow", false); + config.set("displayText.seeThrough", true); + config.set("displayText.background", false); + config.set("displayText.textOpacity", 200); + config.set("displayText.scale", 1.5); + config.set("displayText.motion.enabled", false); + config.set("displayText.motion.riseBlocks", 1.2); + config.set("heals.enabled", true); + config.set("heals.format", "&aHeal {amount}"); PluginSettings settings = new PluginSettings(); settings.reload(config, null); assertFalse(settings.isActionBarDamageDealtEnabled()); assertFalse(settings.isActionBarDamageReceivedEnabled()); + assertEquals("&eHit {damage}", settings.getActionBarDamageDealtFormat()); assertFalse(settings.isDisplayTextEnabled()); assertEquals(25, settings.getLingerTime()); assertEquals(32.5, settings.getNearbyRange()); @@ -41,6 +56,18 @@ void reloadAppliesConfiguredValues() { assertEquals(12, settings.getMaxTotal()); assertEquals(3, settings.getMaxPerChunk()); assertFalse(settings.isViewerScoped()); + assertEquals("&c{dmg}", settings.getDisplayFormat()); + assertEquals("&6CRIT {damage}", settings.getCritFormat()); + assertEquals(Display.Billboard.CENTER, settings.getBillboard()); + assertFalse(settings.isShadow()); + assertTrue(settings.isSeeThrough()); + assertFalse(settings.isBackground()); + assertEquals((byte) 200, settings.getTextOpacity()); + assertEquals(1.5f, settings.getScale()); + assertFalse(settings.isMotionEnabled()); + assertEquals(1.2, settings.getRiseBlocks()); + assertTrue(settings.isHealsEnabled()); + assertEquals("&aHeal {amount}", settings.getHealFormat()); } @Test @@ -57,6 +84,15 @@ void reloadUsesDefaultsWhenKeysAreMissing() { assertEquals(64, settings.getMaxTotal()); assertEquals(8, settings.getMaxPerChunk()); assertTrue(settings.isViewerScoped()); + assertEquals(Display.Billboard.VERTICAL, settings.getBillboard()); + assertTrue(settings.isShadow()); + assertFalse(settings.isSeeThrough()); + assertTrue(settings.isBackground()); + assertEquals((byte) 255, settings.getTextOpacity()); + assertEquals(1.0f, settings.getScale()); + assertTrue(settings.isMotionEnabled()); + assertEquals(0.6, settings.getRiseBlocks()); + assertFalse(settings.isHealsEnabled()); } @Test @@ -86,6 +122,19 @@ void reloadClampsNegativeCapsToZero() { assertEquals(0, settings.getMaxPerChunk()); } + @Test + void reloadFallsBackOnInvalidBillboard() { + YamlConfiguration config = new YamlConfiguration(); + config.set("displayText.billboard", "NOPE"); + + CapturingLogger logger = new CapturingLogger(); + PluginSettings settings = new PluginSettings(); + settings.reload(config, logger); + + assertEquals(Display.Billboard.VERTICAL, settings.getBillboard()); + assertFalse(logger.warnings.isEmpty()); + } + private static final class CapturingLogger extends Logger { private final List warnings = new ArrayList<>();