package net.knarcraft.blacksmith; import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.trait.Trait; import net.citizensnpcs.api.util.DataKey; import net.knarcraft.blacksmith.config.NPCSettings; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.enchantments.EnchantmentTarget; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.util.Vector; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; /** * The class representing the Blacksmith NPC trait */ public class BlacksmithTrait extends Trait { private final Map coolDowns = new HashMap<>(); private ReforgeSession session; private final NPCSettings config; private long _sessionStart = System.currentTimeMillis(); /** * Instantiates a new blacksmith trait */ public BlacksmithTrait() { super("blacksmith"); //This should crash if the blacksmith plugin hasn't been properly registered Bukkit.getServer().getPluginManager().getPlugin("Blacksmith"); this.config = new NPCSettings(BlacksmithPlugin.getInstance().getSettings()); } /** * Adds a cool-down for the given player's next blacksmith reforge * * @param playerUUID

The ID of the player to add the cool-down for

* @param waitUntil

The time when the player can interact again

*/ public void addCoolDown(UUID playerUUID, Calendar waitUntil) { coolDowns.put(playerUUID, waitUntil); } /** * Unsets the session of this blacksmith, making it ready for another round */ public void unsetSession() { this.session = null; } /** * Loads all config values stored in citizens' config file for this NPC * * @param key

The data key used for the config root

*/ @Override public void load(DataKey key) { config.loadVariables(key); } /** * Saves all config values for this NPC * * @param key

The data key used for the config root

*/ @Override public void save(DataKey key) { config.saveVariables(key); } @EventHandler(priority = EventPriority.HIGHEST) public void onClick(PlayerInteractEvent event) { if (event.getHand() == null || (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK)) { return; } //If the player is not looking at a blacksmith, there is no need to do anything Entity target = getTarget(event.getPlayer(), event.getPlayer().getNearbyEntities(15, 10, 15)); if (!CitizensAPI.getNPCRegistry().isNPC(target) || !CitizensAPI.getNPCRegistry().getNPC(target).hasTrait(BlacksmithTrait.class)) { return; } //Block armor equip if interacting with a blacksmith ItemStack usedItem = event.getPlayer().getInventory().getItem(event.getHand()); if (usedItem != null && isArmor(usedItem)) { event.setUseItemInHand(Event.Result.DENY); event.getPlayer().updateInventory(); } } @EventHandler public void onRightClick(net.citizensnpcs.api.event.NPCRightClickEvent event) { if (this.npc != event.getNPC()) { return; } //Perform any necessary pre-session work Player player = event.getClicker(); if (!prepareForSession(player)) { return; } if (session != null) { //Continue the existing session continueSession(player); } else { //Start a new session startSession(player); } } /** * Performs necessary work before the session is started or continued * * @param player

The player to prepare the session for

* @return

True if preparations were successful. False if a session shouldn't be started

*/ private boolean prepareForSession(Player player) { //If cool-down has been disabled after it was set for this player, remove the cool-down if (config.getDisableCoolDown() && coolDowns.get(player.getUniqueId()) != null) { coolDowns.remove(player.getUniqueId()); } //Deny if permission is missing if (!player.hasPermission("blacksmith.reforge")) { return false; } //Deny if on cool-down, or remove cool-down if expired if (coolDowns.get(player.getUniqueId()) != null) { if (!Calendar.getInstance().after(coolDowns.get(player.getUniqueId()))) { player.sendMessage(config.getCoolDownUnexpiredMessage()); return false; } coolDowns.remove(player.getUniqueId()); } //If already in a session, but the player has failed to interact, or left the blacksmith, allow a new session if (session != null) { if (System.currentTimeMillis() > _sessionStart + 10 * 1000 || this.npc.getEntity().getLocation().distance(session.getPlayer().getLocation()) > 20) { session = null; } } return true; } /** * Tries to continue the session for the given player * * @param player

The player to continue the session for

*/ private void continueSession(Player player) { //Another player is using the blacksmith if (!session.isInSession(player)) { player.sendMessage(config.getBusyWithPlayerMessage()); return; } //The blacksmith is already reforging for the player if (session.isRunning()) { player.sendMessage(config.getBusyReforgingMessage()); return; } if (session.endSession()) { //Quit if the player cannot afford, or has changed their item session = null; } else { //Start reforging for the player reforge(npc, player); } } /** * Starts a new session, and prepares to repair the player's item * * @param player

The player to start the session for

*/ private void startSession(Player player) { ItemStack hand = player.getInventory().getItemInMainHand(); //Refuse if not repairable, or if reforge-able items is set, but doesn't include the held item List reforgeAbleItems = config.getReforgeAbleItems(); if (!isRepairable(hand) || (!reforgeAbleItems.isEmpty() && !reforgeAbleItems.contains(hand.getType()))) { player.sendMessage(config.getInvalidItemMessage()); return; } //Start a new reforge session for the player _sessionStart = System.currentTimeMillis(); session = new ReforgeSession(this, player, npc, config); //Tell the player the cost of repairing the item String cost = EconomyManager.formatCost(player); String itemName = hand.getType().name().toLowerCase().replace('_', ' '); player.sendMessage(config.getCostMessage().replace("", cost).replace("", itemName)); } /** * Starts reforging the player's item * * @param npc

The NPC performing the reforge

* @param player

The player that initiated the reforge

*/ private void reforge(NPC npc, Player player) { player.sendMessage(config.getStartReforgeMessage()); EconomyManager.withdraw(player); session.beginReforge(); ItemStack heldItem = player.getInventory().getItemInMainHand(); //Display the item in the NPC's hand if (npc.getEntity() instanceof Player) { ((Player) npc.getEntity()).getInventory().setItemInMainHand(heldItem); } else { Objects.requireNonNull(((LivingEntity) npc.getEntity()).getEquipment()).setItemInMainHand(heldItem); } //Remove the item from the player's inventory player.getInventory().setItemInMainHand(null); } /** * Gets the target-entity the entity is looking at * * @param entity

The entity looking at something

* @param entities

Entities near the player

* @param

The type of entity the player is looking at

* @return

The entity the player is looking at, or null if no such entity exists

*/ private static T getTarget(final Entity entity, final Iterable entities) { if (entity == null) { return null; } T target = null; final double threshold = 1; for (final T other : entities) { final Vector n = other.getLocation().toVector().subtract(entity.getLocation().toVector()); if (entity.getLocation().getDirection().normalize().crossProduct(n).lengthSquared() < threshold && n.normalize().dot(entity.getLocation().getDirection().normalize()) >= 0) { if (target == null || target.getLocation().distanceSquared(entity.getLocation()) > other.getLocation().distanceSquared(entity.getLocation())) { target = other; } } } return target; } /** * Gets whether the given item is repairable * * @param item

The item to check

* @return

True if the item is repairable

*/ private static boolean isRepairable(ItemStack item) { return item.getItemMeta() instanceof Damageable; } /** * Gets whether the given item is a type of armor * * @param item

The item to check if is armor or not

* @return

True if the given item is a type of armor

*/ public static boolean isArmor(ItemStack item) { return EnchantmentTarget.WEARABLE.includes(item); //TODO: Remove this commented-out code if the above line works /*return switch (item.getType()) { case LEATHER_HELMET, LEATHER_CHESTPLATE, LEATHER_LEGGINGS, LEATHER_BOOTS, CHAINMAIL_HELMET, CHAINMAIL_CHESTPLATE, CHAINMAIL_LEGGINGS, CHAINMAIL_BOOTS, GOLDEN_HELMET, GOLDEN_CHESTPLATE, GOLDEN_LEGGINGS, GOLDEN_BOOTS, IRON_HELMET, IRON_CHESTPLATE, IRON_LEGGINGS, IRON_BOOTS, DIAMOND_HELMET, DIAMOND_CHESTPLATE, DIAMOND_LEGGINGS, DIAMOND_BOOTS, TURTLE_HELMET, ELYTRA, NETHERITE_HELMET, NETHERITE_CHESTPLATE, NETHERITE_LEGGINGS, NETHERITE_BOOTS -> true; default -> false; };*/ } }