Ver Fonte

Revamped NPC version, now supports all EntityTypes, NPC now uses Mannequins for Player-like NPC's as default

Jan há 1 mês atrás
pai
commit
94514b2eb2
25 ficheiros alterados com 709 adições e 483 exclusões
  1. 0 6
      pom.xml
  2. 1 1
      src/main/java/me/lethunderhawk/fluxapi/FluxService.java
  3. 3 3
      src/main/java/me/lethunderhawk/fluxapi/util/animation/Animation.java
  4. 2 4
      src/main/java/me/lethunderhawk/fluxapi/util/interfaces/BazaarFluxModule.java
  5. 69 0
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemOptions.java
  6. 8 0
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/LoreDesigner.java
  7. 17 0
      src/main/java/me/lethunderhawk/main/FluxAPI.java
  8. 0 15
      src/main/java/me/lethunderhawk/main/Main.java
  9. 20 13
      src/main/java/me/lethunderhawk/npc/NPCModule.java
  10. 22 0
      src/main/java/me/lethunderhawk/npc/abstraction/DummyNPC.java
  11. 108 0
      src/main/java/me/lethunderhawk/npc/abstraction/NPC.java
  12. 118 0
      src/main/java/me/lethunderhawk/npc/abstraction/NPCOptions.java
  13. 72 10
      src/main/java/me/lethunderhawk/npc/command/NPCCommand.java
  14. 0 16
      src/main/java/me/lethunderhawk/npc/event/NPCClickAction.java
  15. 0 42
      src/main/java/me/lethunderhawk/npc/event/NPCInteractionEvent.java
  16. 0 15
      src/main/java/me/lethunderhawk/npc/event/NPCListener.java
  17. 64 0
      src/main/java/me/lethunderhawk/npc/gui/NPCOptionsGUI.java
  18. 129 85
      src/main/java/me/lethunderhawk/npc/manager/NPCManager.java
  19. 60 0
      src/main/java/me/lethunderhawk/npc/util/MojangAPI.java
  20. 0 5
      src/main/java/me/lethunderhawk/npc/util/NMSHelper.java
  21. 0 13
      src/main/java/me/lethunderhawk/npc/util/NPC.java
  22. 0 58
      src/main/java/me/lethunderhawk/npc/util/NPCOptions.java
  23. 0 22
      src/main/java/me/lethunderhawk/npc/util/string/StringUtility.java
  24. 0 173
      src/main/java/me/lethunderhawk/npc/util/versioned/NPC_1_21_10.java
  25. 16 2
      src/main/resources/plugin.yml

+ 0 - 6
pom.xml

@@ -45,12 +45,6 @@
                 </executions>
             </plugin>
         </plugins>
-        <resources>
-            <resource>
-                <directory>src/main/resources</directory>
-                <filtering>true</filtering>
-            </resource>
-        </resources>
     </build>
 
     <repositories>

+ 1 - 1
src/main/java/me/lethunderhawk/fluxapi/Services.java → src/main/java/me/lethunderhawk/fluxapi/FluxService.java

