The event that's starting
- */ - private void onActionStart(@NotNull ActionStartEvent event) { - BukkitScheduler scheduler = Bukkit.getScheduler(); - int playWorkSound = scheduler.scheduleSyncRepeatingTask(BlacksmithPlugin.getInstance(), - () -> displayWorkAnimation(event), 20, 5); - - scheduler.scheduleSyncDelayedTask(BlacksmithPlugin.getInstance(), () -> scheduler.cancelTask(playWorkSound), - event.getActionDurationTicks()); - } - - /** - * Displays the animation of a working NPC - * - * @param eventThe action start event to display for
- */ - private void displayWorkAnimation(@NotNull ActionStartEvent event) { - if (random.nextInt(100) >= 20) { - return; - } - - this.playWorkSound(event.getNpc().getEntity()); - ProtocolManager manager = ProtocolLibrary.getProtocolManager(); - PacketContainer packet = manager.createPacket(PacketType.Play.Server.ANIMATION); - packet.getIntegers().write(0, event.getNpc().getEntity().getEntityId()); - packet.getIntegers().write(1, 3); - - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getWorld().equals(event.getNpc().getEntity().getWorld())) { - manager.sendServerPacket(player, packet); - } - } - } - - /** - * Plays the fail action sound - * - * @param entityThe entity to play the sound at
- */ - private void playFailSound(@NotNull Entity entity) { - playSound(entity, Sound.ENTITY_VILLAGER_NO); - } - - /** - * Plays the succeed action sound - * - * @param entityThe entity to play the sound at
- */ - private void playSuccessSound(@NotNull Entity entity) { - playSound(entity, Sound.BLOCK_ANVIL_USE); - } - - /** - * Plays a npc sound using a cancellable event - * - * @param entityThe entity that should play the sound
- */ - private void playWorkSound(@NotNull Entity entity) { - playSound(entity, Sound.ITEM_ARMOR_EQUIP_NETHERITE); - } - - /** - * Plays a npc sound using a cancellable event - * - * @param entityThe entity that should play the sound
- */ - private void playSound(@NotNull Entity entity, Sound sound) { - World world = entity.getLocation().getWorld(); - if (world == null) { - return; - } - - world.playSound(entity, sound, SoundCategory.AMBIENT, 0.5f, 1.0f); - } - -} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/BlacksmithVisuals.java b/src/main/java/net/knarcraft/blacksmithvisuals/BlacksmithVisuals.java index 35b23cf..931494a 100644 --- a/src/main/java/net/knarcraft/blacksmithvisuals/BlacksmithVisuals.java +++ b/src/main/java/net/knarcraft/blacksmithvisuals/BlacksmithVisuals.java @@ -1,6 +1,18 @@ package net.knarcraft.blacksmithvisuals; +import net.knarcraft.blacksmithvisuals.command.ReloadCommand; +import net.knarcraft.blacksmithvisuals.command.SetNPCPositionCommand; +import net.knarcraft.blacksmithvisuals.listener.BlacksmithListener; +import net.knarcraft.blacksmithvisuals.manager.ConfigurationManager; +import net.knarcraft.blacksmithvisuals.manager.NPCDataManager; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.PluginCommand; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.logging.Level; /** * The blacksmith visual main class @@ -8,18 +20,102 @@ import org.bukkit.plugin.java.JavaPlugin; @SuppressWarnings("unused") public final class BlacksmithVisuals extends JavaPlugin { + private static BlacksmithVisuals instance; + private ConfigurationManager configurationManager; + private NPCDataManager npcDataManager; + @Override public void onEnable() { - // Plugin startup logic - getServer().getPluginManager().registerEvents(new BlacksmithListener(), this); + BlacksmithVisuals.instance = this; - //TODO: Allow customization of sounds, volumes and pitches - // Allow setting an idle position and a working position, and move the NPC to the right position at the right time. - // Use the distance between the two locations to decide how much sooner before the success/fail sounds are played the NPC should start walking + //Copy default config to disk, and add missing configuration values + this.saveDefaultConfig(); + this.getConfig().options().copyDefaults(true); + this.reloadConfig(); + this.saveConfig(); + + try { + this.configurationManager = new ConfigurationManager(this.getConfig()); + } catch (InvalidConfigurationException exception) { + this.getLogger().log(Level.SEVERE, "Could not properly load the configuration file. " + + "Please check it for errors!"); + this.onDisable(); + return; + } + try { + this.npcDataManager = NPCDataManager.load(); + } catch (IOException e) { + this.getLogger().log(Level.SEVERE, "Could not properly load the data.yml file. " + + "Please check it for errors!"); + this.onDisable(); + return; + } + getServer().getPluginManager().registerEvents(new BlacksmithListener(this.configurationManager), this); + registerCommand("reload", new ReloadCommand()); + registerCommand("setNPCPosition", new SetNPCPositionCommand()); } @Override public void onDisable() { // Plugin shutdown logic } + + /** + * Reloads the configuration file + */ + public void reload() { + reloadConfig(); + saveConfig(); + try { + this.configurationManager.load(getConfig()); + } catch (InvalidConfigurationException exception) { + this.getLogger().log(Level.SEVERE, "Could not properly load the configuration file. " + + "Please check it for errors!"); + this.onDisable(); + } + } + + /** + * Registers a command + * + * @param commandNameThe name of the command
+ * @param executorThe executor to bind to the command
+ */ + private void registerCommand(@NotNull String commandName, @NotNull CommandExecutor executor) { + PluginCommand command = this.getCommand(commandName); + if (command != null) { + command.setExecutor(executor); + } + } + + /** + * Gets the NPC data manager + * + * @returnThe NPC data manager
+ */ + @NotNull + public NPCDataManager getNpcDataManager() { + return this.npcDataManager; + } + + /** + * Gets the configuration manager + * + * @returnThe configuration manager
+ */ + @NotNull + public ConfigurationManager getConfigurationManager() { + return this.configurationManager; + } + + /** + * Gets an instance of this plugin + * + * @returnAn instance of this plugin
+ */ + @NotNull + public static BlacksmithVisuals getInstance() { + return instance; + } + } diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/command/ReloadCommand.java b/src/main/java/net/knarcraft/blacksmithvisuals/command/ReloadCommand.java new file mode 100644 index 0000000..fb1221c --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/command/ReloadCommand.java @@ -0,0 +1,22 @@ +package net.knarcraft.blacksmithvisuals.command; + +import net.knarcraft.blacksmithvisuals.BlacksmithVisuals; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +/** + * The reload command + */ +public class ReloadCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, + @NotNull String[] strings) { + BlacksmithVisuals.getInstance().reload(); + commandSender.sendMessage("BlacksmithVisuals has been reloaded!"); + return true; + } + +} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/command/SetNPCPositionCommand.java b/src/main/java/net/knarcraft/blacksmithvisuals/command/SetNPCPositionCommand.java new file mode 100644 index 0000000..57c89c7 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/command/SetNPCPositionCommand.java @@ -0,0 +1,83 @@ +package net.knarcraft.blacksmithvisuals.command; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.knarcraft.blacksmith.trait.BlacksmithTrait; +import net.knarcraft.blacksmith.trait.ScrapperTrait; +import net.knarcraft.blacksmithvisuals.BlacksmithVisuals; +import net.knarcraft.blacksmithvisuals.container.NPCData; +import net.knarcraft.blacksmithvisuals.manager.NPCDataManager; +import net.knarcraft.blacksmithvisuals.property.NPCPosition; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * The commands for setting an NPC's positions + */ +public class SetNPCPositionCommand implements TabExecutor { + + private static ListWhether to animate the off-hand at all
+ * @param animationDelayThe delay between each time the animation should be attempted (max swing rate)
+ * @param animationChanceThe probability of the animation triggering when the animation delay has passed
+ */ +public record AnimationData(boolean animateOffHand, int animationDelay, int animationChance) { + + /** + * Instantiates a new animation data object with sanity checks + * + * @param animateOffHandWhether to animate the off-hand at all
+ * @param animationDelayThe delay between each time the animation should be attempted (max swing rate)
+ * @param animationChanceThe probability of the animation triggering when the animation delay has passed
+ */ + public AnimationData { + if (animationDelay < 1) { + animationDelay = 1; + } + if (animationChance < 1) { + animationChance = 1; + } + if (animationChance > 100) { + animationChance = 100; + } + } + + /** + * Loads sound data from the given configuration key + * + * @param configurationThe configuration to load values from
+ * @param rootKeyThe key to load the data from
+ * @returnThe loaded sound data
+ */ + @NotNull + public static AnimationData load(@NotNull FileConfiguration configuration, + @NotNull String rootKey) throws InvalidConfigurationException { + ConfigurationSection section = configuration.getConfigurationSection(rootKey); + if (section == null) { + throw new InvalidConfigurationException("Could not find the configuration section " + rootKey); + } + boolean animateOffHand = section.getBoolean("animateOffhand", true); + int animationDelay = section.getInt("animationDelay", 5); + int animationChance = section.getInt("animationChance", 20); + + return new AnimationData(animateOffHand, animationDelay, animationChance); + } + +} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/container/NPCData.java b/src/main/java/net/knarcraft/blacksmithvisuals/container/NPCData.java new file mode 100644 index 0000000..14800f2 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/container/NPCData.java @@ -0,0 +1,16 @@ +package net.knarcraft.blacksmithvisuals.container; + +import net.knarcraft.blacksmithvisuals.property.NPCPosition; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * Data for an NPC + * + * @param positionsA map storing all NPC positions
+ */ +public record NPCData(@NotNull MapThe category the sound should be played in
+ * @param soundThe sound to be played
+ * @param volumeThe volume to play the sound at (> 0)
+ * @param pitchThe pitch to play the sound at
+ * @param offsetTicksThe amount of ticks to offset the sound by
+ * @param enabledWhether this sound is enabled
+ */ +public record SoundData(@NotNull SoundCategory soundCategory, @NotNull Sound sound, float volume, float pitch, + int offsetTicks, boolean enabled) { + + /** + * Instantiates a new sound data object with sanity checks + * + * @param soundCategoryThe category the sound should be played in
+ * @param soundThe sound to be played
+ * @param volumeThe volume to play the sound at (> 0)
+ * @param pitchThe pitch to play the sound at
+ * @param offsetTicksThe amount of ticks to offset the sound by
+ * @param enabledWhether this sound is enabled
+ */ + public SoundData { + if (volume < 0) { + volume = 1; + } + if (pitch < 0) { + pitch = 0; + } else if (pitch > 1) { + pitch = 1; + } + Objects.requireNonNull(soundCategory); + Objects.requireNonNull(sound); + } + + /** + * Loads sound data from the given configuration key + * + * @param configurationThe configuration to load values from
+ * @param rootKeyThe key to load the data from
+ * @returnThe loaded sound data
+ */ + @NotNull + public static SoundData load(@NotNull FileConfiguration configuration, + @NotNull String rootKey) throws InvalidConfigurationException { + ConfigurationSection section = configuration.getConfigurationSection(rootKey); + if (section == null) { + throw new InvalidConfigurationException("Could not find the configuration section " + rootKey); + } + boolean enabled = section.getBoolean("enabled", true); + SoundCategory soundCategory = parseCategory(section.getString("soundCategory", "AMBIENT")); + Sound sound = parseSound(section.getString("sound", "ENTITY_PIG_DEATH")); + float pitch = (float) section.getDouble("pitch", 1); + float volume = (float) section.getDouble("volume", 1); + int offsetTicks = section.getInt("offset", 0); + + return new SoundData(soundCategory, sound, volume, pitch, offsetTicks, enabled); + } + + /** + * Safely parses a sound + * + * @param soundNameThe name of the sound to parse
+ * @returnThe parsed sound
+ */ + @NotNull + private static Sound parseSound(@NotNull String soundName) { + try { + return Sound.valueOf(soundName); + } catch (IllegalArgumentException exception) { + BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Invalid sound in configuration: " + + soundName); + return Sound.ENTITY_PIG_DEATH; + } + } + + /** + * Safely parses a sound category + * + * @param categoryNameThe name of the category to parse
+ * @returnThe parsed category
+ */ + @NotNull + private static SoundCategory parseCategory(@NotNull String categoryName) { + try { + return SoundCategory.valueOf(categoryName); + } catch (IllegalArgumentException exception) { + BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Invalid sound category in " + + "configuration: " + categoryName); + return SoundCategory.AMBIENT; + } + } + +} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/listener/BlacksmithListener.java b/src/main/java/net/knarcraft/blacksmithvisuals/listener/BlacksmithListener.java new file mode 100644 index 0000000..e709134 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/listener/BlacksmithListener.java @@ -0,0 +1,278 @@ +package net.knarcraft.blacksmithvisuals.listener; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.PacketContainer; +import net.citizensnpcs.api.npc.NPC; +import net.knarcraft.blacksmith.event.ActionStartEvent; +import net.knarcraft.blacksmith.event.BlacksmithReforgeFailEvent; +import net.knarcraft.blacksmith.event.BlacksmithReforgeStartEvent; +import net.knarcraft.blacksmith.event.BlacksmithReforgeSucceedEvent; +import net.knarcraft.blacksmith.event.NPCSoundEvent; +import net.knarcraft.blacksmith.event.ScrapperSalvageFailEvent; +import net.knarcraft.blacksmith.event.ScrapperSalvageStartEvent; +import net.knarcraft.blacksmith.event.ScrapperSalvageSucceedEvent; +import net.knarcraft.blacksmithvisuals.BlacksmithVisuals; +import net.knarcraft.blacksmithvisuals.container.AnimationData; +import net.knarcraft.blacksmithvisuals.container.NPCData; +import net.knarcraft.blacksmithvisuals.container.SoundData; +import net.knarcraft.blacksmithvisuals.manager.ConfigurationManager; +import net.knarcraft.blacksmithvisuals.property.NPCPosition; +import net.knarcraft.blacksmithvisuals.property.SoundIdentifier; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Random; +import java.util.logging.Level; + +/** + * A listener for blacksmith-related events + */ +public class BlacksmithListener implements Listener { + + private final Random random; + private final ConfigurationManager configurationManager; + + /** + * Instantiates a new blacksmith listener + * + * @param configurationManagerThe configuration manager to get configuration values from
+ */ + public BlacksmithListener(@NotNull ConfigurationManager configurationManager) { + this.random = new Random(); + this.configurationManager = configurationManager; + } + + @EventHandler + public void onDefaultSound(@NotNull NPCSoundEvent event) { + event.setCancelled(true); + } + + @EventHandler + public void onReforgeStart(@NotNull BlacksmithReforgeStartEvent event) { + runWorkingAnimation(event, SoundIdentifier.REFORGING_WORKING, this.configurationManager.getBlacksmithAnimationData()); + } + + @EventHandler + public void onSalvageStart(@NotNull ScrapperSalvageStartEvent event) { + runWorkingAnimation(event, SoundIdentifier.SALVAGING_WORKING, this.configurationManager.getScrapperAnimationData()); + } + + @EventHandler + public void onReforgeSuccess(@NotNull BlacksmithReforgeSucceedEvent event) { + playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.REFORGING_SUCCESS)); + } + + @EventHandler + public void onSalvageSuccess(@NotNull ScrapperSalvageSucceedEvent event) { + playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.SALVAGING_SUCCESS)); + } + + @EventHandler + public void onReforgeFail(@NotNull BlacksmithReforgeFailEvent event) { + playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.REFORGING_FAILURE)); + } + + @EventHandler + public void onSalvageFail(@NotNull ScrapperSalvageFailEvent event) { + playSound(event.getNpc().getEntity(), this.configurationManager.getSoundData(SoundIdentifier.SALVAGING_FAILURE)); + } + + /** + * Runs the working animation for an NPC + * + * @param eventThe action that started
+ * @param soundIdentifierThe identifier for the sound to play
+ * @param animationDataThe animation data for the animation to play
+ */ + private void runWorkingAnimation(@NotNull ActionStartEvent event, @NotNull SoundIdentifier soundIdentifier, + @NotNull AnimationData animationData) { + BlacksmithVisuals instance = BlacksmithVisuals.getInstance(); + BukkitScheduler scheduler = Bukkit.getScheduler(); + NPC npc = event.getNpc(); + long delay = moveToWorkingPosition(npc, NPCPosition.getFromMaterial(event.getCraftingStation())); + long finishTime = event.getActionDurationTicks() - (2 * delay); + + scheduler.scheduleSyncDelayedTask(instance, () -> startWorkAnimation(npc.getEntity(), + this.configurationManager.getSoundData(soundIdentifier), animationData, finishTime - 20), delay); + scheduler.scheduleSyncDelayedTask(instance, () -> moveBack(event.getNpc()), finishTime); + } + + /** + * Moves an NPC back to its idle position + * + * @param npcThe NPC to move
+ */ + private void moveBack(@NotNull NPC npc) { + if (!npc.isSpawned()) { + return; + } + + NPCData npcData = BlacksmithVisuals.getInstance().getNpcDataManager().getData(npc.getUniqueId()); + if (npcData == null) { + return; + } + + Location targetLocation = npcData.positions().get(NPCPosition.IDLE); + if (!npc.getNavigator().canNavigateTo(targetLocation)) { + BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Idle position for NPC " + + npc.getName() + " is unreachable!"); + return; + } + npc.getNavigator().setTarget(targetLocation); + + Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), () -> + npc.getEntity().teleport(targetLocation), getWalkTime(npc.getEntity().getLocation(), targetLocation)); + } + + /** + * Moves a npc to its working position + * + * @param npcThe NPC to move
+ * @param npcPositionThe npc position to move to
+ * @returnThe time the move will take
+ */ + private long moveToWorkingPosition(@NotNull NPC npc, @Nullable NPCPosition npcPosition) { + if (!npc.isSpawned() || npcPosition == null) { + return 0; + } + + NPCData npcData = BlacksmithVisuals.getInstance().getNpcDataManager().getData(npc.getUniqueId()); + if (npcData == null) { + return 0; + } + + Location targetLocation = npcData.positions().get(npcPosition); + if (targetLocation == null && npcPosition != NPCPosition.WORKING_REPAIRABLE) { + targetLocation = npcData.positions().get(NPCPosition.WORKING_REPAIRABLE); + } + if (targetLocation == null) { + return 0; + } + + if (!npc.getNavigator().canNavigateTo(targetLocation)) { + BlacksmithVisuals.getInstance().getLogger().log(Level.WARNING, "Working position for NPC " + + npc.getName() + " is unreachable!"); + return 0; + } + + // Move NPC using Citizens path-finding + npc.getNavigator().setTarget(targetLocation); + + // Teleport the NPC tp get it in the exact final location + long walkTime = getWalkTime(npc.getEntity().getLocation(), targetLocation); + Location finalTargetLocation = targetLocation; + Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), () -> + npc.getEntity().teleport(finalTargetLocation), walkTime); + return walkTime; + } + + /** + * Starts the working animation and sound + * + * @param entityThe entity to play the sound at
+ * @param soundDataThe sound data for the sound to play
+ * @param animationDataThe animation data for the animation to play
+ * @param durationTicksThe duration of the work animation
+ */ + private void startWorkAnimation(@NotNull Entity entity, @NotNull SoundData soundData, + @NotNull AnimationData animationData, long durationTicks) { + BlacksmithVisuals instance = BlacksmithVisuals.getInstance(); + BukkitScheduler scheduler = Bukkit.getScheduler(); + + int playWorkSound = scheduler.scheduleSyncRepeatingTask(instance, + () -> animateNPC(entity, soundData, animationData), 20, animationData.animationDelay()); + scheduler.scheduleSyncDelayedTask(instance, () -> scheduler.cancelTask(playWorkSound), durationTicks); + } + + /** + * Gets the time it would take to go from one location to another in ticks + * + * @param startLocationThe location to start from
+ * @param targetLocationThe target location
+ * @returnThe time in ticks
+ */ + private long getWalkTime(@NotNull Location startLocation, @NotNull Location targetLocation) { + double distance = startLocation.distance(targetLocation); + double WALK_SPEED = 4.317; + return Math.round((distance / WALK_SPEED) * 20); + } + + /** + * Animates a npc's hand, and plays a sound + * + * @param entityThe entity to animate and play the sound at
+ * @param soundDataThe sound to be played
+ * @param animationDataThe animation to be played
+ */ + private void animateNPC(@NotNull Entity entity, @NotNull SoundData soundData, @NotNull AnimationData animationData) { + if (random.nextInt(100) >= animationData.animationChance()) { + return; + } + + playSound(entity, soundData); + + // Don't play disabled animations + if (!animationData.animateOffHand()) { + return; + } + + if (soundData.offsetTicks() < 0) { + Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), + () -> animateOffhand(entity), -soundData.offsetTicks()); + } else { + animateOffhand(entity); + } + } + + /** + * Animates an NPC's offhand + * + * @param entityThe entity to animate
+ */ + private void animateOffhand(@NotNull Entity entity) { + ProtocolManager manager = ProtocolLibrary.getProtocolManager(); + PacketContainer packet = manager.createPacket(PacketType.Play.Server.ANIMATION); + packet.getIntegers().write(0, entity.getEntityId()); + packet.getIntegers().write(1, 3); + + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getWorld().equals(entity.getWorld())) { + manager.sendServerPacket(player, packet); + } + } + } + + /** + * Plays a sound according to the given sound data + * + * @param entityThe entity to play the sound from
+ * @param soundDataThe data describing the sound to play
+ */ + private void playSound(@NotNull Entity entity, @NotNull SoundData soundData) { + // Don't play disabled sounds + if (!soundData.enabled()) { + return; + } + + World world = entity.getLocation().getWorld(); + if (world == null) { + return; + } + + int delay = Math.max(soundData.offsetTicks(), 0); + Bukkit.getScheduler().scheduleSyncDelayedTask(BlacksmithVisuals.getInstance(), () -> + world.playSound(entity, soundData.sound(), soundData.soundCategory(), + soundData.volume(), soundData.pitch()), delay); + } + +} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/manager/ConfigurationManager.java b/src/main/java/net/knarcraft/blacksmithvisuals/manager/ConfigurationManager.java new file mode 100644 index 0000000..31858a0 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/manager/ConfigurationManager.java @@ -0,0 +1,78 @@ +package net.knarcraft.blacksmithvisuals.manager; + +import net.knarcraft.blacksmithvisuals.container.AnimationData; +import net.knarcraft.blacksmithvisuals.container.SoundData; +import net.knarcraft.blacksmithvisuals.property.SoundIdentifier; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * A manager keeping track of configuration options + */ +public class ConfigurationManager { + + private MapThe file configuration to load values from
+ * @throws InvalidConfigurationExceptionIf the configuration file has missing or invalid values
+ */ + public ConfigurationManager(@NotNull FileConfiguration fileConfiguration) throws InvalidConfigurationException { + load(fileConfiguration); + } + + /** + * Loads the configuration from disk + * + * @param fileConfigurationThe file configuration to get values from
+ * @throws InvalidConfigurationExceptionIf the configuration file has missing or invalid values
+ */ + public void load(@NotNull FileConfiguration fileConfiguration) throws InvalidConfigurationException { + soundSettings = new HashMap<>(); + for (SoundIdentifier identifier : SoundIdentifier.values()) { + soundSettings.put(identifier, SoundData.load(fileConfiguration, identifier.getConfigNode())); + } + scrapperAnimationData = AnimationData.load(fileConfiguration, "scrapper.animation"); + blacksmithAnimationData = AnimationData.load(fileConfiguration, "blacksmith.animation"); + } + + /** + * Gets the animation data for the scrapper's working animation + * + * @returnScrapper animation data
+ */ + @NotNull + public AnimationData getScrapperAnimationData() { + return this.scrapperAnimationData; + } + + /** + * Gets the animation data for the blacksmith's working animation + * + * @returnBlacksmith animation data
+ */ + @NotNull + public AnimationData getBlacksmithAnimationData() { + return this.blacksmithAnimationData; + } + + /** + * Gets the sound data for the given identifier + * + * @param identifierThe identifier of the sound
+ * @returnThe sound's sound data
+ */ + @NotNull + public SoundData getSoundData(@NotNull SoundIdentifier identifier) { + return soundSettings.get(identifier); + } + +} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/manager/NPCDataManager.java b/src/main/java/net/knarcraft/blacksmithvisuals/manager/NPCDataManager.java new file mode 100644 index 0000000..e45bb3a --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/manager/NPCDataManager.java @@ -0,0 +1,115 @@ +package net.knarcraft.blacksmithvisuals.manager; + +import net.knarcraft.blacksmithvisuals.BlacksmithVisuals; +import net.knarcraft.blacksmithvisuals.container.NPCData; +import net.knarcraft.blacksmithvisuals.property.NPCPosition; +import org.bukkit.Location; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +/** + * A manager for NPC data + */ +public class NPCDataManager { + + private final MapThe map containing existing NPC data
+ */ + public NPCDataManager(@NotNull MapThe id of the NPC to get data for
+ * @returnThe NPC data, or null if not set
+ */ + @Nullable + public NPCData getData(@NotNull UUID npcId) { + return this.npcDataMap.get(npcId); + } + + /** + * Adds new NPC data to an NPC + * + * @param npcIdThe id of the NPC to add data to
+ * @param npcDataThe data to set
+ */ + public void putData(@NotNull UUID npcId, @NotNull NPCData npcData) { + this.npcDataMap.put(npcId, npcData); + try { + this.save(); + } catch (IOException exception) { + BlacksmithVisuals.getInstance().getLogger().log(Level.SEVERE, "Unable to save NPC data. Error was: " + + exception); + } + } + + /** + * Saves this data manager's data to disk + * + * @throws IOExceptionIf unable to write the configuration file
+ */ + public void save() throws IOException { + if (!configurationFile.exists() && !configurationFile.createNewFile()) { + throw new FileNotFoundException("data.yml could not be found or created. Please create it manually."); + } + FileConfiguration configuration = new YamlConfiguration(); + for (Map.EntryThe loaded data manager
+ * @throws IOExceptionIf unable to load the configuration file
+ */ + @NotNull + public static NPCDataManager load() throws IOException { + if (!configurationFile.exists() && !configurationFile.createNewFile()) { + throw new FileNotFoundException("data.yml could not be found or created. Please create it manually."); + } + MapThe position name
+ */ + @NotNull + public String getPositionName() { + return this.positionName; + } + + /** + * Gets the NPC position the work station's material + * + * @param materialThe material of the workstation
+ * @returnThe corresponding NPC position
+ */ + @Nullable + public static NPCPosition getFromMaterial(@NotNull Material material) { + return switch (material) { + case CRAFTING_TABLE -> NPCPosition.WORKING_CRAFTING; + case ANVIL -> NPCPosition.WORKING_REPAIRABLE; + case SMITHING_TABLE -> NPCPosition.WORKING_NETHERITE; + default -> null; + }; + } + + /** + * Gets an NPC position from the given user input + * + * @param inputThe input to get an NPC position from
+ * @returnThe NPC position, or null if not recognized
+ */ + @Nullable + public static NPCPosition fromInput(@NotNull String input) { + String cleaned = input.toLowerCase().replace("_", "-"); + for (NPCPosition position : NPCPosition.values()) { + if (position.getPositionName().equalsIgnoreCase(cleaned)) { + return position; + } + } + return null; + } + +} diff --git a/src/main/java/net/knarcraft/blacksmithvisuals/property/SoundIdentifier.java b/src/main/java/net/knarcraft/blacksmithvisuals/property/SoundIdentifier.java new file mode 100644 index 0000000..a656f01 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmithvisuals/property/SoundIdentifier.java @@ -0,0 +1,61 @@ +package net.knarcraft.blacksmithvisuals.property; + +import org.jetbrains.annotations.NotNull; + +/** + * An identifier for the available blacksmith/scrapper sounds + */ +public enum SoundIdentifier { + + /** + * The sound played while a blacksmith is working + */ + REFORGING_WORKING("blacksmith.sounds.reforgeWorking"), + + /** + * The sound played when a blacksmith finishes successfully + */ + REFORGING_SUCCESS("blacksmith.sounds.reforgeSuccess"), + + /** + * The sound played when a blacksmith finishes with a failure + */ + REFORGING_FAILURE("blacksmith.sounds.reforgeFailure"), + + /** + * The sound played while a scrapper is working + */ + SALVAGING_WORKING("scrapper.sounds.salvageWorking"), + + /** + * The sound played when a scrapper finishes successfully + */ + SALVAGING_SUCCESS("scrapper.sounds.salvageSuccess"), + + /** + * The sound played when a scrapper finishes with a failure + */ + SALVAGING_FAILURE("scrapper.sounds.salvageFailure"), + ; + + private final String configNode; + + /** + * Instantiates a new sound identifier + * + * @param configNodeThe sound's configuration node
+ */ + SoundIdentifier(@NotNull String configNode) { + this.configNode = configNode; + } + + /** + * Gets the configuration node corresponding to the sound + * + * @returnThe configuration node
+ */ + public String getConfigNode() { + return this.configNode; + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..1dd17ca --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,98 @@ +blacksmith: + animation: + # Whether to simulate hitting an anvil or similar by animating the blacksmith's off-hand + animateOffhand: true + # The delay between each potential arm move in ticks + animationDelay: 5 + # The probability (percentage) of an arm swing happening once the animation delay is reached + animationChance: 20 + # Sounds that play on specific actions or events + sounds: + # The sound played when reforging finishes, if successful + reforgeSuccess: + # If disabled, this sound will never play + enabled: true + # The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html) + sound: BLOCK_ANVIL_USE + # The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html) + soundCategory: AMBIENT + # The pitch to play at, between 0 and 2. 1 is default. + pitch: 1 + # The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range. + volume: 1 + # The sound played when reforging finishes, if not successful. + reforgeFailure: + # If disabled, this sound will never play. + enabled: true + # The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html). + sound: ENTITY_VILLAGER_NO + # The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html) + soundCategory: AMBIENT + # The pitch to play at, between 0 and 2. 1 is default. + pitch: 1 + # The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range. + volume: 1 + # The sound played when the NPC's arm moves. + reforgeWorking: + # If disabled, this sound will never play. + enabled: true + # The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html). + sound: ITEM_ARMOR_EQUIP_NETHERITE + # The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html) + soundCategory: AMBIENT + # The pitch to play at, between 0 and 2. 1 is default. + pitch: 1 + # The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range. + volume: 1 + # The offset (delay), in ticks (negative or positive), before the sound should play. + # This can be used to better synchronize the sound to the arm movement. + offset: 0 +scrapper: + animation: + # Whether to simulate hitting an anvil or similar by animating the scrapper's off-hand + animateOffhand: true + # The delay between each potential arm move in ticks + animationDelay: 5 + # The probability (percentage) of an arm swing happening once the animation delay is reached + animationChance: 20 + # Sounds that play on specific actions or events. + sounds: + # The sound played when salvaging finishes, if successful. + salvageSuccess: + # If disabled, this sound will never play. + enabled: true + # The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html). + sound: BLOCK_ANVIL_USE + # The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html) + soundCategory: AMBIENT + # The pitch to play at, between 0 and 2. 1 is default. + pitch: 1 + # The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range. + volume: 1 + # The sound played when salvaging finishes, if not successful. + salvageFailure: + # If disabled, this sound will never play. + enabled: true + # The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html). + sound: ENTITY_VILLAGER_NO + # The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html) + soundCategory: AMBIENT + # The pitch to play at, between 0 and 2. 1 is default. + pitch: 1 + # The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range. + volume: 1 + # The sound played when the NPC's arm moves. + salvageWorking: + # If disabled, this sound will never play. + enabled: true + # The sound to play (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html). + sound: ITEM_ARMOR_EQUIP_NETHERITE + # The sound category to play the sound in (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/SoundCategory.html) + soundCategory: AMBIENT + # The pitch to play at, between 0 and 2. 1 is default. + pitch: 1 + # The volume to play at. Between 0 and infinity. 1 is max volume, but higher value increases the range. + volume: 1 + # The offset (delay), in ticks (negative or positive), before the sound should play. + # This can be used to better synchronize the sound to the arm movement. + offset: 0 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4708a2d..fb04e8a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,4 +2,35 @@ name: BlacksmithVisuals version: '${project.version}' main: net.knarcraft.blacksmithvisuals.BlacksmithVisuals api-version: 1.20 -depend: [ ProtocolLib ] \ No newline at end of file +depend: [ ProtocolLib, Blacksmith, Citizens ] +prefix: "Blacksmith Visuals" +description: "And add-on plugin to the blacksmith plugin that adds visual animation and configurable sounds." +website: "https://git.knarcraft.net/KnarCraft/BlacksmithVisuals" + +commands: + reload: + permission: blacksmithvisuals.reload + usage: /