@@ -5,7 +5,7 @@ import me.lethunderhawk.fluxapi.util.interfaces.BazaarFluxModule;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
-public final class Services {
+public final class FluxService {
 
     private static final Map<Class<?>, Object> services = new ConcurrentHashMap<>();
 

+ 3 - 3
src/main/java/me/lethunderhawk/fluxapi/util/animation/Animation.java

@@ -1,7 +1,7 @@
 package me.lethunderhawk.fluxapi.util.animation;
 
-import me.lethunderhawk.fluxapi.Services;
-import me.lethunderhawk.main.Main;
+import me.lethunderhawk.fluxapi.FluxService;
+import me.lethunderhawk.main.FluxAPI;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
 import org.bukkit.plugin.java.JavaPlugin;
@@ -18,7 +18,7 @@ public abstract class Animation {
     private Runnable onComplete;
 
     protected Animation(World world, int duration) {
-        this.plugin = Services.get(Main.class);
+        this.plugin = FluxService.get(FluxAPI.class);
         this.world = world;
         this.duration = duration;
     }

+ 2 - 4
src/main/java/me/lethunderhawk/fluxapi/util/interfaces/BazaarFluxModule.java

@@ -1,8 +1,6 @@
 package me.lethunderhawk.fluxapi.util.interfaces;
 
-import me.lethunderhawk.fluxapi.Services;
 import me.lethunderhawk.fluxapi.util.MessageSender;
-import me.lethunderhawk.main.Main;
 import net.kyori.adventure.audience.Audience;
 import net.kyori.adventure.text.Component;
 import org.bukkit.command.CommandSender;
@@ -10,8 +8,8 @@ import org.bukkit.plugin.java.JavaPlugin;
 
 public abstract class BazaarFluxModule{
     protected JavaPlugin plugin;
-    public BazaarFluxModule() {
-        this.plugin = Services.get(Main.class);
+    public BazaarFluxModule(JavaPlugin plugin) {
+        this.plugin = plugin;
     }
     public abstract String getPrefix();
     public abstract void onEnable();

+ 69 - 0
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemOptions.java

@@ -0,0 +1,69 @@
+package me.lethunderhawk.fluxapi.util.itemdesign;
+
+import me.lethunderhawk.fluxapi.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.List;
+
+public class ItemOptions {
+
+
+    private Component name;
+    private List<Component> lore;
+    private int amount = 1;
+    private Material material;
+    public ItemOptions(Material material){
+        this.material = material;
+    }
+    public ItemStack buildItemStack(){
+        ItemStack item = new ItemStack(material);
+        item.setAmount(amount);
+        ItemMeta meta = item.getItemMeta();
+        meta.lore(lore);
+        meta.displayName(name);
+        item.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+
+        return item;
+    }
+
+    // ====== Getters and Setters ======
+
+    public Component getName() {
+        return name;
+    }
+
+    public ItemOptions setName(Component name) {
+        this.name = name;
+        return this;
+    }
+
+    public List<Component> getLore() {
+        return lore;
+    }
+
+    public ItemOptions setLore(List<Component> lore) {
+        this.lore = lore;
+        return this;
+    }
+
+    public int getAmount() {
+        return amount;
+    }
+
+    public ItemOptions setAmount(int amount) {
+        this.amount = amount;
+        return this;
+    }
+
+    public Material getMaterial() {
+        return material;
+    }
+
+    public ItemOptions setMaterial(Material material) {
+        this.material = material;
+        return this;
+    }
+}

+ 8 - 0
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/LoreDesigner.java

@@ -41,6 +41,14 @@ public final class LoreDesigner {
 
         return builder.build();
     }
+    public static List<Component> createLore(String text, String widthReference, NamedTextColor color) {
+        List<Component> lore = createLore(text, widthReference);
+        for(Component component : lore) {
+            component.color(color);
+        }
+        return lore;
+    }
+
     public static List<Component> createLore(String text, String widthReference) {
         int maxLength = widthReference.length();
         if (text == null || text.isBlank() || maxLength <= 0) {

+ 17 - 0
src/main/java/me/lethunderhawk/main/FluxAPI.java

@@ -0,0 +1,17 @@
+package me.lethunderhawk.main;
+
+import me.lethunderhawk.fluxapi.FluxService;
+import me.lethunderhawk.npc.NPCModule;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class FluxAPI extends JavaPlugin {
+
+    @Override
+    public void onEnable() {
+        FluxService.register(FluxAPI.class, this);
+
+        NPCModule module = new NPCModule(this);
+        module.onEnable();
+        FluxService.register(NPCModule.class, module);
+    }
+}

+ 0 - 15
src/main/java/me/lethunderhawk/main/Main.java

@@ -1,15 +0,0 @@
-package me.lethunderhawk.main;
-
-import me.lethunderhawk.fluxapi.Services;
-import me.lethunderhawk.npc.manager.NPCManager;
-import org.bukkit.plugin.java.JavaPlugin;
-
-public class Main extends JavaPlugin {
-
-    @Override
-    public void onEnable() {
-        Services.register(Main.class, this);
-        NPCManager manager = new NPCManager();
-        Services.register(NPCManager.class, manager);
-    }
-}

+ 20 - 13
src/main/java/me/lethunderhawk/npc/NPCModule.java

@@ -1,34 +1,41 @@
 package me.lethunderhawk.npc;
 
-import me.lethunderhawk.fluxapi.Services;
+import me.lethunderhawk.fluxapi.FluxService;
 import me.lethunderhawk.fluxapi.util.interfaces.BazaarFluxModule;
 import me.lethunderhawk.npc.command.NPCCommand;
-import me.lethunderhawk.npc.event.NPCListener;
 import me.lethunderhawk.npc.manager.NPCManager;
 import org.bukkit.event.HandlerList;
+import org.bukkit.plugin.java.JavaPlugin;
 
 public class NPCModule extends BazaarFluxModule{
+    public static final int viewDistance = 50;
+    private NPCManager NPCManager;
 
-    private NPCManager npcManager;
-    private NPCListener npcListener;
+    public NPCModule(JavaPlugin plugin) {
+        super(plugin);
+    }
 
     public String getPrefix() {
         return "[NPC]";
     }
 
+    @Override
     public void onEnable() {
-        this.npcManager = new NPCManager();
-        plugin.getCommand("npc").setExecutor(new NPCCommand(this));
-        Services.register(NPCManager.class, npcManager);
+        this.NPCManager = new NPCManager();
+
+        FluxService.register(NPCManager.class, NPCManager);
+
+        NPCCommand npcCommand = new NPCCommand(this);
+        plugin.getCommand("npc").setExecutor(npcCommand);
+        plugin.getCommand("npc").setTabCompleter(npcCommand);
 
-        this.npcListener = new NPCListener();
-        plugin.getServer().getPluginManager().registerEvents(npcListener, plugin);
+        plugin.getServer().getPluginManager().registerEvents(NPCManager, plugin);
+        plugin.getLogger().info("NPC Extension Enabled");
     }
 
+    @Override
     public void onDisable() {
-        npcManager.deleteAllNPCs();
-        HandlerList.unregisterAll(npcListener);
-        npcManager = null;
-        npcListener = null;
+        HandlerList.unregisterAll(NPCManager);
+        NPCManager = null;
     }
 }

+ 22 - 0
src/main/java/me/lethunderhawk/npc/abstraction/DummyNPC.java

@@ -0,0 +1,22 @@
+package me.lethunderhawk.npc.abstraction;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.entity.Player;
+
+public class DummyNPC extends NPC{
+
+    public DummyNPC(NPCOptions npcOptions) {
+        super(npcOptions);
+    }
+
+    @Override
+    public void onLeftClick(Player player) {
+        player.sendMessage(Component.text("<" + getOptions().getName() + "> Hello there!"));
+    }
+
+    @Override
+    public void onRightClick(Player player) {
+        player.sendMessage(Component.text("Bye there!"));
+
+    }
+}

+ 108 - 0
src/main/java/me/lethunderhawk/npc/abstraction/NPC.java

@@ -0,0 +1,108 @@
+package me.lethunderhawk.npc.abstraction;
+
+import com.destroystokyo.paper.profile.ProfileProperty;
+import io.papermc.paper.datacomponent.item.ResolvableProfile;
+import me.lethunderhawk.fluxapi.FluxService;
+import me.lethunderhawk.npc.manager.NPCManager;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Location;
+import org.bukkit.entity.*;
+
+import java.util.UUID;
+
+public abstract class NPC{
+    private final NPCOptions npcOptions;
+    private final Entity entity;
+
+    public NPC(NPCOptions npcOptions) {
+        this.entity = npcOptions.getLocation().getWorld().spawnEntity(npcOptions.getLocation(), npcOptions.getEntityType());
+        this.npcOptions = npcOptions;
+        if(npcOptions.getEntityType() == EntityType.MANNEQUIN){
+            Mannequin mannequin = (Mannequin) entity;
+
+            ProfileProperty skin = new ProfileProperty("textures", npcOptions.getTexture(), npcOptions.getTextureSignature());
+            ResolvableProfile profile = ResolvableProfile.resolvableProfile().addProperty(skin).build();
+
+            mannequin.setProfile(profile);
+            mannequin.setDescription(npcOptions.getDescription());
+        }
+        entity.customName(Component.text(npcOptions.getName()));
+        entity.setCustomNameVisible(npcOptions.isNameVisible());
+        entity.setPersistent(true);
+        entity.setGravity(false);
+        entity.setNoPhysics(true);
+        if(entity instanceof LivingEntity livingEntity){
+            livingEntity.setAI(false);
+        }
+    }
+
+    public abstract void onLeftClick(Player player);
+
+    public void onShiftLeftClick(Player player){
+        onLeftClick(player);
+    }
+
+    public abstract void onRightClick(Player player);
+
+    public void onShiftRightClick(Player player){
+        onRightClick(player);
+    }
+
+    public Entity getEntity() {
+        return entity;
+    }
+
+    public String getName() {
+        return npcOptions.getName();
+    }
+
+    public void delete() {
+        entity.remove();
+    }
+
+    public UUID getUUID() {
+        return entity.getUniqueId();
+    }
+
+    public NPCOptions getOptions() {
+        return npcOptions;
+    }
+
+    public void rename(String newName) {
+        npcOptions.setName(newName);
+        entity.customName(Component.text(newName));
+    }
+    public void register(){
+        FluxService.get(NPCManager.class).registerNPC(this);
+    }
+
+    public void lookAt(Location target) {
+        if (target == null || this.entity == null || !(entity instanceof LivingEntity livingEntity)) return;
+
+        Location npcLoc = this.entity.getLocation();
+
+        // Use eye height for more natural look
+        double dx = target.getX() - npcLoc.getX();
+        double dy = target.getY() - (npcLoc.getY() + livingEntity.getEyeHeight());
+        double dz = target.getZ() - npcLoc.getZ();
+
+        // Prevent division by zero
+        if (dx == 0 && dz == 0) return;
+
+        double distanceXZ = Math.sqrt(dx * dx + dz * dz);
+
+        float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
+        float pitch = (float) Math.toDegrees(-Math.atan2(dy, distanceXZ));
+
+        // Normalize yaw to Minecraft format
+        yaw = normalizeYaw(yaw);
+
+        this.entity.setRotation(yaw, pitch);
+    }
+    private float normalizeYaw(float yaw) {
+        yaw %= 360.0F;
+        if (yaw >= 180.0F) yaw -= 360.0F;
+        if (yaw < -180.0F) yaw += 360.0F;
+        return yaw;
+    }
+}

+ 118 - 0
src/main/java/me/lethunderhawk/npc/abstraction/NPCOptions.java

@@ -0,0 +1,118 @@
+package me.lethunderhawk.npc.abstraction;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Location;
+import org.bukkit.entity.EntityType;
+import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.Nullable;
+
+import java.util.UUID;
+
+public class NPCOptions {
+    private String name;
+    private String texture;
+    private String signature;
+    private Location location;
+    private boolean visibleName = true;
+    private UUID uuid;
+    private boolean listed;
+    private EntityType entityType = EntityType.MANNEQUIN;
+    private Component description = null;
+    private boolean lookingAtNearest = false;
+    public NPCOptions(){
+    }
+    // ------ Getters
+    public String getName() {
+        return name;
+    }
+
+    public String getTexture() {
+        return texture;
+    }
+
+    public String getTextureSignature() {
+        return signature;
+    }
+
+    public Location getLocation() {
+        return location;
+    }
+
+    public boolean isNameVisible() {
+        return visibleName;
+    }
+
+    public UUID getUUID(){
+        if(uuid == null){
+            uuid = UUID.randomUUID();
+        }
+        return uuid;
+    }
+
+    public EntityType getEntityType(){
+        return entityType;
+    }
+
+    public boolean isListed() {
+        return listed;
+    }
+
+    public @Nullable Component getDescription() {
+        return description;
+    }
+
+    // ------ Setters
+    public NPCOptions setName(@NotNull String name) {
+        this.name = name;
+        return this;
+    }
+
+    public NPCOptions setEntityType(@NotNull EntityType entityType) {
+        this.entityType = entityType;
+        return this;
+    }
+
+    public NPCOptions setTexture(@NotNull String texture) {
+        this.texture = texture;
+        return this;
+    }
+
+    public NPCOptions setSignature(@NotNull String signature) {
+        this.signature = signature;
+        return this;
+    }
+
+    public NPCOptions setLocation(@NotNull Location location) {
+        this.location = location;
+        return this;
+    }
+
+    public NPCOptions setVisibleName(boolean visibleName) {
+        this.visibleName = visibleName;
+        return this;
+    }
+
+    public NPCOptions setUUID(@NotNull UUID uuid) {
+        this.uuid = uuid;
+        return this;
+    }
+
+    public NPCOptions setListed(boolean listed) {
+        this.listed = listed;
+        return this;
+    }
+
+    public NPCOptions setDescription(@Nullable Component description) {
+        this.description = description;
+        return this;
+    }
+
+    public boolean isLookingAtNearest() {
+        return lookingAtNearest;
+    }
+
+    public NPCOptions setLookingAtNearest(boolean lookingAtNearest) {
+        this.lookingAtNearest = lookingAtNearest;
+        return this;
+    }
+}

+ 72 - 10
src/main/java/me/lethunderhawk/npc/command/NPCCommand.java

@@ -1,15 +1,23 @@
 package me.lethunderhawk.npc.command;
 
-import me.lethunderhawk.fluxapi.Services;
+import me.lethunderhawk.fluxapi.FluxService;
 import me.lethunderhawk.fluxapi.util.command.CommandNode;
 import me.lethunderhawk.fluxapi.util.command.CustomCommand;
+import me.lethunderhawk.fluxapi.util.gui.InventoryManager;
 import me.lethunderhawk.fluxapi.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.npc.abstraction.DummyNPC;
+import me.lethunderhawk.npc.abstraction.NPCOptions;
+import me.lethunderhawk.npc.util.MojangAPI;
 import me.lethunderhawk.npc.manager.NPCManager;
-import me.lethunderhawk.npc.util.NPC;
-import me.lethunderhawk.npc.util.NPCOptions;
+import me.lethunderhawk.npc.abstraction.NPC;
+import me.lethunderhawk.npc.gui.NPCOptionsGUI;
 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
 
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
 public class NPCCommand extends CustomCommand {
     public NPCCommand(BazaarFluxModule module) {
         super(new CommandNode("npc", "Base npc command", null), module);
@@ -17,18 +25,72 @@ public class NPCCommand extends CustomCommand {
 
     @Override
     public void createCommands() {
-        registerCommand("create", "Test creation command", this::createNPC);
+        CommandNode create = registerCommand("create", "Test creation command", this::createNPC);
+        create.setTabCompleter(this::createCompleter);
+        CommandNode delete = registerCommand("delete", "Test deletion command", this::deleteNPC);
+        delete.setTabCompleter(this::npcDeletionCompleter);
+        CommandNode options = registerCommand("options", "Test options command", this::showOptionsMenu);
+        options.setTabCompleter(this::npcOptionsCompleter);
+    }
+
+    private List<String> npcDeletionCompleter(CommandSender sender, String[] args) {
+        if(!sender.hasPermission("npc.delete")) return null;
+        return npcUUIDCompleter(args);
+    }
+
+    private List<String> createCompleter(CommandSender sender, String[] args) {
+        if(!sender.hasPermission("npc.create")) return null;
+        return List.of("1. <name>", "2. <Player name for texture>");
+    }
+
+    private List<String> npcOptionsCompleter(CommandSender sender, String[] args) {
+        if(!sender.hasPermission("npc.edit")) return null;
+        return npcUUIDCompleter(args);
+    }
+
+    private List<String> npcUUIDCompleter(String[] args){
+        String partial = args[1].toLowerCase();
+        return FluxService.get(NPCManager.class).getRegisteredNPCsUUID().stream()
+                .map(UUID::toString)
+                .filter(uuid -> uuid.startsWith(partial))
+                .collect(Collectors.toList());
+    }
+
+    private void showOptionsMenu(CommandSender sender, String[] args) {
+        if(!sender.hasPermission("npc.options")) return;
+        if(!(sender instanceof Player player)) return;
+        NPC npc = FluxService.get(NPCManager.class).findByUUID(args[0]);
+        if(npc != null) {
+            InventoryManager.openFor(player, new NPCOptionsGUI(npc));
+        }else{
+            sender.sendMessage("That NPC doesn't exist!");
+        }
     }
 
     private void createNPC(CommandSender sender, String[] strings) {
+
         if(!(sender instanceof Player p)) return;
-        NPCOptions npcOptions = new NPCOptions();
-        npcOptions.setName(p.getName());
-        npcOptions.setLocation(p.getLocation());
-        npcOptions.setHideNametag(false);
 
-        NPC npc = Services.get(NPCManager.class).newNPC(npcOptions);
-        npc.showTo(p);
+        String name = strings[0];
+        String textureName = strings[1];
+        String[] stringdata = new String[]{"", ""};
+        if(textureName != null){
+            stringdata = MojangAPI.getSkinDataFromName(textureName);
+        }
+        NPCOptions options = new NPCOptions()
+                .setName(name)
+                .setLocation(p.getLocation())
+                .setTexture(stringdata[0])
+                .setSignature(stringdata[1])
+                .setLookingAtNearest(true);
+
+        NPC npc = new DummyNPC(options);
+        npc.register();
     }
 
+    private void deleteNPC(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player p)) return;
+        if(strings.length == 0) return;
+        FluxService.get(NPCManager.class).deleteNPCByUUIDString(strings[0]);
+    }
 }

+ 0 - 16
src/main/java/me/lethunderhawk/npc/event/NPCClickAction.java

@@ -1,16 +0,0 @@
-package me.lethunderhawk.npc.event;
-
-import com.comphenix.protocol.wrappers.EnumWrappers;
-
-public enum NPCClickAction {
-    INTERACT, INTERACT_AT, ATTACK;
-
-    public static NPCClickAction fromProtocolLibAction(EnumWrappers.EntityUseAction action) {
-        switch (action) {
-            case ATTACK: return ATTACK;
-            case INTERACT: return INTERACT;
-            case INTERACT_AT: return INTERACT_AT;
-            default: return null;
-        }
-    }
-}

+ 0 - 42
src/main/java/me/lethunderhawk/npc/event/NPCInteractionEvent.java

@@ -1,42 +0,0 @@
-package me.lethunderhawk.npc.event;
-
-import me.lethunderhawk.npc.util.NPC;
-import org.bukkit.entity.Player;
-import org.bukkit.event.Event;
-import org.bukkit.event.HandlerList;
-
-public class NPCInteractionEvent extends Event {
-    private static final HandlerList HANDLER_LIST = new HandlerList();
-
-    private final NPC clicked;
-
-    private final Player player;
-
-    private final NPCClickAction clickAction;
-
-    public NPCInteractionEvent(NPC clicked, Player player, NPCClickAction clickAction) {
-        this.clicked = clicked;
-        this.player = player;
-        this.clickAction = clickAction;
-    }
-
-    @Override
-    public HandlerList getHandlers() {
-        return HANDLER_LIST;
-    }
-
-    public static HandlerList getHandlerList() {
-        return HANDLER_LIST;
-    }
-
-    public NPC getClicked() {
-        return clicked;
-    }
-
-    public Player getPlayer() {
-        return player;
-    }
-    public NPCClickAction getClickAction() {
-        return clickAction;
-    }
-}

+ 0 - 15
src/main/java/me/lethunderhawk/npc/event/NPCListener.java

@@ -1,15 +0,0 @@
-package me.lethunderhawk.npc.event;
-
-import me.lethunderhawk.npc.util.NPC;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-
-public class NPCListener implements Listener {
-
-    @EventHandler
-    private void onNPCClick(NPCInteractionEvent event) {
-        NPC clicked = event.getClicked();
-        event.getPlayer().sendMessage("<" + clicked.getName() + "> Sorry, I don't think you're looking for me.");
-    }
-}
-

+ 64 - 0
src/main/java/me/lethunderhawk/npc/gui/NPCOptionsGUI.java

@@ -0,0 +1,64 @@
+package me.lethunderhawk.npc.gui;
+
+import me.lethunderhawk.fluxapi.FluxService;
+import me.lethunderhawk.fluxapi.util.gui.InventoryGUI;
+import me.lethunderhawk.fluxapi.util.gui.input.SignMenuFactory;
+import me.lethunderhawk.fluxapi.util.itemdesign.ItemOptions;
+import me.lethunderhawk.fluxapi.util.itemdesign.LoreDesigner;
+import me.lethunderhawk.main.FluxAPI;
+import me.lethunderhawk.npc.abstraction.NPC;
+import me.lethunderhawk.npc.manager.NPCManager;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.List;
+
+public class NPCOptionsGUI extends InventoryGUI {
+    private final NPC npc;
+
+    public NPCOptionsGUI(NPC npc) {
+        super("Options for NPC: " + npc.getName(), 36);
+        this.npc = npc;
+        buildGUI();
+    }
+
+    private void buildGUI() {
+        fillGlassPaneBackground();
+        ItemStack item = new ItemOptions(Material.OAK_SIGN)
+                .setName(Component.text("Rename this NPC",  NamedTextColor.YELLOW))
+                .setLore(
+                        LoreDesigner.createLore("Opens a new sign input which will allow you to rename the NPC's base Name", "Opens a new sign input which ", NamedTextColor.GRAY)
+                )
+                .buildItemStack();
+        setItemWithClickAction(4, item, this::editName);
+    }
+
+    private void editName(Player player, ClickType type) {
+        if(player.hasPermission("npc.edit")) {
+            SignMenuFactory.Menu menu = new SignMenuFactory(FluxService.get(FluxAPI.class)).newMenu(List.of(
+                    npc.getName(),
+                    "^^^^^^^^",
+                    "Edit the name",
+                    "of this NPC"
+            )).reopenIfFail(true);
+            menu.response((Player pl, String[] input) -> {
+                String newName = input[0];
+                if(newName.isEmpty()) return false;
+                pl.sendMessage("Renamed " + npc.getName() + " to " + newName);
+                FluxService.get(NPCManager.class).renameNPC(npc.getUUID(), newName);
+                return true;
+            });
+            menu.open(player);
+        }
+    }
+
+
+    @Override
+    public void update() {
+
+    }
+}

+ 129 - 85
src/main/java/me/lethunderhawk/npc/manager/NPCManager.java

@@ -1,104 +1,148 @@
 package me.lethunderhawk.npc.manager;
 
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.PacketAdapter;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.events.PacketEvent;
-import com.comphenix.protocol.wrappers.EnumWrappers;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import me.lethunderhawk.fluxapi.Services;
-import me.lethunderhawk.main.Main;
-import me.lethunderhawk.npc.event.NPCClickAction;
-import me.lethunderhawk.npc.event.NPCInteractionEvent;
-import me.lethunderhawk.npc.util.NPC;
-import me.lethunderhawk.npc.util.NPCOptions;
-import me.lethunderhawk.npc.util.versioned.NPC_1_21_10;
-import org.bukkit.Bukkit;
+import me.lethunderhawk.npc.abstraction.DummyNPC;
+import me.lethunderhawk.npc.abstraction.NPC;
+import me.lethunderhawk.npc.abstraction.NPCOptions;
+import org.bukkit.Location;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Mannequin;
 import org.bukkit.entity.Player;
-import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.player.PlayerInteractAtEntityEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import org.bukkit.inventory.EquipmentSlot;
 
-import java.util.HashSet;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
 
-public class NPCManager {
-
-    private final Set<NPC> registeredNPCs = new HashSet<>();
-    private final JavaPlugin plugin;
+public class NPCManager implements Listener {
+    private static final double FOCUS_RANGE_SQUARED = 250;
+    public int focusRange = 30;
+    private final Map<UUID, NPC> npcs = new HashMap<>();
 
     public NPCManager() {
-        this.plugin = Services.get(Main.class);
-        ProtocolLibrary.getProtocolManager().addPacketListener(
-                new PacketAdapter(plugin, PacketType.Play.Client.USE_ENTITY) {
-                    @Override
-                    public void onPacketReceiving(PacketEvent event) {
-                        PacketContainer packet = event.getPacket();
-
-                        int id = packet.getIntegers().read(0);
-                        com.comphenix.protocol.wrappers.WrappedEnumEntityUseAction useAction = packet.getEnumEntityUseActions().read(0);
-                        if(useAction == null) return;
-                        EnumWrappers.Hand hand = useAction.getHand();
-                        EnumWrappers.EntityUseAction action = packet.getEntityUseActions().readSafely(0);
-                        if(action == null) return;
-                        if(hand == EnumWrappers.Hand.MAIN_HAND && action == EnumWrappers.EntityUseAction.INTERACT){
-                            handleEntityClick(event.getPlayer(), id, NPCClickAction.INTERACT);
-                            return;
-                        }
-                        if(hand == EnumWrappers.Hand.MAIN_HAND && action == EnumWrappers.EntityUseAction.ATTACK){
-                            handleEntityClick(event.getPlayer(), id, NPCClickAction.ATTACK);
-                            return;
-                        }
-
-                    }
-                }
-        );
+
+    }
+
+    /**
+     * Registers a new NPC with {@link EntityType} set as {@link Mannequin} with attributes
+     * @param displayName The name of the NPC
+     * @param textureValue The texture of the NPC
+     * @param textureSignature The signature of the NPC's texture
+     * @param location The location to spawn it in
+     */
+    public void addNPC(String displayName, String textureValue, String textureSignature, Location location ){
+        NPCOptions npcOptions = new NPCOptions()
+                .setName(displayName)
+                .setTexture(textureValue)
+                .setSignature(textureSignature)
+                .setLocation(location);
+
+        NPC npc = new DummyNPC(npcOptions);
+    }
+
+    /**
+     * Registers a new NPC
+     * @param npcOptions The NPC's options
+     */
+    public void addNPC(NPCOptions npcOptions){
+        NPC npc = new DummyNPC(npcOptions);
+    }
+
+    @EventHandler
+    public void onEntityDamageByEntityEvent(EntityDamageByEntityEvent e) {
+        if(!(e.getDamager() instanceof Player player)) return;
+
+        NPC npc = getNPC(e.getEntity());
+        if(npc == null) return;
+
+        e.setCancelled(true);
+        if(player.isSneaking()){
+            npc.onShiftLeftClick(player);
+        }else{
+            npc.onLeftClick(player);
+        }
+    }
+    @EventHandler
+    public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent e) {
+        Player player = e.getPlayer();
+
+        NPC npc = getNPC(e.getRightClicked());
+        if(npc == null) return;
+
+        if(e.getHand() != EquipmentSlot.HAND) return;
+        if(player.isSneaking()){
+            npc.onShiftRightClick(player);
+        }else{
+            npc.onRightClick(player);
+        }
+        e.setCancelled(true);
+    }
+
+    @EventHandler(ignoreCancelled = true)
+    public void onPlayerMove(PlayerMoveEvent event) {
+
+        // Ignore pure rotation (huge performance gain)
+        if (event.getFrom().getX() == event.getTo().getX()
+                && event.getFrom().getY() == event.getTo().getY()
+                && event.getFrom().getZ() == event.getTo().getZ()) {
+            return;
+        }
+
+        Player player = event.getPlayer();
+        Location playerLocation = player.getEyeLocation();
+
+        for (NPC npc : npcs.values()) {
+            if(!npc.getOptions().isLookingAtNearest()) return;
+            Location npcLocation = npc.getEntity().getLocation();
+
+            // Skip different worlds immediately
+            if (!npcLocation.getWorld().equals(playerLocation.getWorld())) {
+                continue;
+            }
+
+            // Use squared distance (NO sqrt → much faster)
+            if (npcLocation.distanceSquared(playerLocation) > FOCUS_RANGE_SQUARED) {
+                continue;
+            }
+
+            // Optional: Visibility check if really needed
+            if (!player.canSee(npc.getEntity())) {
+                continue;
+            }
+
+            npc.lookAt(playerLocation);
+        }
     }
 
-    private final Cache<Player, NPC> clickedNPCCache = CacheBuilder.newBuilder()
-            .expireAfterWrite(1L, TimeUnit.SECONDS)
-            .build();
-
-    private void handleEntityClick(Player player, int entityId, NPCClickAction action) {
-        registeredNPCs.stream()
-                .filter(npc -> npc.getId() == entityId)
-                .forEach(npc -> Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> {
-                    /*NPC previouslyClickedNPC = clickedNPCCache.getIfPresent(player);
-                    if (previouslyClickedNPC != null && previouslyClickedNPC.equals(npc)) return; // If they've clicked this same NPC in the last 0.5 seconds ignore this click
-                    clickedNPCCache.put(player, npc);*/
-
-                    NPCInteractionEvent event = new NPCInteractionEvent(npc, player, action);
-                    Bukkit.getPluginManager().callEvent(event);
-                }, 2));
+    private NPC getNPC(Entity entity){
+        UUID entityUUID = entity.getUniqueId();
+        return npcs.getOrDefault(entityUUID, null);
     }
 
-    public NPC newNPC(NPCOptions options) {
-        NPC npc = new NPC_1_21_10(options.getName(), options.getLocation());
-        registeredNPCs.add(npc);
-        return npc;
+    public void registerNPC(NPC toRegister){
+        npcs.put(toRegister.getEntity().getUniqueId(), toRegister);
     }
 
-    public Optional<NPC> findNPC(String name) {
-        return registeredNPCs.stream()
-                .filter(npc -> npc.getName().equalsIgnoreCase(name))
-                .findFirst();
+    public NPC findByUUID(String uuid){
+        UUID manUUID = UUID.fromString(uuid);
+        return npcs.get(manUUID);
     }
 
-    public void deleteNPC(NPC npc) {
-        npc.delete();
-        registeredNPCs.remove(npc);
+    public Collection<UUID> getRegisteredNPCsUUID() {
+        return npcs.keySet();
     }
 
-    public void deleteAllNPCs() {
-        // Copy the set to prevent concurrent modification exception
-        Set<NPC> npcsCopy = new HashSet<>(registeredNPCs);
-        npcsCopy.forEach(this::deleteNPC);
+    public void deleteNPCByUUIDString(String uuid) {
+        npcs.remove(UUID.fromString(uuid)).getEntity().remove();
     }
-    public NPC getNPCbyId(int id) {
-        if(registeredNPCs.isEmpty()) return null;
-        Optional<NPC> optionalNPC = registeredNPCs.stream().filter(npc -> npc.getId() == id).findFirst();
-        return optionalNPC.orElse(null);
+
+    public void renameNPC(UUID entityUUID, String newName) {
+        npcs.get(entityUUID).rename(newName);
     }
-}
+}

+ 60 - 0
src/main/java/me/lethunderhawk/npc/util/MojangAPI.java

@@ -0,0 +1,60 @@
+package me.lethunderhawk.npc.util;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+
+public class MojangAPI {
+
+    private static final String PROFILE_URL = "https://api.mojang.com/users/profiles/minecraft/<name>";
+    private static final String SESSION_URL = "https://sessionserver.mojang.com/session/minecraft/profile/<uuid>?unsigned=false";
+
+    public static String[] getSkinDataFromName(String name){
+        if(name == null) return null;
+        JsonObject data = getJsonData(PROFILE_URL.replace("<name>", name));
+        String id = data.get("id").getAsString();
+        return getSkinDataFromUUID(id);
+    }
+
+
+    public static String[] getSkinDataFromUUID(String uuid) {
+        if(uuid == null) return null;
+
+        JsonObject data = getJsonData(SESSION_URL.replace("<uuid>", uuid));
+        if(data == null) return null;
+
+        List<JsonElement> properties = data.getAsJsonArray("properties").asList();
+        for(JsonElement element : properties) {
+            JsonObject json = element.getAsJsonObject();
+            String name = json.get("name").getAsString();
+            if(name == null || !name.equals("textures")) continue;
+
+            String texture = json.get("value").getAsString();
+            String signature = json.get("signature").getAsString();
+            return new String[]{texture, signature};
+        }
+
+        return null;
+    }
+
+    public static JsonObject getJsonData(String urlString){
+        try{
+            URL url = URI.create(urlString).toURL();
+            URLConnection connection = url.openConnection();
+            connection.connect();
+
+            InputStreamReader reader = new InputStreamReader((InputStream) connection.getContent());
+            JsonObject data = JsonParser.parseReader(reader).getAsJsonObject();
+            return data;
+        }catch(Exception e){
+        }
+        return null;
+    }
+}

+ 0 - 5
src/main/java/me/lethunderhawk/npc/util/NMSHelper.java

@@ -1,5 +0,0 @@
-package me.lethunderhawk.npc.util;
-
-public class NMSHelper {
-
-}

+ 0 - 13
src/main/java/me/lethunderhawk/npc/util/NPC.java

@@ -1,13 +0,0 @@
-package me.lethunderhawk.npc.util;
-
-import org.bukkit.Location;
-import org.bukkit.entity.Player;
-
-public interface NPC {
-    String getName();
-    void showTo(Player player);
-    void hideFrom(Player player);
-    void delete();
-    Location getLocation();
-    int getId();
-}

+ 0 - 58
src/main/java/me/lethunderhawk/npc/util/NPCOptions.java

@@ -1,58 +0,0 @@
-package me.lethunderhawk.npc.util;
-
-import org.bukkit.Location;
-
-public class NPCOptions {
-    public String getName() {
-        return name;
-    }
-
-    public String getTexture() {
-        return texture;
-    }
-
-    public String getSignature() {
-        return signature;
-    }
-
-    public Location getLocation() {
-        return location;
-    }
-
-    public boolean isHideNametag() {
-        return hideNametag;
-    }
-
-
-
-    private String name;
-    private String texture;
-    private String signature;
-    private Location location;
-    private boolean hideNametag;
-
-    public NPCOptions setName(String name) {
-        this.name = name;
-        return this;
-    }
-
-    public NPCOptions setTexture(String texture) {
-        this.texture = texture;
-        return this;
-    }
-
-    public NPCOptions setSignature(String signature) {
-        this.signature = signature;
-        return this;
-    }
-
-    public NPCOptions setLocation(Location location) {
-        this.location = location;
-        return this;
-    }
-
-    public NPCOptions setHideNametag(boolean hideNametag) {
-        this.hideNametag = hideNametag;
-        return this;
-    }
-}

+ 0 - 22
src/main/java/me/lethunderhawk/npc/util/string/StringUtility.java

@@ -1,22 +0,0 @@
-package me.lethunderhawk.npc.util.string;
-
-import java.util.Random;
-
-public class StringUtility {
-    public static String randomCharacters(int length) {
-        if (length < 1) {
-            throw new IllegalArgumentException("Invalid length. Length must be at least 1 characters");
-        }
-
-        int leftLimit = 97; // letter 'a'
-        int rightLimit = 122; // letter 'z'
-        Random random = new Random();
-        StringBuilder buffer = new StringBuilder(length);
-        for (int i = 0; i < length; i++) {
-            int randomLimitedInt = leftLimit + (int)
-                    (random.nextFloat() * (rightLimit - leftLimit + 1));
-            buffer.append((char) randomLimitedInt);
-        }
-        return buffer.toString();
-    }
-}

+ 0 - 173
src/main/java/me/lethunderhawk/npc/util/versioned/NPC_1_21_10.java

@@ -1,173 +0,0 @@
-package me.lethunderhawk.npc.util.versioned;
-
-import com.mojang.authlib.GameProfile;
-import me.lethunderhawk.npc.util.NPC;
-import net.minecraft.network.Connection;
-import net.minecraft.network.protocol.Packet;
-import net.minecraft.network.protocol.PacketFlow;
-import net.minecraft.network.protocol.game.ClientGamePacketListener;
-import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
-import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.level.ClientInformation;
-import net.minecraft.server.level.ServerEntity;
-import net.minecraft.server.level.ServerLevel;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.server.network.CommonListenerCookie;
-import net.minecraft.server.network.ServerGamePacketListenerImpl;
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
-import org.bukkit.craftbukkit.v1_21_R6.CraftServer;
-import org.bukkit.craftbukkit.v1_21_R6.CraftWorld;
-import org.bukkit.craftbukkit.v1_21_R6.entity.CraftPlayer;
-import org.bukkit.entity.Player;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.Predicate;
-
-public class NPC_1_21_10 implements NPC {
-    private final UUID uuid = UUID.randomUUID();
-    private final String name;
-
-    private ServerPlayer npc;
-    private ServerLevel level;
-
-    private final Set<UUID> viewers = new HashSet<>();
-    private MinecraftServer server;
-
-    public NPC_1_21_10(String name, Location location) {
-        this.name = name;
-        spawn(location);
-    }
-
-    // -----------------------------
-    // Core spawn logic
-    // -----------------------------
-    private void spawn(Location location) {
-        if (location.getWorld() == null) {
-            throw new IllegalArgumentException("World cannot be null");
-        }
-
-        this.server =
-                ((CraftServer) Bukkit.getServer()).getServer();
-
-        this.level =
-                ((CraftWorld) location.getWorld()).getHandle();
-
-        GameProfile profile = new GameProfile(uuid, name);
-        this.npc = new ServerPlayer(
-                server,
-                level,
-                profile,
-                ClientInformation.createDefault()
-        );
-
-        npc.setPos(
-                location.getX(),
-                location.getY(),
-                location.getZ()
-        );
-
-        npc.setYRot(location.getYaw());
-        npc.setXRot(location.getPitch());
-    }
-
-    // -----------------------------
-    // Show / hide
-    // -----------------------------
-    public void showTo(Player viewer) {
-        if (!viewers.add(viewer.getUniqueId())) return;
-        npc.connection = new ServerGamePacketListenerImpl(
-                server,
-                new Connection(PacketFlow.SERVERBOUND),
-                npc,
-                CommonListenerCookie.createInitial(npc.getGameProfile(), false)
-        );
-
-        ServerGamePacketListenerImpl conn = ((CraftPlayer) viewer).getHandle().connection;
-        conn.send(new ClientboundPlayerInfoUpdatePacket(
-                ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, npc
-        ));
-
-        // Spawn the NPC entity
-        ServerEntity spawnData = new ServerEntity(
-                npc.level(), npc, 0, false, this.getDummySynchronizer(), Set.of()
-        );
-        conn.send(npc.getAddEntityPacket(spawnData));
-    }
-
-
-
-    public void hideFrom(org.bukkit.entity.Player player) {
-        if (!viewers.remove(player.getUniqueId())) return;
-
-        ((CraftPlayer) player).getHandle()
-                .connection
-                .send(
-                        new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID()))
-                );
-    }
-
-    public void delete() {
-        viewers.stream()
-                .map(Bukkit::getPlayer)
-                .filter(p -> p != null && p.isOnline())
-                .forEach(this::hideFrom);
-
-        viewers.clear();
-    }
-
-    // -----------------------------
-    // Getters
-    // -----------------------------
-    public int getEntityId() {
-        return npc.getId();
-    }
-
-    public Location getLocation() {
-        return new Location(
-                level.getWorld(),
-                npc.getX(),
-                npc.getY(),
-                npc.getZ(),
-                npc.getYRot(),
-                npc.getXRot()
-        );
-    }
-
-    private ServerEntity.Synchronizer getDummySynchronizer() {
-        return new ServerEntity.Synchronizer() {
-            @Override
-            public void sendToTrackingPlayers(Packet<? super ClientGamePacketListener> packet) {
-
-            }
-
-            @Override
-            public void sendToTrackingPlayersAndSelf(Packet<? super ClientGamePacketListener> packet) {
-
-            }
-
-            @Override
-            public void sendToTrackingPlayersFiltered(Packet<? super ClientGamePacketListener> packet, Predicate<ServerPlayer> predicate) {
-
-            }
-
-            @Override
-            public void sendToTrackingPlayersFilteredAndSelf(Packet<? super ClientGamePacketListener> packet, Predicate<ServerPlayer> predicate) {
-
-            }
-        };
-    }
-
-    @Override
-    public int getId() {
-        return npc.getId();
-    }
-
-    public String getName() {
-        return name;
-    }
-}

+ 16 - 2
src/main/resources/plugin.yml

@@ -1,5 +1,19 @@
 name: FluxAPI
-version: '${project.version}'
-main: me.lethunderhawk.main.Main
+version: ${project.version}
+main: me.lethunderhawk.main.FluxAPI
 api-version: 1.21.10
 prefix: FluxAPI
+depend:
+  - ProtocolLib
+commands:
+  npc:
+    description: Create a NPC
+    usage: /npc <next>
+
+permissions:
+  fluxapi.commands:
+    description: Allows usage of the base NPC command
+    default: op
+  fluxapi.delete:
+    description: Allows usage of deleting NPC command
+    default: op