From 0b1e4f9ed41dce12929665d1813e6373a64df9ff Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Mar 2026 05:20:11 -0500 Subject: [PATCH] Huge Overhaul to Fullscreen Modes + Folder Support for: - Animations - Abilities - Forms - Auras - Outlines - Effects - Linked Items --- .../npcs/controllers/AbilityController.java | 323 ++++-- .../kamkeel/npcs/network/PacketHandler.java | 14 + .../npcs/network/enums/EnumRequestPacket.java | 7 + .../category/AbilityCategoryMovePacket.java | 75 ++ .../category/CategoryItemsRequestPacket.java | 82 ++ .../category/CategoryListRequestPacket.java | 64 ++ .../category/CategoryMoveItemPacket.java | 74 ++ .../category/CategoryRemovePacket.java | 70 ++ .../request/category/CategorySavePacket.java | 107 ++ .../npcs/client/gui/SubGuiEffectGeneral.java | 6 +- .../gui/global/GuiAbilityDirectory.java | 996 ++++++++++++++++++ .../gui/global/GuiAnimationDirectory.java | 523 +++++++++ .../client/gui/global/GuiEffectDirectory.java | 193 ++++ .../gui/global/GuiLinkedItemDirectory.java | 285 +++++ .../gui/global/GuiNPCManageAnimations.java | 22 +- .../gui/global/GuiNPCManageEffects.java | 19 +- .../client/gui/global/GuiNPCManageLinked.java | 15 + .../gui/global/GuiNpcManageAbilities.java | 31 +- .../client/gui/item/SubGuiLinkedItem.java | 4 +- .../gui/util/GuiDirectoryCategorized.java | 755 +++++++++++++ .../npcs/constants/EnumCategoryType.java | 13 + .../noppes/npcs/constants/EnumScrollData.java | 4 +- .../npcs/controllers/AnimationController.java | 150 +-- .../npcs/controllers/CategoryManager.java | 248 +++++ .../controllers/CustomEffectController.java | 147 +-- .../controllers/LinkedItemController.java | 128 ++- .../npcs/controllers/data/Category.java | 31 + .../assets/customnpcs/lang/en_US.lang | 3 + .../customnpcs/textures/gui/directory.png | Bin 0 -> 547 bytes 29 files changed, 4110 insertions(+), 279 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/category/AbilityCategoryMovePacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/category/CategoryListRequestPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/category/CategoryMoveItemPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/category/CategoryRemovePacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/category/CategorySavePacket.java create mode 100644 src/main/java/noppes/npcs/client/gui/global/GuiAbilityDirectory.java create mode 100644 src/main/java/noppes/npcs/client/gui/global/GuiAnimationDirectory.java create mode 100644 src/main/java/noppes/npcs/client/gui/global/GuiEffectDirectory.java create mode 100644 src/main/java/noppes/npcs/client/gui/global/GuiLinkedItemDirectory.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/GuiDirectoryCategorized.java create mode 100644 src/main/java/noppes/npcs/constants/EnumCategoryType.java create mode 100644 src/main/java/noppes/npcs/controllers/CategoryManager.java create mode 100644 src/main/java/noppes/npcs/controllers/data/Category.java create mode 100644 src/main/resources/assets/customnpcs/textures/gui/directory.png diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index f365aadd1..08586669f 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -51,6 +51,7 @@ import noppes.npcs.LogWriter; import noppes.npcs.api.ability.IChainedAbility; import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.CategoryManager; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.util.NBTJsonUtil; import kamkeel.npcs.util.FileNameHelper; @@ -88,6 +89,12 @@ public class AbilityController implements IAbilityHandler { private final Map chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility private int chainedAbilityRevision = 0; + // ── Category Managers ───────────────────────────────────────────────── + public final CategoryManager customAbilityCategories = new CategoryManager(); + public final CategoryManager chainedAbilityCategories = new CategoryManager(); + private final Map customAbilityCatMap = new HashMap<>(); // abilityName → catId + private final Map chainedAbilityCatMap = new HashMap<>(); // chainName → catId + // ── Effect Action Registry ────────────────────────────────────────────── private final Map effectActions = new LinkedHashMap<>(); @@ -222,51 +229,18 @@ public class AbilityController implements IAbilityHandler { customAbilities.clear(); customAbilitiesById.clear(); abilityScriptHandlers.clear(); + customAbilityCatMap.clear(); File dir = getDir(); - File[] files = dir.exists() ? dir.listFiles() : null; - if (files != null) { - for (File file : files) { - if (!file.isFile() || !file.getName().endsWith(".json")) continue; - try { - String filename = file.getName(); - String fileKey = filename.substring(0, filename.length() - 5); + customAbilityCategories.loadCategories(dir, "chained"); - NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); - Ability ability = fromNBT(nbt); - if (ability == null) continue; + // Load from root directory (Uncategorized) + loadAbilitiesFromDir(dir, CategoryManager.UNCATEGORIZED_ID); - String name = FileNameHelper.sanitizeName(fileKey, "Ability"); - name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey); - - boolean dirty = false; - if (!name.equals(fileKey)) { - File renamedFile = new File(dir, name + ".json"); - if (file.renameTo(renamedFile)) { - file = renamedFile; - dirty = true; - } - } - - ability.setName(name); - - String uuid = ability.getId(); - if (uuid == null || uuid.isEmpty()) { - uuid = UUID.randomUUID().toString(); - ability.setId(uuid); - dirty = true; - } - - if (dirty) { - NBTJsonUtil.SaveFile(file, ability.writeNBT(true)); - } - - customAbilities.put(name, ability); - customAbilitiesById.put(uuid, ability); - } catch (Exception e) { - LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); - } - } + // Load from category subdirectories + for (Map.Entry entry : customAbilityCategories.getCategories().entrySet()) { + File catDir = new File(dir, entry.getValue().title); + loadAbilitiesFromDir(catDir, entry.getKey()); } customAbilityRevision++; @@ -275,6 +249,55 @@ public class AbilityController implements IAbilityHandler { loadChainedAbilities(); } + private void loadAbilitiesFromDir(File dir, int catId) { + File[] files = dir.exists() ? dir.listFiles() : null; + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + String filename = file.getName(); + String fileKey = filename.substring(0, filename.length() - 5); + + NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); + Ability ability = fromNBT(nbt); + if (ability == null) continue; + + String name = FileNameHelper.sanitizeName(fileKey, "Ability"); + name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey); + + boolean dirty = false; + if (!name.equals(fileKey)) { + File renamedFile = new File(dir, name + ".json"); + if (file.renameTo(renamedFile)) { + file = renamedFile; + dirty = true; + } + } + + ability.setName(name); + + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); + dirty = true; + } + + if (dirty) { + NBTJsonUtil.SaveFile(file, ability.writeNBT(true)); + } + + customAbilities.put(name, ability); + customAbilitiesById.put(uuid, ability); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.put(name, catId); + } + } catch (Exception e) { + LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); + } + } + } + private boolean nameFileExists(File dir, String name, File ignoredFile) { File existing = new File(dir, name + ".json"); if (!existing.exists()) return false; @@ -307,6 +330,7 @@ public class AbilityController implements IAbilityHandler { String uuid = ability.getId(); boolean isNew = (uuid == null || uuid.isEmpty()); + int catId = CategoryManager.UNCATEGORIZED_ID; if (isNew) { // Generate UUID for new ability @@ -321,16 +345,23 @@ public class AbilityController implements IAbilityHandler { Ability existing = customAbilitiesById.get(uuid); if (existing != null) { String oldName = existing.getName(); + catId = customAbilityCatMap.getOrDefault(oldName, CategoryManager.UNCATEGORIZED_ID); name = makeUniqueNameForMap(customAbilities, name, oldName); ability.setName(name); if (!oldName.equals(name)) { // Delete old file and remove old map entry - File oldFile = new File(getDir(), oldName + ".json"); + File oldDir = getCustomAbilityDir(oldName); + File oldFile = new File(oldDir, oldName + ".json"); if (oldFile.exists()) { oldFile.delete(); } customAbilities.remove(oldName); + // Update catMap key + customAbilityCatMap.remove(oldName); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.put(name, catId); + } } } else { name = makeUniqueNameForMap(customAbilities, name, null); @@ -338,7 +369,7 @@ public class AbilityController implements IAbilityHandler { } } - File dir = getDir(); + File dir = getCustomAbilityDir(name); File fileNew = new File(dir, name + ".json_new"); File fileCurrent = new File(dir, name + ".json"); @@ -389,11 +420,12 @@ public class AbilityController implements IAbilityHandler { abilityScriptHandlers.remove(uuid); } - File dir = getDir(); + File dir = getCustomAbilityDir(name); File file = new File(dir, name + ".json"); if (file.exists()) { file.delete(); } + customAbilityCatMap.remove(name); // Clean up online players' unlocked ability lists if (PlayerDataController.Instance != null) { @@ -465,6 +497,98 @@ public class AbilityController implements IAbilityHandler { return dir; } + private File getCustomAbilityDir(String name) { + int catId = customAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + return customAbilityCategories.getCategoryDir(catId); + } + + private File getChainedAbilityDir(String name) { + int catId = chainedAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + return chainedAbilityCategories.getCategoryDir(catId); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CUSTOM ABILITY CATEGORY HELPERS + // ═══════════════════════════════════════════════════════════════════════════ + + public Map getCustomAbilityCategoryScrollData() { + return customAbilityCategories.getCategoryScrollData(); + } + + public Map getCustomAbilityItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + for (Map.Entry entry : customAbilities.entrySet()) { + int assignedCat = customAbilityCatMap.getOrDefault(entry.getKey(), CategoryManager.UNCATEGORIZED_ID); + if (assignedCat == catId) { + map.put(entry.getKey(), entry.getValue().getAllowedBy().ordinal()); + } + } + return map; + } + + public void moveCustomAbilityToCategory(String name, int destCatId) { + Ability ability = customAbilities.get(name); + if (ability == null) return; + + int oldCatId = customAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + if (oldCatId == destCatId) return; + + File oldDir = customAbilityCategories.getCategoryDir(oldCatId); + File newDir = customAbilityCategories.getCategoryDir(destCatId); + if (!newDir.exists()) newDir.mkdirs(); + + File oldFile = new File(oldDir, name + ".json"); + File newFile = new File(newDir, name + ".json"); + if (oldFile.exists()) oldFile.renameTo(newFile); + + if (destCatId == CategoryManager.UNCATEGORIZED_ID) { + customAbilityCatMap.remove(name); + } else { + customAbilityCatMap.put(name, destCatId); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CHAINED ABILITY CATEGORY HELPERS + // ═══════════════════════════════════════════════════════════════════════════ + + public Map getChainedAbilityCategoryScrollData() { + return chainedAbilityCategories.getCategoryScrollData(); + } + + public Map getChainedAbilityItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + for (Map.Entry entry : chainedAbilities.entrySet()) { + int assignedCat = chainedAbilityCatMap.getOrDefault(entry.getKey(), CategoryManager.UNCATEGORIZED_ID); + if (assignedCat == catId) { + map.put(entry.getKey(), entry.getValue().getAllowedBy().ordinal()); + } + } + return map; + } + + public void moveChainedAbilityToCategory(String name, int destCatId) { + ChainedAbility chain = chainedAbilities.get(name); + if (chain == null) return; + + int oldCatId = chainedAbilityCatMap.getOrDefault(name, CategoryManager.UNCATEGORIZED_ID); + if (oldCatId == destCatId) return; + + File oldDir = chainedAbilityCategories.getCategoryDir(oldCatId); + File newDir = chainedAbilityCategories.getCategoryDir(destCatId); + if (!newDir.exists()) newDir.mkdirs(); + + File oldFile = new File(oldDir, name + ".json"); + File newFile = new File(newDir, name + ".json"); + if (oldFile.exists()) oldFile.renameTo(newFile); + + if (destCatId == CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.remove(name); + } else { + chainedAbilityCatMap.put(name, destCatId); + } + } + // ═══════════════════════════════════════════════════════════════════════════ // CHAINED ABILITIES // ═══════════════════════════════════════════════════════════════════════════ @@ -473,57 +597,73 @@ public class AbilityController implements IAbilityHandler { chainedAbilities.clear(); chainedAbilitiesById.clear(); chainedAbilityScriptHandlers.clear(); + chainedAbilityCatMap.clear(); File dir = getChainedDir(); - File[] files = dir.exists() ? dir.listFiles() : null; - if (files != null) { - for (File file : files) { - if (!file.isFile() || !file.getName().endsWith(".json")) continue; - try { - String filename = file.getName(); - String fileKey = filename.substring(0, filename.length() - 5); + chainedAbilityCategories.loadCategories(dir); - NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); - ChainedAbility chain = new ChainedAbility(); - chain.readNBT(nbt); + // Load from root directory (Uncategorized) + loadChainedFromDir(dir, CategoryManager.UNCATEGORIZED_ID); - String name = FileNameHelper.sanitizeName(fileKey, "Chain"); - name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey); - - boolean dirty = false; - if (!name.equals(fileKey)) { - File renamedFile = new File(dir, name + ".json"); - if (file.renameTo(renamedFile)) { - file = renamedFile; - dirty = true; - } - } - - chain.setName(name); - - String uuid = chain.getId(); - if (uuid == null || uuid.isEmpty()) { - uuid = UUID.randomUUID().toString(); - chain.setId(uuid); - dirty = true; - } - - if (dirty) { - NBTJsonUtil.SaveFile(file, chain.writeNBT(true)); - } - - chainedAbilities.put(name, chain); - chainedAbilitiesById.put(uuid, chain); - } catch (Exception e) { - LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e); - } - } + // Load from category subdirectories + for (Map.Entry entry : chainedAbilityCategories.getCategories().entrySet()) { + File catDir = new File(dir, entry.getValue().title); + loadChainedFromDir(catDir, entry.getKey()); } chainedAbilityRevision++; LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities"); } + private void loadChainedFromDir(File dir, int catId) { + File[] files = dir.exists() ? dir.listFiles() : null; + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + String filename = file.getName(); + String fileKey = filename.substring(0, filename.length() - 5); + + NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + + String name = FileNameHelper.sanitizeName(fileKey, "Chain"); + name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey); + + boolean dirty = false; + if (!name.equals(fileKey)) { + File renamedFile = new File(dir, name + ".json"); + if (file.renameTo(renamedFile)) { + file = renamedFile; + dirty = true; + } + } + + chain.setName(name); + + String uuid = chain.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + chain.setId(uuid); + dirty = true; + } + + if (dirty) { + NBTJsonUtil.SaveFile(file, chain.writeNBT(true)); + } + + chainedAbilities.put(name, chain); + chainedAbilitiesById.put(uuid, chain); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.put(name, catId); + } + } catch (Exception e) { + LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e); + } + } + } + public boolean saveChainedAbility(ChainedAbility chain) { if (chain == null) return false; @@ -543,15 +683,21 @@ public class AbilityController implements IAbilityHandler { ChainedAbility existing = chainedAbilitiesById.get(uuid); if (existing != null) { String oldName = existing.getName(); + int catId = chainedAbilityCatMap.getOrDefault(oldName, CategoryManager.UNCATEGORIZED_ID); name = makeUniqueNameForMap(chainedAbilities, name, oldName); chain.setName(name); if (!oldName.equals(name)) { - File oldFile = new File(getChainedDir(), oldName + ".json"); + File oldDir = getChainedAbilityDir(oldName); + File oldFile = new File(oldDir, oldName + ".json"); if (oldFile.exists()) { oldFile.delete(); } chainedAbilities.remove(oldName); + chainedAbilityCatMap.remove(oldName); + if (catId > CategoryManager.UNCATEGORIZED_ID) { + chainedAbilityCatMap.put(name, catId); + } } } else { name = makeUniqueNameForMap(chainedAbilities, name, null); @@ -559,7 +705,7 @@ public class AbilityController implements IAbilityHandler { } } - File dir = getChainedDir(); + File dir = getChainedAbilityDir(name); File fileNew = new File(dir, name + ".json_new"); File fileCurrent = new File(dir, name + ".json"); @@ -609,11 +755,12 @@ public class AbilityController implements IAbilityHandler { chainedAbilityScriptHandlers.remove(uuid); } - File dir = getChainedDir(); + File dir = getChainedAbilityDir(name); File file = new File(dir, name + ".json"); if (file.exists()) { file.delete(); } + chainedAbilityCatMap.remove(name); // Clean up online players' unlocked ability lists (chain keys use "chain:" prefix) if (PlayerDataController.Instance != null) { diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 7c73a3edb..71b8a0092 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -92,6 +92,12 @@ import kamkeel.npcs.network.packets.request.ability.ChainedAbilityGetPacket; import kamkeel.npcs.network.packets.request.ability.ChainedAbilityRemovePacket; import kamkeel.npcs.network.packets.request.ability.ChainedAbilitySavePacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.category.AbilityCategoryMovePacket; +import kamkeel.npcs.network.packets.request.category.CategoryItemsRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryListRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryMoveItemPacket; +import kamkeel.npcs.network.packets.request.category.CategoryRemovePacket; +import kamkeel.npcs.network.packets.request.category.CategorySavePacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; @@ -526,6 +532,14 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new ChainedAbilitySavePacket()); REQUEST_PACKET.registerPacket(new ChainedAbilityRemovePacket()); + // Category Packets + REQUEST_PACKET.registerPacket(new CategorySavePacket()); + REQUEST_PACKET.registerPacket(new CategoryRemovePacket()); + REQUEST_PACKET.registerPacket(new CategoryMoveItemPacket()); + REQUEST_PACKET.registerPacket(new CategoryListRequestPacket()); + REQUEST_PACKET.registerPacket(new CategoryItemsRequestPacket()); + REQUEST_PACKET.registerPacket(new AbilityCategoryMovePacket()); + // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); REQUEST_PACKET.registerPacket(new GuiRequestPacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 03e605af7..11b330762 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -216,4 +216,11 @@ public enum EnumRequestPacket { ManageAuctionAction, AbilityScript, ChainedAbilityScript, + + CategorySave, + CategoryRemove, + CategoryMoveItem, + CategoryListRequest, + CategoryItemsRequest, + AbilityCategoryMoveItem, } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/AbilityCategoryMovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/AbilityCategoryMovePacket.java new file mode 100644 index 000000000..e498ed4a2 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/AbilityCategoryMovePacket.java @@ -0,0 +1,75 @@ +package kamkeel.npcs.network.packets.request.category; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.constants.EnumCategoryType; + +import java.io.IOException; + +public final class AbilityCategoryMovePacket extends AbstractPacket { + public static String packetName = "NPC|AbCatMove"; + + private int catType; + private String name; + private int catId; + + public AbilityCategoryMovePacket(int catType, String name, int catId) { + this.catType = catType; + this.name = name; + this.catId = catId; + } + + public AbilityCategoryMovePacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.AbilityCategoryMoveItem; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(catType); + ByteBufUtils.writeString(out, name); + out.writeInt(catId); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + int type = in.readInt(); + String itemName = ByteBufUtils.readString(in); + int cat = in.readInt(); + + switch (type) { + case EnumCategoryType.ABILITY: + AbilityController.Instance.moveCustomAbilityToCategory(itemName, cat); + break; + case EnumCategoryType.CHAINED_ABILITY: + AbilityController.Instance.moveChainedAbilityToCategory(itemName, cat); + break; + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java new file mode 100644 index 000000000..ec7a52d97 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryItemsRequestPacket.java @@ -0,0 +1,82 @@ +package kamkeel.npcs.network.packets.request.category; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import kamkeel.npcs.controllers.AbilityController; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.LinkedItemController; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public final class CategoryItemsRequestPacket extends AbstractPacket { + public static String packetName = "NPC|CatItems"; + + private int catType; + private int catId; + + public CategoryItemsRequestPacket(int catType, int catId) { + this.catType = catType; + this.catId = catId; + } + + public CategoryItemsRequestPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CategoryItemsRequest; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(catType); + out.writeInt(catId); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + int type = in.readInt(); + int categoryId = in.readInt(); + + Map items = getItemsByCat(type, categoryId); + ScrollDataPacket.sendScrollData((EntityPlayerMP) player, items, EnumScrollData.CATEGORY_GROUP); + } + + private static Map getItemsByCat(int type, int catId) { + switch (type) { + case EnumCategoryType.EFFECT: return CustomEffectController.getInstance().getItemsByCategoryScrollData(catId); + case EnumCategoryType.ANIMATION: return AnimationController.getInstance().getItemsByCategoryScrollData(catId); + case EnumCategoryType.LINKED_ITEM: return LinkedItemController.getInstance().getItemsByCategoryScrollData(catId); + case EnumCategoryType.ABILITY: return AbilityController.Instance.getCustomAbilityItemsByCategoryScrollData(catId); + case EnumCategoryType.CHAINED_ABILITY: return AbilityController.Instance.getChainedAbilityItemsByCategoryScrollData(catId); + } + return new HashMap<>(); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryListRequestPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryListRequestPacket.java new file mode 100644 index 000000000..303e2ed08 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryListRequestPacket.java @@ -0,0 +1,64 @@ +package kamkeel.npcs.network.packets.request.category; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.CategoryManager; + +import java.io.IOException; + +public final class CategoryListRequestPacket extends AbstractPacket { + public static String packetName = "NPC|CatList"; + + private int catType; + + public CategoryListRequestPacket(int catType) { + this.catType = catType; + } + + public CategoryListRequestPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CategoryListRequest; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(catType); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + int type = in.readInt(); + + CategoryManager cm = CategorySavePacket.getManager(type); + if (cm == null) + return; + + ScrollDataPacket.sendScrollData((EntityPlayerMP) player, cm.getCategoryScrollData(), EnumScrollData.CATEGORY_LIST); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryMoveItemPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryMoveItemPacket.java new file mode 100644 index 000000000..0aed23e7e --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryMoveItemPacket.java @@ -0,0 +1,74 @@ +package kamkeel.npcs.network.packets.request.category; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import kamkeel.npcs.controllers.AbilityController; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.LinkedItemController; + +import java.io.IOException; + +public final class CategoryMoveItemPacket extends AbstractPacket { + public static String packetName = "NPC|CatMove"; + + private int catType; + private int itemId; + private int catId; + + public CategoryMoveItemPacket(int catType, int itemId, int catId) { + this.catType = catType; + this.itemId = itemId; + this.catId = catId; + } + + public CategoryMoveItemPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CategoryMoveItem; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(catType); + out.writeInt(itemId); + out.writeInt(catId); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + int type = in.readInt(); + int item = in.readInt(); + int cat = in.readInt(); + + switch (type) { + case EnumCategoryType.EFFECT: CustomEffectController.getInstance().moveItemToCategory(item, cat); break; + case EnumCategoryType.ANIMATION: AnimationController.getInstance().moveItemToCategory(item, cat); break; + case EnumCategoryType.LINKED_ITEM: LinkedItemController.getInstance().moveItemToCategory(item, cat); break; + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryRemovePacket.java new file mode 100644 index 000000000..ecc9e6652 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategoryRemovePacket.java @@ -0,0 +1,70 @@ +package kamkeel.npcs.network.packets.request.category; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.CategoryManager; + +import java.io.IOException; + +public final class CategoryRemovePacket extends AbstractPacket { + public static String packetName = "NPC|CatRemove"; + + private int catType; + private int categoryId; + + public CategoryRemovePacket(int catType, int categoryId) { + this.catType = catType; + this.categoryId = categoryId; + } + + public CategoryRemovePacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CategoryRemove; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(catType); + out.writeInt(categoryId); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + int type = in.readInt(); + int catId = in.readInt(); + + CategoryManager cm = CategorySavePacket.getManager(type); + if (cm == null) + return; + + cm.removeCategory(catId); + CategorySavePacket.saveController(type); + ScrollDataPacket.sendScrollData((EntityPlayerMP) player, cm.getCategoryScrollData(), EnumScrollData.CATEGORY_LIST); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/category/CategorySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/category/CategorySavePacket.java new file mode 100644 index 000000000..15eafa3fd --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/category/CategorySavePacket.java @@ -0,0 +1,107 @@ +package kamkeel.npcs.network.packets.request.category; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.ScrollDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import kamkeel.npcs.controllers.AbilityController; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.CategoryManager; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.LinkedItemController; +import noppes.npcs.controllers.data.Category; + +import java.io.IOException; + +public final class CategorySavePacket extends AbstractPacket { + public static String packetName = "NPC|CatSave"; + + private int catType; + private NBTTagCompound categoryNBT; + + public CategorySavePacket(int catType, NBTTagCompound categoryNBT) { + this.catType = catType; + this.categoryNBT = categoryNBT; + } + + public CategorySavePacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CategorySave; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(catType); + ByteBufUtils.writeNBT(out, categoryNBT); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + int type = in.readInt(); + NBTTagCompound compound = ByteBufUtils.readNBT(in); + + CategoryManager cm = getManager(type); + if (cm == null) + return; + + Category cat = new Category(); + cat.readNBT(compound); + + if (cat.id <= 0) { + cm.createCategory(cat.title); + } else { + cm.saveCategory(cat); + } + + saveController(type); + ScrollDataPacket.sendScrollData((EntityPlayerMP) player, cm.getCategoryScrollData(), EnumScrollData.CATEGORY_LIST); + } + + public static CategoryManager getManager(int type) { + switch (type) { + case EnumCategoryType.EFFECT: return CustomEffectController.getInstance().categoryManager; + case EnumCategoryType.ANIMATION: return AnimationController.getInstance().categoryManager; + case EnumCategoryType.LINKED_ITEM: return LinkedItemController.getInstance().categoryManager; + case EnumCategoryType.ABILITY: return AbilityController.Instance.customAbilityCategories; + case EnumCategoryType.CHAINED_ABILITY: return AbilityController.Instance.chainedAbilityCategories; + } + return null; + } + + public static void saveController(int type) { + switch (type) { + case EnumCategoryType.EFFECT: CustomEffectController.getInstance().saveEffectLoadMap(); break; + case EnumCategoryType.ANIMATION: AnimationController.getInstance().saveAnimationMap(); break; + case EnumCategoryType.LINKED_ITEM: LinkedItemController.getInstance().saveLinkedItemsMap(); break; + case EnumCategoryType.ABILITY: break; // abilities save per-file, no bulk save needed + case EnumCategoryType.CHAINED_ABILITY: break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java index ce8aa221d..1b0ce59b2 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java @@ -6,7 +6,7 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.ClientCacheHandler; -import noppes.npcs.client.gui.global.GuiNPCManageEffects; +import net.minecraft.client.gui.GuiScreen; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.renderer.AnimationHelper; import noppes.npcs.client.gui.util.GuiMenuTopButton; @@ -27,13 +27,13 @@ import java.util.List; import static noppes.npcs.client.gui.player.inventory.GuiCNPCInventory.specialIcons; public class SubGuiEffectGeneral extends SubGuiInterface implements ITextfieldListener { - private final GuiNPCManageEffects parent; + private final GuiScreen parent; public CustomEffect effect; private final String originalName; private final List topButtons = new ArrayList<>(); - public SubGuiEffectGeneral(GuiNPCManageEffects parent, CustomEffect effect) { + public SubGuiEffectGeneral(GuiScreen parent, CustomEffect effect) { this.effect = effect; this.parent = parent; this.originalName = effect.name; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiAbilityDirectory.java b/src/main/java/noppes/npcs/client/gui/global/GuiAbilityDirectory.java new file mode 100644 index 000000000..c52a6c0d5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiAbilityDirectory.java @@ -0,0 +1,996 @@ +package noppes.npcs.client.gui.global; + +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.preview.AbilityPreviewExecutor; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.ChainedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.ChainedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.ChainedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; +import kamkeel.npcs.network.packets.request.category.AbilityCategoryMovePacket; +import kamkeel.npcs.network.packets.request.category.CategoryItemsRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryListRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryRemovePacket; +import kamkeel.npcs.network.packets.request.category.CategorySavePacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.ClientEventHandler; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.advanced.SubGuiAbilityTypeSelect; +import noppes.npcs.client.gui.advanced.SubGuiAbilityVariantSelect; +import noppes.npcs.client.gui.advanced.SubGuiChainedAbilityConfig; +import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiDirectoryCategorized; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiTexturedButton; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.Category; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.Vector; + +public class GuiAbilityDirectory extends GuiDirectoryCategorized + implements IAbilityConfigCallback, IChainedAbilityConfigCallback { + + private EntityNPCInterface npc; + private EntityNPCInterface originalNpc; + + // View mode: 0=Built-In, 1=Custom, 2=Chained + private int viewMode = 1; + private boolean currentIsBuiltIn = false; + + // Data caches + private HashMap builtInData = new HashMap<>(); + private Ability selectedAbility = null; + private ChainedAbility selectedChain = null; + + // User type filter: 0=Both, 1=NPC Only, 2=Player Only + private int userTypeFilter = 0; + private static final String[] FILTER_LABELS = {"gui.both", "gui.npcs", "gui.players"}; + + // Pending save state + private Ability pendingSaveAbility = null; + private ChainedAbility pendingSaveChain = null; + private String pendingTypeId = null; + + // Preview playback + private AbilityPreviewExecutor previewExecutor; + private long prevTick = 0; + private double npcStartX, npcStartY, npcStartZ; + private boolean trackingMovement = false; + + // Fixed camera settings (matching GuiAbilityInterface) + private static final float NPC_FACING_YAW = 310f; + private static final float CAMERA_PITCH = 5f; + + public GuiAbilityDirectory(EntityNPCInterface npc) { + super(); + this.originalNpc = npc; + this.npc = createFakeNPC(npc); + rightPanelPercent = 0.38f; + minRightPanelW = 200; + zoomed = 50; + + // Initialize preview executor + previewExecutor = new AbilityPreviewExecutor(); + + // Re-request categories — viewMode was 0 during super() call + requestCategoryList(); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + } + + private static EntityNPCInterface createFakeNPC(EntityNPCInterface original) { + EntityCustomNpc fake = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); + fake.display.readToNBT(original.display.writeToNBT(new NBTTagCompound())); + fake.display.name = "ability preview"; + fake.height = original.height; + fake.width = original.width; + fake.display.animationData.setEnabled(true); + return fake; + } + + private boolean isBuiltInMode() { return viewMode == 0; } + private boolean isCustomMode() { return viewMode == 1; } + private boolean isChainedMode() { return viewMode == 2; } + + private int getCategoryType() { + return isChainedMode() ? EnumCategoryType.CHAINED_ABILITY : EnumCategoryType.ABILITY; + } + + // ========== Layout Overrides ========== + + @Override + protected boolean hasCategories() { return !isBuiltInMode(); } + + @Override + protected void computeLayout() { + if (isBuiltInMode()) { + leftPanelPercent = 0.0f; + minLeftPanelW = 0; + } else { + leftPanelPercent = 0.15f; + minLeftPanelW = 120; + } + super.computeLayout(); + } + + @Override + protected void drawPanels() { + if (isBuiltInMode()) { + // No left panel border + if (rightPanelW > 0) { + GuiUtil.drawRectD(rightX - 1, contentY - 1, rightX + rightPanelW + 1, originY + usableH + 1, panelBorder); + } + } else { + super.drawPanels(); + } + } + + @Override + protected void initLeftPanel() { + if (isBuiltInMode()) return; + super.initLeftPanel(); + } + + // ========== Title ========== + + @Override + protected String getTitle() { + if (isBuiltInMode()) return "Abilities (Built-in)"; + if (isChainedMode()) return "Chained Abilities"; + return "Abilities"; + } + + // ========== Top Bar ========== + + @Override + protected int initExtraTopBarButtons(int x, int topBtnY) { + int topBtnW = 55; + + // View mode toggle: Built-In / Custom / Chained + String[] labels = {"gui.builtin", "gui.custom", "gui.chained"}; + int[] colors = {0x55FFFF, 0xFFFFFF, 0xFFFF55}; + GuiNpcButton viewBtn = new GuiNpcButton(60, x, topBtnY, topBtnW, btnH, labels[viewMode]); + viewBtn.setTextColor(colors[viewMode]); + addButton(viewBtn); + x += topBtnW + 2; + + // User type filter (only for Custom and Built-in, not Chained) + if (!isChainedMode()) { + addButton(new GuiNpcButton(61, x, topBtnY, topBtnW, btnH, FILTER_LABELS[userTypeFilter])); + x += topBtnW + 2; + } + + return x; + } + + @Override + protected void initTopBar(int topBtnY) { + super.initTopBar(topBtnY); + + // Disable category-dependent buttons in built-in mode + if (isBuiltInMode()) { + if (getButton(50) != null) getButton(50).enabled = false; // Add + if (getButton(54) != null) getButton(54).enabled = false; // Move + } + } + + // ========== Category Requests ========== + + @Override + protected void requestCategoryList() { + if (isBuiltInMode()) return; + PacketClient.sendClient(new CategoryListRequestPacket(getCategoryType())); + } + + @Override + protected void requestItemsInCategory(int catId) { + if (isBuiltInMode()) return; + PacketClient.sendClient(new CategoryItemsRequestPacket(getCategoryType(), catId)); + } + + @Override + protected void requestItemData(int itemId) { + // Not used — we override customScrollClicked for name-based requests + } + + @Override + protected void onSaveCategory(Category cat) { + PacketClient.sendClient(new CategorySavePacket(getCategoryType(), cat.writeNBT(new NBTTagCompound()))); + } + + @Override + protected void onRemoveCategory(int catId) { + PacketClient.sendClient(new CategoryRemovePacket(getCategoryType(), catId)); + } + + @Override + protected void onAddItem(int catId) { + if (isBuiltInMode()) return; + if (isChainedMode()) { + ChainedAbility newChain = new ChainedAbility("New Chain"); + setSubGui(new SubGuiChainedAbilityConfig(newChain, this)); + } else { + setSubGui(new SubGuiAbilityTypeSelect()); + } + } + + @Override + protected void onRemoveItem(int itemId) { + // Handled via confirmClicked with name-based packets + } + + @Override + protected void onEditItem() { + stopPreviewPlayback(); + if (isChainedMode() && selectedChain != null) { + setSubGui(new SubGuiChainedAbilityConfig(selectedChain, this)); + } else if (!currentIsBuiltIn && selectedAbility != null) { + setSubGui(selectedAbility.createConfigGui(this)); + } + } + + @Override + protected void onCloneItem() { + // Abilities don't support cloning through this mechanism + } + + @Override + protected void onItemReceived(NBTTagCompound compound) { + if (isChainedMode() && compound.hasKey("Entries")) { + selectedChain = new ChainedAbility(); + selectedChain.readNBT(compound); + setPrevItemName(selectedChain.getName()); + } else { + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + selectedAbility = AbilityController.Instance.fromNBT(compound); + if (selectedAbility != null) { + setPrevItemName(selectedAbility.getName()); + } + } + } + + @Override + protected boolean hasSelectedItem() { + if (isChainedMode()) return selectedChain != null; + if (currentIsBuiltIn) return selectedAbility != null; + return selectedAbility != null; + } + + @Override + protected int getSelectedItemId() { + return -1; // Abilities use names, not IDs + } + + @Override + protected void sendMovePacket(int itemId, int destCatId) { + // Not used — we override executeMoveItems for name-based moves + } + + @Override + protected GuiScreen getWindowedVariant() { + return new GuiNpcManageAbilities(originalNpc, false); + } + + @Override + protected void saveCurrentItem() { + // Abilities are saved via SubGui callbacks, not on close + } + + // ========== Move Override (name-based) ========== + + @Override + protected void executeMoveItems(int destCatId) { + for (String name : moveSelection) { + PacketClient.sendClient(new AbilityCategoryMovePacket(getCategoryType(), name, destCatId)); + } + movePhase = 0; + moveSelection.clear(); + if (selectedCatId >= 0) { + requestItemsInCategory(selectedCatId); + } + initGui(); + } + + // ========== Scroll Events (name-based) ========== + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { + if (scroll.id == 0) { + if (isBuiltInMode()) { + // Built-in: name-based request + String selected = itemScroll.getSelected(); + if (selected != null && !selected.equals(prevItemName)) { + stopPreviewPlayback(); + currentIsBuiltIn = true; + PacketClient.sendClient(new BuiltInAbilityGetPacket(selected)); + prevItemName = selected; + } + return; + } + + if (movePhase == 1) { + // Move phase 1: selecting items + if (moveSelection.size() > MAX_MOVE_ITEMS) { + java.util.HashSet trimmed = new java.util.HashSet<>(); + int count = 0; + for (String s : moveSelection) { + if (count++ >= MAX_MOVE_ITEMS) break; + trimmed.add(s); + } + moveSelection.clear(); + moveSelection.addAll(trimmed); + itemScroll.setSelectedList(moveSelection); + } + if (getButton(55) != null) { + getButton(55).enabled = !moveSelection.isEmpty(); + } + return; + } + + // Normal click: name-based request + String selected = itemScroll.getSelected(); + if (selected != null && !selected.equals(prevItemName)) { + stopPreviewPlayback(); + prevItemName = selected; + currentIsBuiltIn = false; + if (isChainedMode()) { + PacketClient.sendClient(new ChainedAbilityGetPacket(selected)); + } else { + PacketClient.sendClient(new CustomAbilityGetPacket(selected)); + } + } + return; + } + + // Category scroll — delegate to parent + super.customScrollClicked(i, j, k, scroll); + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + if (scroll.id == 0 && hasSelectedItem() && movePhase == 0) { + onEditItem(); + } + } + + // ========== Search with User Type Filtering ========== + + @Override + protected List getItemSearchList() { + if (selectedCatId < 0 && !isBuiltInMode()) return new ArrayList<>(); + List list = new ArrayList<>(); + HashMap data = isBuiltInMode() ? builtInData : itemData; + for (Map.Entry entry : data.entrySet()) { + String name = entry.getKey(); + if (!itemSearch.isEmpty() && !name.toLowerCase().contains(itemSearch)) continue; + if (!isChainedMode() && userTypeFilter != 0) { + UserType ut = UserType.fromOrdinal(entry.getValue()); + if (userTypeFilter == 1 && !ut.allowsNpc()) continue; + if (userTypeFilter == 2 && !ut.allowsPlayer()) continue; + } + list.add(name); + } + return list; + } + + // ========== Data from Server ========== + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.BUILTIN_ABILITIES) { + builtInData = data; + if (isBuiltInMode()) { + itemScroll.setList(getItemSearchList()); + initGui(); + } + return; + } + if (type == EnumScrollData.CUSTOM_ABILITIES) { + // Received flat list — used only for built-in data alongside it + // Category mode uses CATEGORY_GROUP instead + return; + } + if (type == EnumScrollData.CHAINED_ABILITIES) { + // Flat list — not used in category mode + return; + } + super.setData(list, data, type); + } + + // ========== Actions ========== + + @Override + protected void actionPerformed(GuiButton guibutton) { + int id = guibutton.id; + + // View mode toggle + if (id == 60) { + viewMode = (viewMode + 1) % 3; + stopPreviewPlayback(); + clearSelection(); + if (isBuiltInMode()) { + selectedCatId = 0; + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + } else { + selectedCatId = -1; + requestCategoryList(); + } + initGui(); + return; + } + + // User type filter + if (id == 61) { + userTypeFilter = (userTypeFilter + 1) % 3; + itemScroll.setList(getItemSearchList()); + initGui(); + return; + } + + // Preview controls + if (id == 91) { // Play + if (previewExecutor.isPaused()) { + previewExecutor.play(); + } else { + startPreviewPlayback(); + } + initGui(); + return; + } else if (id == 92) { // Pause + previewExecutor.pause(); + initGui(); + return; + } else if (id == 93) { // Stop + stopPreviewPlayback(); + initGui(); + return; + } + + // Remove — need custom handling for name-based remove + if (id == 53 && prevItemName != null && !prevItemName.isEmpty() && movePhase == 0) { + int confirmId = isChainedMode() ? 3 : 2; + GuiYesNo guiyesno = new GuiYesNo(this, prevItemName, StatCollector.translateToLocal("gui.delete"), confirmId); + displayGuiScreen(guiyesno); + return; + } + + super.actionPerformed(guibutton); + } + + private void clearSelection() { + prevItemName = ""; + selectedAbility = null; + selectedChain = null; + currentIsBuiltIn = false; + itemSearch = ""; + movePhase = 0; + moveSelection.clear(); + trackingMovement = false; + } + + // ========== Confirm Dialog ========== + + @Override + public void confirmClicked(boolean result, int id) { + NoppesUtil.openGUI(player, this); + if (!result) return; + + // Remove custom ability + if (id == 2 && prevItemName != null && !prevItemName.isEmpty()) { + PacketClient.sendClient(new CustomAbilityRemovePacket(prevItemName)); + selectedAbility = null; + prevItemName = ""; + if (selectedCatId >= 0) requestItemsInCategory(selectedCatId); + initGui(); + return; + } + + // Remove chained ability + if (id == 3 && prevItemName != null && !prevItemName.isEmpty()) { + PacketClient.sendClient(new ChainedAbilityRemovePacket(prevItemName)); + selectedChain = null; + prevItemName = ""; + if (selectedCatId >= 0) requestItemsInCategory(selectedCatId); + initGui(); + return; + } + + // Category remove (id == 5) handled by parent + super.confirmClicked(result, id); + } + + // ========== SubGui Callbacks ========== + + @Override + protected void onSubGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiChainedAbilityConfig) { + if (pendingSaveChain != null) { + PacketClient.sendClient(new ChainedAbilitySavePacket(pendingSaveChain.writeNBT(false))); + prevItemName = pendingSaveChain.getName(); + pendingSaveChain = null; + if (selectedCatId >= 0 && isChainedMode()) requestItemsInCategory(selectedCatId); + } + initGui(); + return; + } + if (subgui instanceof SubGuiAbilityVariantSelect) { + SubGuiAbilityVariantSelect gui = (SubGuiAbilityVariantSelect) subgui; + int idx = gui.getSelectedIndex(); + if (idx >= 0 && pendingTypeId != null) { + Ability newAbility = AbilityController.Instance.create(pendingTypeId); + if (newAbility != null) { + gui.getVariants().get(idx).apply(newAbility); + newAbility.setId(UUID.randomUUID().toString()); + pendingTypeId = null; + selectedAbility = newAbility; + prevItemName = newAbility.getName(); + setSubGui(newAbility.createConfigGui(this)); + return; + } + } + pendingTypeId = null; + } else if (subgui instanceof SubGuiAbilityTypeSelect) { + String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId(); + if (typeId != null) { + List variants = AbilityController.Instance.getVariantsForType(typeId); + if (variants.size() > 1) { + pendingTypeId = typeId; + setSubGui(new SubGuiAbilityVariantSelect(variants)); + return; + } + Ability newAbility = AbilityController.Instance.create(typeId); + if (newAbility != null) { + if (variants.size() == 1) variants.get(0).apply(newAbility); + newAbility.setId(UUID.randomUUID().toString()); + selectedAbility = newAbility; + prevItemName = newAbility.getName(); + setSubGui(newAbility.createConfigGui(this)); + return; + } + } + } else if (subgui instanceof SubGuiDuplicateNameConfirm) { + SubGuiDuplicateNameConfirm gui = (SubGuiDuplicateNameConfirm) subgui; + if (pendingSaveAbility != null) { + if (gui.isConfirmed()) { + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT(false))); + prevItemName = pendingSaveAbility.getName(); + pendingSaveAbility = null; + if (selectedCatId >= 0 && isCustomMode()) requestItemsInCategory(selectedCatId); + } else if (gui.isBack()) { + Ability ability = pendingSaveAbility; + pendingSaveAbility = null; + setSubGui(ability.createConfigGui(this)); + return; + } else { + pendingSaveAbility = null; + } + } + } else if (pendingSaveAbility != null) { + if (hasDuplicateName(pendingSaveAbility)) { + setSubGui(new SubGuiDuplicateNameConfirm()); + return; + } + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT(false))); + prevItemName = pendingSaveAbility.getName(); + pendingSaveAbility = null; + if (selectedCatId >= 0 && isCustomMode()) requestItemsInCategory(selectedCatId); + } + initGui(); + } + + private boolean hasDuplicateName(Ability ability) { + String name = ability.getName(); + if (name == null || name.isEmpty()) return false; + if (!itemData.containsKey(name)) return false; + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) return true; + Ability existing = AbilityController.Instance.getCustomAbilityByName(name); + return existing == null || !uuid.equals(existing.getId()); + } + + @Override + public void onAbilitySaved(Ability ability) { + pendingSaveAbility = ability; + } + + @Override + public void onChainedAbilitySaved(ChainedAbility chain) { + pendingSaveChain = chain; + } + + // ========== Right Panel ========== + + @Override + protected void initRightPanel(int startY) { + // Calculate layout: playback row + edit + remove rows + int bottomRows = 2; // edit, remove + boolean hasPlayback = hasSelectedItem(); + if (hasPlayback) bottomRows++; // playback row + int bottomH = bottomRows * (btnH + gap) + 14; + + previewX = rightX; + previewY = contentY; + previewW = rightPanelW; + previewH = contentH - bottomH - gap; + + // Playback controls + if (hasPlayback) { + int playY = contentY + previewH + gap; + String animTexture = "customnpcs:textures/gui/animation.png"; + int btnX = rightX + 4; + + boolean isPlaying = previewExecutor.isPlaying() && !previewExecutor.isPaused(); + boolean isPaused = previewExecutor.isPaused(); + boolean isActive = previewExecutor.isActive(); + + if (!isPlaying || isPaused) { + String statusKey = isPaused ? "animation.paused" : "animation.stopped"; + addLabel(new GuiNpcLabel(90, statusKey, btnX, playY + 5)); + addButton(new GuiTexturedButton(91, "", btnX + 65, playY, 11, 20, animTexture, 18, 71)); + } else { + addLabel(new GuiNpcLabel(90, "animation.playing", btnX, playY + 5)); + addButton(new GuiTexturedButton(92, "", btnX + 65, playY, 14, 20, animTexture, 0, 71)); + } + if (isActive) { + addButton(new GuiTexturedButton(93, "", btnX + 85, playY, 14, 20, animTexture, 33, 71)); + } + } + + // Edit + Remove at bottom + int editY = contentY + contentH - btnH * 2 - gap; + GuiNpcButton editBtn = new GuiNpcButton(51, rightX, editY, rightPanelW, btnH, "gui.edit"); + editBtn.enabled = hasSelectedItem() && movePhase == 0 && !currentIsBuiltIn; + addButton(editBtn); + + int removeY = editY + btnH + gap; + GuiNpcButton removeBtn = new GuiNpcButton(53, rightX, removeY, rightPanelW, btnH, "gui.remove"); + removeBtn.enabled = hasSelectedItem() && movePhase == 0 && !currentIsBuiltIn; + removeBtn.setTextColor(0xFF5555); + addButton(removeBtn); + } + + // ========== Preview Rendering ========== + + @Override + protected void drawItemPreview(int centerX, int centerY, int mouseX, int mouseY, float partialTicks) { + if (selectedAbility == null && selectedChain == null) return; + + boolean previewing = previewExecutor.isActive(); + + // When preview is active, use fixed camera like GuiAbilityInterface + // Otherwise use the standard mouse-follow rotation + if (previewing) { + drawAbilityPreview(partialTicks); + } else { + drawStaticNpcPreview(centerX, centerY, mouseX, mouseY); + } + } + + /** + * Renders the full ability preview with fixed camera, preview entities, and telegraph. + * Matches GuiAbilityInterface's rendering pipeline. + */ + private void drawAbilityPreview(float partialTicks) { + // Set NPC facing direction + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + npc.rotationPitch = 0; + + // Calculate NPC position delta for movement rendering + double npcDeltaX = 0, npcDeltaY = 0, npcDeltaZ = 0; + if (trackingMovement) { + double interpX = npc.prevPosX + (npc.posX - npc.prevPosX) * partialTicks; + double interpY = npc.prevPosY + (npc.posY - npc.prevPosY) * partialTicks; + double interpZ = npc.prevPosZ + (npc.posZ - npc.prevPosZ) * partialTicks; + npcDeltaX = interpX - npcStartX; + npcDeltaY = interpY - npcStartY; + npcDeltaZ = interpZ - npcStartZ; + } + + // NPC render position: left-center of preview area + int npcScreenX = previewX + (int)(previewW * 0.33f); + int npcScreenY = previewY + (int)(previewH * 0.80f); + + // Scale camera zoom based on panel size + float scale = Math.min(previewW, previewH) / 200f; + float renderZoom = zoomed * scale; + + // Enable scissor to clip rendering to preview area + GL11.glEnable(GL11.GL_SCISSOR_TEST); + setScissorClip(previewX, previewY, previewW, previewH); + + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glPushMatrix(); + + GL11.glTranslatef(npcScreenX, npcScreenY, 500F); + GL11.glScalef(-renderZoom, renderZoom, renderZoom); + GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); + GL11.glRotatef(CAMERA_PITCH, 1.0F, 0.0F, 0.0F); + GL11.glRotatef(rotation, 0.0F, 1.0F, 0.0F); + + GL11.glRotatef(135F, 0.0F, 1.0F, 0.0F); + RenderHelper.enableStandardItemLighting(); + GL11.glRotatef(-135F, 0.0F, 1.0F, 0.0F); + + GL11.glTranslatef(0.0F, npc.yOffset, 0.0F); + RenderManager.instance.playerViewY = 180F; + ClientEventHandler.renderingEntityInGUI = true; + + // Render NPC at its position delta + try { + RenderManager.instance.renderEntityWithPosYaw(npc, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks); + } catch (Exception ignored) {} + + // Render preview entities + renderPreviewEntities(partialTicks); + + // Render telegraph + renderPreviewTelegraph(partialTicks); + + ClientEventHandler.renderingEntityInGUI = false; + GL11.glPopMatrix(); + + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + } + + /** + * Standard static NPC preview with mouse-follow rotation (when not previewing an ability). + */ + private void drawStaticNpcPreview(int centerX, int centerY, int mouseX, int mouseY) { + float scale = Math.min(previewW, previewH) / 200f; + float renderZoom = zoomed * scale; + + GL11.glColor4f(1, 1, 1, 1); + EntityLivingBase entity = this.npc; + + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glPushMatrix(); + GL11.glTranslatef(centerX, centerY, 60F); + GL11.glScalef(-renderZoom, renderZoom, renderZoom); + GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); + + float f2 = entity.renderYawOffset; + float f3 = entity.rotationYaw; + float f4 = entity.rotationPitch; + float f7 = entity.rotationYawHead; + float f5 = (float) centerX - mouseX; + float f6 = (float) (centerY - 50) - mouseY; + + GL11.glRotatef(135F, 0.0F, 1.0F, 0.0F); + RenderHelper.enableStandardItemLighting(); + GL11.glRotatef(-135F, 0.0F, 1.0F, 0.0F); + GL11.glRotatef(-(float) Math.atan(f6 / 800F) * 20F, 1.0F, 0.0F, 0.0F); + entity.prevRenderYawOffset = entity.renderYawOffset = rotation; + entity.prevRotationYaw = entity.rotationYaw = (float) Math.atan(f5 / 80F) * 40F + rotation; + entity.rotationPitch = -(float) Math.atan(f6 / 80F) * 20F; + entity.prevRotationYawHead = entity.rotationYawHead = entity.rotationYaw; + GL11.glTranslatef(0.0F, entity.yOffset, 1F); + RenderManager.instance.playerViewY = 180F; + + try { + RenderManager.instance.renderEntityWithPosYaw(entity, 0.0, 0.0, 0.0, 0.0F, 1.0F); + } catch (Exception ignored) {} + + entity.prevRenderYawOffset = entity.renderYawOffset = f2; + entity.prevRotationYaw = entity.rotationYaw = f3; + entity.rotationPitch = f4; + entity.prevRotationYawHead = entity.rotationYawHead = f7; + + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + GL11.glPopMatrix(); + } + + private void renderPreviewEntities(float partialTicks) { + List entities = previewExecutor.getPreviewEntities(); + if (entities.isEmpty()) return; + + boolean lightingWasEnabled = GL11.glIsEnabled(GL11.GL_LIGHTING); + double refX = trackingMovement ? npcStartX : npc.posX; + double refY = trackingMovement ? npcStartY : npc.posY; + double refZ = trackingMovement ? npcStartZ : npc.posZ; + + for (Entity entity : entities) { + if (entity == null || entity.isDead) continue; + double offsetX = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - refX; + double offsetY = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - refY - npc.yOffset; + double offsetZ = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - refZ; + + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glDisable(GL11.GL_LIGHTING); + try { + RenderManager.instance.renderEntityWithPosYaw(entity, offsetX, offsetY, offsetZ, entity.rotationYaw, partialTicks); + } catch (Exception ignored) {} + } + + if (lightingWasEnabled) GL11.glEnable(GL11.GL_LIGHTING); + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + } + + private void renderPreviewTelegraph(float partialTicks) { + TelegraphInstance telegraph = previewExecutor.getTelegraph(); + if (telegraph == null || TelegraphRenderer.Instance == null) return; + + double refX = trackingMovement ? npcStartX : npc.posX; + double refY = trackingMovement ? npcStartY : npc.posY; + double refZ = trackingMovement ? npcStartZ : npc.posZ; + + double offsetX = telegraph.getInterpolatedX(partialTicks) - refX; + double offsetY = telegraph.getInterpolatedY(partialTicks) - refY - npc.yOffset; + double offsetZ = telegraph.getInterpolatedZ(partialTicks) - refZ; + + TelegraphRenderer.Instance.renderTelegraphInGUI(telegraph, offsetX, offsetY, offsetZ, 1.0f, partialTicks); + } + + private void setScissorClip(int x, int y, int w, int h) { + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scale = sr.getScaleFactor(); + int scaledY = mc.displayHeight - (y + h) * scale; + GL11.glScissor(x * scale, scaledY, w * scale, h * scale); + } + + // ========== Preview Tick ========== + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // Set NPC rotation BEFORE ticking for correct anchor points + if (previewExecutor.isActive()) { + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + } + + tickPreview(); + super.drawScreen(mouseX, mouseY, partialTicks); + } + + private void tickPreview() { + if (previewExecutor.isPlaying() && !previewExecutor.isPaused()) { + long time = mc.theWorld != null ? mc.theWorld.getTotalWorldTime() : System.currentTimeMillis() / 50; + if (time != prevTick) { + npc.display.animationData.increaseTime(); + previewExecutor.tick(); + prevTick = time; + + if (!previewExecutor.isActive()) { + trackingMovement = false; + initGui(); + } + } + } + } + + private void startPreviewPlayback() { + // Save NPC start position for movement tracking + npcStartX = npc.posX; + npcStartY = npc.posY; + npcStartZ = npc.posZ; + trackingMovement = true; + + if (isChainedMode() && selectedChain != null) { + previewExecutor.startChainPreview(selectedChain, npc); + } else if (selectedAbility != null) { + previewExecutor.startPreview(selectedAbility, npc); + } + } + + private void stopPreviewPlayback() { + previewExecutor.stop(); + trackingMovement = false; + // Reset NPC animation + AnimationData data = npc.display.animationData; + data.setAnimation(new Animation()); + } + + // ========== Preview Status Drawing ========== + + @Override + protected void drawOverlay(int mouseX, int mouseY, float partialTicks) { + super.drawOverlay(mouseX, mouseY, partialTicks); + + // Draw preview status text inside the preview area + if (previewExecutor.isActive() && !hasSubGui()) { + String status = previewExecutor.getStatusString(); + fontRendererObj.drawString(status, previewX + 4, previewY + previewH - 12, 0xFFFFFF, true); + } + } + + @Override + protected void drawItemDetails(int x, int y, int w) { + if (isChainedMode() && selectedChain != null) { + fontRendererObj.drawString(selectedChain.getDisplayName(), x, y, 0xFFFFFF, true); + y += 14; + fontRendererObj.drawString("Entries: " + selectedChain.getEntries().size(), x, y, 0xB5B5B5, false); + y += 12; + fontRendererObj.drawString("Cooldown: " + selectedChain.getCooldownTicks() + "t", x, y, 0xB5B5B5, false); + y += 12; + UserType ut = selectedChain.getAllowedBy(); + fontRendererObj.drawString("Allowed: " + ut.name(), x, y, 0xB5B5B5, false); + } else if (selectedAbility != null) { + fontRendererObj.drawString(selectedAbility.getDisplayName(), x, y, 0xFFFFFF, true); + y += 14; + String typeId = selectedAbility.getTypeId(); + String typeName = StatCollector.translateToLocal(typeId); + fontRendererObj.drawString("Type: " + typeName, x, y, 0xFFAE0D, false); + y += 12; + fontRendererObj.drawString("Cooldown: " + selectedAbility.getCooldownTicks() + "t", x, y, 0xB5B5B5, false); + y += 12; + fontRendererObj.drawString("Windup: " + selectedAbility.getWindUpTicks() + "t", x, y, 0xB5B5B5, false); + y += 12; + UserType ut = selectedAbility.getAllowedBy(); + fontRendererObj.drawString("Allowed: " + ut.name(), x, y, 0xB5B5B5, false); + if (selectedAbility.hasDamage()) { + y += 12; + fontRendererObj.drawString("Damage: " + selectedAbility.getDisplayDamage(), x, y, 0xFF5555, false); + } + if (currentIsBuiltIn) { + y += 12; + fontRendererObj.drawString("(Built-in)", x, y, 0x55FF55, false); + } + } + } + + // ========== Cleanup ========== + + @Override + public void onGuiClosed() { + stopPreviewPlayback(); + if (npc != null) { + npc.display.animationData.setEnabled(false); + } + super.onGuiClosed(); + } + + @Override + public void save() {} +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiAnimationDirectory.java b/src/main/java/noppes/npcs/client/gui/global/GuiAnimationDirectory.java new file mode 100644 index 000000000..5cc629a4a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiAnimationDirectory.java @@ -0,0 +1,523 @@ +package noppes.npcs.client.gui.global; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; +import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; +import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; +import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; +import kamkeel.npcs.network.packets.request.category.CategoryItemsRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryListRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryMoveItemPacket; +import kamkeel.npcs.network.packets.request.category.CategoryRemovePacket; +import kamkeel.npcs.network.packets.request.category.CategorySavePacket; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiDirectoryCategorized; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiTexturedButton; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Category; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.HashMap; +import java.util.Vector; + +public class GuiAnimationDirectory extends GuiDirectoryCategorized { + public Animation animation = new Animation(); + public EntityNPCInterface npc; + private EntityNPCInterface originalNpc; + + // Animation playback + public boolean playingAnimation = false; + private long prevTick; + + // Built-in mode + private boolean showingBuiltIn = false; + private boolean currentIsBuiltIn = false; + private HashMap builtInData = new HashMap<>(); + + public GuiAnimationDirectory(EntityNPCInterface npc) { + super(); + this.originalNpc = npc; + this.npc = createFakeNPC(npc); + zoomed = 70; + PacketClient.sendClient(new AnimationsGetPacket()); + } + + private static EntityNPCInterface createFakeNPC(EntityNPCInterface original) { + EntityCustomNpc fake = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); + fake.display.readToNBT(original.display.writeToNBT(new NBTTagCompound())); + fake.display.name = "anim preview"; + fake.height = original.height; + fake.width = original.width; + fake.display.animationData.setEnabled(true); + return fake; + } + + @Override + protected boolean hasCategories() { return !showingBuiltIn; } + + @Override + protected void computeLayout() { + if (showingBuiltIn) { + leftPanelPercent = 0.0f; + minLeftPanelW = 0; + } else { + leftPanelPercent = 0.15f; + minLeftPanelW = 120; + } + super.computeLayout(); + } + + @Override + protected void drawPanels() { + if (showingBuiltIn) { + if (rightPanelW > 0) { + GuiUtil.drawRectD(rightX - 1, contentY - 1, rightX + rightPanelW + 1, originY + usableH + 1, panelBorder); + } + } else { + super.drawPanels(); + } + } + + @Override + protected void initLeftPanel() { + if (showingBuiltIn) return; + super.initLeftPanel(); + } + + @Override + protected String getTitle() { + return showingBuiltIn ? "Animations (Built-in)" : "Animations"; + } + + @Override + protected void requestCategoryList() { + if (!showingBuiltIn) { + PacketClient.sendClient(new CategoryListRequestPacket(EnumCategoryType.ANIMATION)); + } + } + + @Override + protected void requestItemsInCategory(int catId) { + if (!showingBuiltIn) { + PacketClient.sendClient(new CategoryItemsRequestPacket(EnumCategoryType.ANIMATION, catId)); + } + } + + @Override + protected void requestItemData(int itemId) { + PacketClient.sendClient(new AnimationGetPacket(itemId)); + } + + @Override + protected void onSaveCategory(Category cat) { + PacketClient.sendClient(new CategorySavePacket(EnumCategoryType.ANIMATION, cat.writeNBT(new NBTTagCompound()))); + } + + @Override + protected void onRemoveCategory(int catId) { + PacketClient.sendClient(new CategoryRemovePacket(EnumCategoryType.ANIMATION, catId)); + } + + @Override + protected void onAddItem(int catId) { + if (showingBuiltIn) return; + String name = "New"; + while (itemData.containsKey(name)) name += "_"; + Animation newAnim = new Animation(-1, name); + PacketClient.sendClient(new AnimationSavePacket(newAnim.writeToNBT())); + } + + @Override + protected void onRemoveItem(int itemId) { + if (showingBuiltIn) return; + PacketClient.sendClient(new AnimationRemovePacket(itemId)); + animation = new Animation(); + } + + @Override + protected void onEditItem() { + if (animation != null && animation.id >= 0 && !currentIsBuiltIn) { + NoppesUtil.openGUI(player, new GuiNPCEditAnimation(this, animation, npc)); + } + } + + @Override + protected void onCloneItem() { + if (showingBuiltIn || currentIsBuiltIn) return; + if (animation != null && animation.id >= 0) { + Animation clone = new Animation(); + clone.readFromNBT(animation.writeToNBT()); + clone.id = -1; + while (itemData.containsKey(clone.name)) clone.name += "_"; + PacketClient.sendClient(new AnimationSavePacket(clone.writeToNBT())); + } + } + + @Override + protected void onItemReceived(NBTTagCompound compound) { + animation = new Animation(); + animation.readFromNBT(compound); + setPrevItemName(animation.name); + + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + + playingAnimation = false; + showFirstFrame(); + } + + private void showFirstFrame() { + if (animation.id != -1 || currentIsBuiltIn) { + AnimationData data = npc.display.animationData; + data.setAnimation(new Animation()); + data.animation.smooth = animation.smooth; + data.animation.loop = 0; + if (!animation.frames.isEmpty()) { + Frame firstFrame = new Frame(); + firstFrame.parent = data.animation; + firstFrame.readFromNBT(animation.frames.get(0).writeToNBT()); + data.animation.addFrame(firstFrame); + } + } + } + + @Override + protected boolean hasSelectedItem() { + if (currentIsBuiltIn) return true; + return animation != null && animation.id >= 0; + } + + @Override + protected int getSelectedItemId() { + return animation != null ? animation.id : -1; + } + + @Override + protected void sendMovePacket(int itemId, int destCatId) { + PacketClient.sendClient(new CategoryMoveItemPacket(EnumCategoryType.ANIMATION, itemId, destCatId)); + } + + @Override + protected GuiScreen getWindowedVariant() { + return new GuiNPCManageAnimations(originalNpc, false, false); + } + + @Override + protected void saveCurrentItem() { + if (!currentIsBuiltIn && animation != null && animation.id >= 0 && prevItemName != null && !prevItemName.isEmpty()) { + PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); + prevItemName = animation.name; + } + } + + @Override + protected void onSubGuiClosed(SubGuiInterface subgui) { + if (animation != null && animation.id >= 0) { + setPrevItemName(animation.name); + if (selectedCatId >= 0 && !showingBuiltIn) requestItemsInCategory(selectedCatId); + } + } + + // ========== Built-in Toggle ========== + + @Override + protected int initExtraTopBarButtons(int x, int topBtnY) { + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + GuiNpcButton toggleBtn = new GuiNpcButton(60, x, topBtnY, 55, btnH, toggleLabel); + toggleBtn.setTextColor(showingBuiltIn ? 0x55FFFF : 0xFFFFFF); + addButton(toggleBtn); + return x + 55 + 2; + } + + @Override + protected void initTopBar(int topBtnY) { + super.initTopBar(topBtnY); + + // Disable item operations in built-in mode + if (showingBuiltIn) { + if (getButton(50) != null) getButton(50).enabled = false; // Add + if (getButton(54) != null) getButton(54).enabled = false; // Move + } + } + + @Override + protected void initRightPanel(int startY) { + // Calculate layout: playback row + edit/copy + remove rows + int bottomRows = 2; // edit+copy, remove + boolean hasPlayback = animation != null && !animation.frames.isEmpty() && hasSelectedItem(); + if (hasPlayback) bottomRows++; // playback row + int bottomH = bottomRows * (btnH + gap) + 14; + + previewX = rightX; + previewY = contentY; + previewW = rightPanelW; + previewH = contentH - bottomH - gap; + + // Playback controls + if (hasPlayback) { + int playY = contentY + previewH + gap; + String animTexture = "customnpcs:textures/gui/animation.png"; + AnimationData data = npc.display.animationData; + int btnX = rightX + 4; + + if (!playingAnimation || (data.animation != null && data.animation.paused)) { + String statusKey = (data.animation != null && data.animation.paused) ? "animation.paused" : "animation.stopped"; + addLabel(new GuiNpcLabel(90, statusKey, btnX, playY + 5)); + addButton(new GuiTexturedButton(91, "", btnX + 65, playY, 11, 20, animTexture, 18, 71)); + } else { + addLabel(new GuiNpcLabel(90, "animation.playing", btnX, playY + 5)); + addLabel(new GuiNpcLabel(94, "", btnX + 60, playY + 5)); + addButton(new GuiTexturedButton(92, "", btnX + 65, playY, 14, 20, animTexture, 0, 71)); + } + if (playingAnimation) { + addButton(new GuiTexturedButton(93, "", btnX + 85, playY, 14, 20, animTexture, 33, 71)); + } + } + + // Edit + Copy on one row, Remove below + int btnY = contentY + contentH - btnH * 2 - gap; + int halfW = (rightPanelW - gap) / 2; + + GuiNpcButton editBtn = new GuiNpcButton(51, rightX, btnY, halfW, btnH, "gui.edit"); + editBtn.enabled = hasSelectedItem() && movePhase == 0 && !currentIsBuiltIn; + addButton(editBtn); + + GuiNpcButton cloneBtn = new GuiNpcButton(52, rightX + halfW + gap, btnY, halfW, btnH, "gui.copy"); + cloneBtn.enabled = hasSelectedItem() && movePhase == 0 && !currentIsBuiltIn; + addButton(cloneBtn); + + int removeY = btnY + btnH + gap; + GuiNpcButton removeBtn = new GuiNpcButton(53, rightX, removeY, rightPanelW, btnH, "gui.remove"); + removeBtn.enabled = hasSelectedItem() && movePhase == 0 && !currentIsBuiltIn; + removeBtn.setTextColor(0xFF5555); + addButton(removeBtn); + } + + // ========== Scroll Events Override for Built-in ========== + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { + if (scroll.id == 0 && showingBuiltIn) { + String selected = itemScroll.getSelected(); + if (selected != null && !selected.equals(prevItemName)) { + PacketClient.sendClient(new BuiltInAnimationGetPacket(selected)); + prevItemName = selected; + } + return; + } + super.customScrollClicked(i, j, k, scroll); + } + + // ========== Data ========== + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; + if (showingBuiltIn) { + this.itemData = data; + itemScroll.setList(getItemSearchList()); + initGui(); + } + return; + } + super.setData(list, data, type); + } + + // ========== Animation Tick ========== + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + AnimationData data = npc.display.animationData; + if (!data.isActive() && playingAnimation) { + playingAnimation = false; + initGui(); + } else if (data.isActive()) { + long time = mc.theWorld.getTotalWorldTime(); + if (time != prevTick) { + npc.display.animationData.increaseTime(); + GuiNpcLabel label = getLabel(94); + if (label != null && data.animation != null) { + int frameIdx = data.animation.currentFrame; + int frameTime = data.animation.currentFrameTime; + int totalFrames = data.animation.frames.size(); + Frame curFrame = (Frame) data.animation.currentFrame(); + int duration = curFrame != null ? curFrame.getDuration() : 0; + label.label = (frameIdx + 1) + "/" + totalFrames + " (" + frameTime + "/" + duration + "t)"; + } + } + prevTick = time; + } + super.drawScreen(mouseX, mouseY, partialTicks); + } + + // ========== Actions ========== + + @Override + protected void actionPerformed(GuiButton guibutton) { + int id = guibutton.id; + + // Toggle built-in / custom + if (id == 60) { + showingBuiltIn = !showingBuiltIn; + animation = new Animation(); + currentIsBuiltIn = false; + playingAnimation = false; + prevItemName = ""; + itemSearch = ""; + + if (showingBuiltIn) { + // Show built-in flat list + this.itemData = builtInData; + selectedCatId = 0; // Ensure list is visible + } else { + // Re-enter category mode + this.itemData = new HashMap<>(); + selectedCatId = -1; + requestCategoryList(); + } + initGui(); + return; + } + + // Playback controls + AnimationData data = npc.display.animationData; + if (id == 91) { + if (!playingAnimation || !data.isActive()) { + animation.currentFrame = 0; + animation.currentFrameTime = 0; + for (Frame frame : animation.frames) { + for (FramePart framePart : frame.frameParts.values()) { + framePart.prevRotations = new float[]{0, 0, 0}; + framePart.prevPivots = new float[]{0, 0, 0}; + } + } + } + playingAnimation = true; + data.setAnimation(this.animation); + data.animation.paused = false; + initGui(); + return; + } else if (id == 92) { + data.animation.paused = true; + initGui(); + return; + } else if (id == 93) { + playingAnimation = false; + data.animation.paused = false; + showFirstFrame(); + initGui(); + return; + } + + super.actionPerformed(guibutton); + } + + // ========== Preview Rendering ========== + + @Override + protected void drawItemPreview(int centerX, int centerY, int mouseX, int mouseY, float partialTicks) { + if (animation == null) return; + if (!currentIsBuiltIn && animation.id == -1) return; + + // Scale NPC with panel size + float scale = Math.min(previewW, previewH) / 200f; + float renderZoom = zoomed * scale; + + GL11.glColor4f(1, 1, 1, 1); + EntityLivingBase entity = this.npc; + + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glPushMatrix(); + GL11.glTranslatef(centerX, centerY, 60F); + GL11.glScalef(-renderZoom, renderZoom, renderZoom); + GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); + + float f2 = entity.renderYawOffset; + float f3 = entity.rotationYaw; + float f4 = entity.rotationPitch; + float f7 = entity.rotationYawHead; + float f5 = (float) centerX - mouseX; + float f6 = (float) (centerY - 50) - mouseY; + + GL11.glRotatef(135F, 0.0F, 1.0F, 0.0F); + RenderHelper.enableStandardItemLighting(); + GL11.glRotatef(-135F, 0.0F, 1.0F, 0.0F); + GL11.glRotatef(-(float) Math.atan(f6 / 800F) * 20F, 1.0F, 0.0F, 0.0F); + entity.prevRenderYawOffset = entity.renderYawOffset = rotation; + entity.prevRotationYaw = entity.rotationYaw = (float) Math.atan(f5 / 80F) * 40F + rotation; + entity.rotationPitch = -(float) Math.atan(f6 / 80F) * 20F; + entity.prevRotationYawHead = entity.rotationYawHead = entity.rotationYaw; + GL11.glTranslatef(0.0F, entity.yOffset, 1F); + RenderManager.instance.playerViewY = 180F; + + try { + RenderManager.instance.renderEntityWithPosYaw(entity, 0.0, 0.0, 0.0, 0.0F, 1.0F); + } catch (Exception ignored) { + } + + entity.prevRenderYawOffset = entity.renderYawOffset = f2; + entity.prevRotationYaw = entity.rotationYaw = f3; + entity.rotationPitch = f4; + entity.prevRotationYawHead = entity.rotationYawHead = f7; + + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + GL11.glPopMatrix(); + } + + @Override + protected void drawItemDetails(int x, int y, int w) { + if (animation == null) return; + if (!currentIsBuiltIn && animation.id == -1) return; + + fontRendererObj.drawString(animation.name, x, y, 0xFFFFFF, true); + y += 14; + fontRendererObj.drawString("Frames: " + animation.frames.size(), x, y, 0xB5B5B5, false); + y += 12; + fontRendererObj.drawString("Speed: " + animation.speed, x, y, 0xB5B5B5, false); + y += 12; + String loopStr = animation.loop == 0 ? "None" : animation.loop == 1 ? "Loop" : "Mirror"; + fontRendererObj.drawString("Loop: " + loopStr, x, y, 0xB5B5B5, false); + if (currentIsBuiltIn) { + y += 12; + fontRendererObj.drawString("(Built-in)", x, y, 0x55FF55, false); + } + } + + // ========== Cleanup ========== + + @Override + public void onGuiClosed() { + if (npc != null) { + npc.display.animationData.setEnabled(false); + } + super.onGuiClosed(); + } + +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiEffectDirectory.java b/src/main/java/noppes/npcs/client/gui/global/GuiEffectDirectory.java new file mode 100644 index 000000000..ef1ad314d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiEffectDirectory.java @@ -0,0 +1,193 @@ +package noppes.npcs.client.gui.global; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.category.CategoryItemsRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryListRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryMoveItemPacket; +import kamkeel.npcs.network.packets.request.category.CategoryRemovePacket; +import kamkeel.npcs.network.packets.request.category.CategorySavePacket; +import kamkeel.npcs.network.packets.request.effects.EffectGetPacket; +import kamkeel.npcs.network.packets.request.effects.EffectRemovePacket; +import kamkeel.npcs.network.packets.request.effects.EffectSavePacket; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.CustomNpcResourceListener; +import noppes.npcs.client.gui.SubGuiEffectGeneral; +import noppes.npcs.client.gui.util.GuiDirectoryCategorized; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.client.renderer.ImageData; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.controllers.CustomEffectController; +import noppes.npcs.controllers.data.Category; +import noppes.npcs.controllers.data.CustomEffect; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import static noppes.npcs.client.gui.player.inventory.GuiCNPCInventory.specialIcons; + +public class GuiEffectDirectory extends GuiDirectoryCategorized { + public CustomEffect effect = new CustomEffect(); + public EntityNPCInterface npc; + + public GuiEffectDirectory(EntityNPCInterface npc) { + super(); + this.npc = npc; + } + + @Override + protected String getTitle() { return "Effects"; } + + @Override + protected void requestCategoryList() { + PacketClient.sendClient(new CategoryListRequestPacket(EnumCategoryType.EFFECT)); + } + + @Override + protected void requestItemsInCategory(int catId) { + PacketClient.sendClient(new CategoryItemsRequestPacket(EnumCategoryType.EFFECT, catId)); + } + + @Override + protected void requestItemData(int itemId) { + PacketClient.sendClient(new EffectGetPacket(itemId)); + } + + @Override + protected void onSaveCategory(Category cat) { + PacketClient.sendClient(new CategorySavePacket(EnumCategoryType.EFFECT, cat.writeNBT(new NBTTagCompound()))); + } + + @Override + protected void onRemoveCategory(int catId) { + PacketClient.sendClient(new CategoryRemovePacket(EnumCategoryType.EFFECT, catId)); + } + + @Override + protected void onAddItem(int catId) { + String name = "New"; + while (itemData.containsKey(name)) name += "_"; + CustomEffect newEffect = new CustomEffect(-1, name); + PacketClient.sendClient(new EffectSavePacket(newEffect.writeToNBT(false), "")); + } + + @Override + protected void onRemoveItem(int itemId) { + PacketClient.sendClient(new EffectRemovePacket(itemId)); + effect = new CustomEffect(); + } + + @Override + protected void onEditItem() { + if (effect != null && effect.id >= 0) { + setSubGui(new SubGuiEffectGeneral(this, effect)); + } + } + + @Override + protected void onCloneItem() { + if (effect != null && effect.id >= 0) { + CustomEffect clone = effect.cloneEffect(); + while (itemData.containsKey(clone.name)) clone.name += "_"; + PacketClient.sendClient(new EffectSavePacket(clone.writeToNBT(false), "")); + } + } + + @Override + protected void onItemReceived(NBTTagCompound compound) { + effect = new CustomEffect(); + effect.readFromNBT(compound); + setPrevItemName(effect.name); + if (effect.id != -1) { + CustomEffectController.getInstance().getCustomEffects().replace(effect.id, effect); + } + } + + @Override + protected boolean hasSelectedItem() { + return effect != null && effect.id >= 0; + } + + @Override + protected int getSelectedItemId() { + return effect != null ? effect.id : -1; + } + + @Override + protected void sendMovePacket(int itemId, int destCatId) { + PacketClient.sendClient(new CategoryMoveItemPacket(EnumCategoryType.EFFECT, itemId, destCatId)); + } + + @Override + protected GuiScreen getWindowedVariant() { + return new GuiNPCManageEffects(npc); + } + + @Override + protected void saveCurrentItem() { + if (effect != null && effect.id >= 0 && prevItemName != null && !prevItemName.isEmpty()) { + PacketClient.sendClient(new EffectSavePacket(effect.writeToNBT(false), prevItemName)); + prevItemName = effect.name; + } + } + + @Override + protected void onSubGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiEffectGeneral) { + if (effect != null && effect.id >= 0) { + setPrevItemName(effect.name); + if (selectedCatId >= 0) requestItemsInCategory(selectedCatId); + } + } + } + + @Override + protected void drawItemPreview(int centerX, int centerY, int mouseX, int mouseY, float partialTicks) { + if (effect == null || effect.id == -1) return; + + int iconRenderSize = Math.min(previewW, previewH) / 3; + iconRenderSize = Math.max(32, Math.min(iconRenderSize, 128)); + int x = centerX - iconRenderSize / 2; + int y = centerY - iconRenderSize; + + TextureManager textureManager = mc.getTextureManager(); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + ImageData data = ClientCacheHandler.getImageData(effect.icon); + if (data.imageLoaded()) { + data.bindTexture(); + int iconX = effect.iconX; + int iconY = effect.iconY; + int iconWidth = effect.getWidth(); + int iconHeight = effect.getHeight(); + int width = data.getTotalWidth(); + int height = data.getTotalHeight(); + func_152125_a(x, y, iconX, iconY, iconWidth, iconHeight, iconRenderSize, iconRenderSize, width, height); + } else { + textureManager.bindTexture(new ResourceLocation("customnpcs", "textures/marks/question.png")); + func_152125_a(x, y, 0, 0, 1, 1, iconRenderSize, iconRenderSize, 1, 1); + } + GL11.glDisable(GL11.GL_DEPTH_TEST); + textureManager.bindTexture(specialIcons); + func_152125_a(x, y, 0, 224, 16, 16, iconRenderSize, iconRenderSize, 256, 256); + GL11.glEnable(GL11.GL_DEPTH_TEST); + } + + @Override + protected void drawItemDetails(int x, int y, int w) { + if (effect == null || effect.id == -1) return; + + String drawString = effect.getMenuName(); + fontRendererObj.drawString(drawString, x, y, CustomNpcResourceListener.DefaultTextColor, true); + + y += 14; + fontRendererObj.drawString(StatCollector.translateToLocal("gui.name") + ": " + effect.name, x, y, 0xFFFFFF, false); + y += 12; + fontRendererObj.drawString(StatCollector.translateToLocal("effect.runsEveryX") + ": " + effect.everyXTick + "t", x, y, 0xB5B5B5, false); + y += 12; + fontRendererObj.drawString(StatCollector.translateToLocal("effect.defaultLength") + ": " + effect.length + "s", x, y, 0xB5B5B5, false); + } + +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiLinkedItemDirectory.java b/src/main/java/noppes/npcs/client/gui/global/GuiLinkedItemDirectory.java new file mode 100644 index 000000000..86199b818 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiLinkedItemDirectory.java @@ -0,0 +1,285 @@ +package noppes.npcs.client.gui.global; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.category.CategoryItemsRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryListRequestPacket; +import kamkeel.npcs.network.packets.request.category.CategoryMoveItemPacket; +import kamkeel.npcs.network.packets.request.category.CategoryRemovePacket; +import kamkeel.npcs.network.packets.request.category.CategorySavePacket; +import kamkeel.npcs.network.packets.request.linked.LinkedGetPacket; +import kamkeel.npcs.network.packets.request.linked.LinkedItemBuildPacket; +import kamkeel.npcs.network.packets.request.linked.LinkedItemRemovePacket; +import kamkeel.npcs.network.packets.request.linked.LinkedItemSavePacket; +import kamkeel.npcs.util.ColorUtil; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.CustomNpcResourceListener; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.item.SubGuiLinkedItem; +import noppes.npcs.client.gui.util.GuiDirectoryCategorized; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.client.renderer.ImageData; +import noppes.npcs.constants.EnumCategoryType; +import noppes.npcs.controllers.data.Category; +import noppes.npcs.controllers.data.LinkedItem; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +public class GuiLinkedItemDirectory extends GuiDirectoryCategorized { + public LinkedItem linkedItem = null; + public EntityNPCInterface npc; + + public GuiLinkedItemDirectory(EntityNPCInterface npc) { + super(); + this.npc = npc; + } + + @Override + protected String getTitle() { return "Linked Items"; } + + @Override + protected void requestCategoryList() { + PacketClient.sendClient(new CategoryListRequestPacket(EnumCategoryType.LINKED_ITEM)); + } + + @Override + protected void requestItemsInCategory(int catId) { + PacketClient.sendClient(new CategoryItemsRequestPacket(EnumCategoryType.LINKED_ITEM, catId)); + } + + @Override + protected void requestItemData(int itemId) { + LinkedGetPacket.GetItem(itemId); + } + + @Override + protected void onSaveCategory(Category cat) { + PacketClient.sendClient(new CategorySavePacket(EnumCategoryType.LINKED_ITEM, cat.writeNBT(new NBTTagCompound()))); + } + + @Override + protected void onRemoveCategory(int catId) { + PacketClient.sendClient(new CategoryRemovePacket(EnumCategoryType.LINKED_ITEM, catId)); + } + + @Override + protected void onAddItem(int catId) { + String name = "New"; + while (itemData.containsKey(name)) name += "_"; + LinkedItem newItem = new LinkedItem(name); + PacketClient.sendClient(new LinkedItemSavePacket(newItem.writeToNBT(false), "")); + } + + @Override + protected void onRemoveItem(int itemId) { + PacketClient.sendClient(new LinkedItemRemovePacket(itemId)); + linkedItem = null; + } + + @Override + protected void onEditItem() { + if (linkedItem != null && linkedItem.id >= 0) { + setSubGui(new SubGuiLinkedItem(this, linkedItem)); + } + } + + @Override + protected void onCloneItem() { + if (linkedItem != null && linkedItem.id >= 0) { + LinkedItem clone = linkedItem.clone(); + while (itemData.containsKey(clone.name)) clone.name += "_"; + clone.id = -1; + PacketClient.sendClient(new LinkedItemSavePacket(clone.writeToNBT(false), "")); + } + } + + @Override + protected void onItemReceived(NBTTagCompound compound) { + linkedItem = new LinkedItem(); + linkedItem.readFromNBT(compound); + setPrevItemName(linkedItem.name); + } + + @Override + protected boolean hasSelectedItem() { + return linkedItem != null && linkedItem.id >= 0; + } + + @Override + protected int getSelectedItemId() { + return linkedItem != null ? linkedItem.id : -1; + } + + @Override + protected void sendMovePacket(int itemId, int destCatId) { + PacketClient.sendClient(new CategoryMoveItemPacket(EnumCategoryType.LINKED_ITEM, itemId, destCatId)); + } + + @Override + protected GuiScreen getWindowedVariant() { + return new GuiNPCManageLinked(npc); + } + + @Override + protected void saveCurrentItem() { + if (linkedItem != null && linkedItem.id >= 0 && prevItemName != null && !prevItemName.isEmpty()) { + PacketClient.sendClient(new LinkedItemSavePacket(linkedItem.writeToNBT(false), prevItemName)); + prevItemName = linkedItem.name; + } + } + + @Override + protected void onSubGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiLinkedItem) { + if (linkedItem != null && linkedItem.id >= 0) { + setPrevItemName(linkedItem.name); + if (selectedCatId >= 0) requestItemsInCategory(selectedCatId); + } + } + } + + @Override + protected void initRightPanel(int startY) { + // Reserve space for: edit+build row, copy+remove row, ID label + int bottomH = (btnH + gap) * 2 + 14; + previewX = rightX; + previewY = contentY; + previewW = rightPanelW; + previewH = contentH - bottomH - gap; + + int halfW = (rightPanelW - gap) / 2; + + // Edit + Build on first row + int row1Y = contentY + contentH - btnH * 2 - gap; + GuiNpcButton editBtn = new GuiNpcButton(51, rightX, row1Y, halfW, btnH, "gui.edit"); + editBtn.enabled = hasSelectedItem() && movePhase == 0; + addButton(editBtn); + + GuiNpcButton buildBtn = new GuiNpcButton(56, rightX + halfW + gap, row1Y, halfW, btnH, "gui.build"); + buildBtn.enabled = hasSelectedItem() && movePhase == 0; + addButton(buildBtn); + + // Copy + Remove on second row + int row2Y = row1Y + btnH + gap; + GuiNpcButton cloneBtn = new GuiNpcButton(52, rightX, row2Y, halfW, btnH, "gui.copy"); + cloneBtn.enabled = hasSelectedItem() && movePhase == 0; + addButton(cloneBtn); + + GuiNpcButton removeBtn = new GuiNpcButton(53, rightX + halfW + gap, row2Y, halfW, btnH, "gui.remove"); + removeBtn.enabled = hasSelectedItem() && movePhase == 0; + removeBtn.setTextColor(0xFF5555); + addButton(removeBtn); + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == 56 && linkedItem != null && linkedItem.id >= 0) { + PacketClient.sendClient(new LinkedItemBuildPacket(linkedItem.getId())); + return; + } + super.actionPerformed(guibutton); + } + + @Override + protected void drawItemPreview(int centerX, int centerY, int mouseX, int mouseY, float partialTicks) { + if (linkedItem == null || linkedItem.id < 0) return; + + int iconRenderSize = Math.min(previewW, previewH) / 3; + iconRenderSize = Math.max(32, Math.min(iconRenderSize, 128)); + int x = centerX - iconRenderSize / 2; + int y = centerY - iconRenderSize; + + TextureManager textureManager = mc.getTextureManager(); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + ImageData imageData = ClientCacheHandler.getImageData(linkedItem.display.texture); + if (imageData.imageLoaded()) { + float[] colors = ColorUtil.hexToRGB(linkedItem.display.itemColor); + GL11.glColor3f(colors[0], colors[1], colors[2]); + imageData.bindTexture(); + int iconWidth = imageData.getTotalWidth(); + int iconHeight = imageData.getTotalHeight(); + func_152125_a(x, y, 0, 0, iconWidth, iconHeight, iconRenderSize, iconRenderSize, iconWidth, iconHeight); + } else { + textureManager.bindTexture(new ResourceLocation("customnpcs", "textures/marks/question.png")); + func_152125_a(x, y, 0, 0, 1, 1, iconRenderSize, iconRenderSize, 1, 1); + } + GL11.glColor3f(1.0f, 1.0f, 1.0f); + } + + @Override + protected void drawItemDetails(int x, int y, int w) { + if (linkedItem == null || linkedItem.id < 0) return; + + fontRendererObj.drawString(linkedItem.name, x, y, CustomNpcResourceListener.DefaultTextColor, true); + y += 14; + + int labelColor = 0xffae0d; + fontRendererObj.drawString(StatCollector.translateToLocal("display.version") + ": " + linkedItem.version, x, y, labelColor, false); + y += 12; + + labelColor = 0xff5714; + fontRendererObj.drawString(StatCollector.translateToLocal("display.maxStack") + ": " + linkedItem.stackSize, x, y, labelColor, false); + y += 12; + + labelColor = 0xf7ca28; + fontRendererObj.drawString(StatCollector.translateToLocal("display.digSpeed") + ": " + linkedItem.digSpeed, x, y, labelColor, false); + y += 12; + + labelColor = 0x29d6b9; + String[] useActions = { + StatCollector.translateToLocal("use_action.none"), + StatCollector.translateToLocal("use_action.block"), + StatCollector.translateToLocal("use_action.eat"), + StatCollector.translateToLocal("use_action.drink"), + StatCollector.translateToLocal("use_action.bow") + }; + int useActionIndex; + switch (linkedItem.itemUseAction) { + case 0: useActionIndex = 0; break; + case 1: useActionIndex = 1; break; + case 2: useActionIndex = 4; break; + case 3: useActionIndex = 2; break; + case 4: useActionIndex = 3; break; + default: useActionIndex = 0; break; + } + fontRendererObj.drawString(StatCollector.translateToLocal("display.useAction") + ": " + useActions[useActionIndex], x, y, labelColor, false); + y += 12; + + String[] armorOptions = { + StatCollector.translateToLocal("armor_type.none"), + StatCollector.translateToLocal("armor_type.all"), + StatCollector.translateToLocal("armor_type.head"), + StatCollector.translateToLocal("armor_type.chestplate"), + StatCollector.translateToLocal("armor_type.leggings"), + StatCollector.translateToLocal("armor_type.boots") + }; + int armorIndex; + if (linkedItem.armorType == -2) armorIndex = 0; + else if (linkedItem.armorType == -1) armorIndex = 1; + else armorIndex = linkedItem.armorType + 2; + fontRendererObj.drawString(StatCollector.translateToLocal("display.armor") + ": " + armorOptions[armorIndex], x, y, labelColor, false); + y += 12; + + labelColor = 0x7cff54; + fontRendererObj.drawString(StatCollector.translateToLocal("display.isTool") + ": " + ("" + linkedItem.isTool).toUpperCase(), x, y, labelColor, false); + y += 12; + + fontRendererObj.drawString(StatCollector.translateToLocal("display.isNormalItem") + ": " + ("" + linkedItem.isNormalItem).toUpperCase(), x, y, labelColor, false); + y += 12; + + labelColor = 0xce75fa; + fontRendererObj.drawString(StatCollector.translateToLocal("model.scale") + ": " + linkedItem.display.scaleX + ", " + linkedItem.display.scaleY + ", " + linkedItem.display.scaleZ, x, y, labelColor, false); + y += 12; + + fontRendererObj.drawString(StatCollector.translateToLocal("model.rotate") + ": " + linkedItem.display.rotationX + ", " + linkedItem.display.rotationY + ", " + linkedItem.display.rotationZ, x, y, labelColor, false); + y += 12; + + fontRendererObj.drawString(StatCollector.translateToLocal("model.translate") + ": " + linkedItem.display.translateX + ", " + linkedItem.display.translateY + ", " + linkedItem.display.translateZ, x, y, labelColor, false); + } + +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java index 3ac3923b4..c7f1b7ce4 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java @@ -65,15 +65,22 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void initGui() { super.initGui(); - // Toggle button - added FIRST to maintain stable buttonList index - // (prevents double-toggle when initGui() is called from actionPerformed) + // Fullscreen button - FIRST + GuiNpcButton fullBtn = new GuiNpcButton(66, guiLeft + 368, guiTop + 8, 45, 20, "gui.fullscreen"); + fullBtn.setTextColor(0x55FF55); + fullBtn.setHoverText("gui.fullscreen.tooltip"); + this.addButton(fullBtn); + + // Toggle button String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; - this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel)); + GuiNpcButton toggleBtn = new GuiNpcButton(10, guiLeft + 368, guiTop + 36, 45, 20, toggleLabel); + toggleBtn.setTextColor(showingBuiltIn ? 0x55FFFF : 0xFFFFFF); + this.addButton(toggleBtn); // Add/Remove buttons - only show for custom animations if (!showingBuiltIn) { - this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 60, 45, 20, "gui.add")); + this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 84, 45, 20, "gui.remove")); } if (scrollAnimations == null) { @@ -198,6 +205,11 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; + if (button.id == 66) { + mc.displayGuiScreen(new GuiAnimationDirectory(npc)); + return; + } + // Toggle button if (button.id == 10) { showingBuiltIn = !showingBuiltIn; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java index 57df39d20..741b7d2d8 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageEffects.java @@ -57,15 +57,21 @@ public class GuiNPCManageEffects extends GuiNPCInterface2 implements ICustomScro public void initGui() { super.initGui(); - addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); + // Fullscreen button - FIRST + GuiNpcButton fullBtn = new GuiNpcButton(66, guiLeft + 368, guiTop + 8, 45, 20, "gui.fullscreen"); + fullBtn.setTextColor(0x55FF55); + fullBtn.setHoverText("gui.fullscreen.tooltip"); + addButton(fullBtn); - addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 36, 45, 20, "gui.add")); + + addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 60, 45, 20, "gui.remove")); getButton(1).enabled = effect != null && effect.id != -1; - addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 56, 45, 20, "gui.copy")); + addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 84, 45, 20, "gui.copy")); getButton(2).enabled = effect != null && effect.id != -1; - addButton(new GuiNpcButton(3, guiLeft + 368, guiTop + 80, 45, 20, "gui.edit")); + addButton(new GuiNpcButton(3, guiLeft + 368, guiTop + 108, 45, 20, "gui.edit")); getButton(3).enabled = effect != null && effect.id != -1; if (scrollEffects == null) { @@ -106,6 +112,11 @@ public class GuiNPCManageEffects extends GuiNPCInterface2 implements ICustomScro PacketClient.sendClient(new EffectSavePacket(effect.writeToNBT(false), "")); } + if (button.id == 66) { + mc.displayGuiScreen(new GuiEffectDirectory(npc)); + return; + } + if (effect == null) return; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java index 52e78f7d6..eeeb69a77 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java @@ -86,6 +86,15 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, int y = guiTop + 8; + // Fullscreen button - FIRST (only for Items tab) + if (tab == 1) { + GuiNpcButton fullBtn = new GuiNpcButton(66, guiLeft + 368, y, 45, 20, "gui.fullscreen"); + fullBtn.setTextColor(0x55FF55); + fullBtn.setHoverText("gui.fullscreen.tooltip"); + this.addButton(fullBtn); + y += 22; + } + this.addButton(new GuiNpcButton(10, guiLeft + 368, y, 45, 20, "gui.npcs")); this.addButton(new GuiNpcButton(11, guiLeft + 368, y += 22, 45, 20, "gui.items")); getButton(10).enabled = tab == 1; @@ -216,6 +225,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, } private List getSearchList() { + if (data == null) return new ArrayList(); if (search.isEmpty()) { return new ArrayList(this.data.keySet()); } @@ -265,6 +275,11 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, scroll.setSelected(""); } + if (button.id == 66) { + mc.displayGuiScreen(new GuiLinkedItemDirectory(npc)); + return; + } + if (linkedItem == null) return; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index 5511eeb4f..b0398d48a 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -145,24 +145,34 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface public void initGui() { super.initGui(); + // Fullscreen button - FIRST + GuiNpcButton fullBtn = new GuiNpcButton(66, guiLeft + 368, guiTop + 8, 45, 20, "gui.fullscreen"); + fullBtn.setTextColor(0x55FF55); + fullBtn.setHoverText("gui.fullscreen.tooltip"); + addButton(fullBtn); + // Toggle button — cycles: Custom → Built-in → Chained String toggleLabel = showingChained ? "gui.chained" : (showingBuiltIn ? "gui.builtin" : "gui.custom"); - addButton(new GuiNpcButton(BTN_TOGGLE_VIEW, guiLeft + 368, guiTop + 52, 45, 20, toggleLabel)); + GuiNpcButton toggleBtn = new GuiNpcButton(BTN_TOGGLE_VIEW, guiLeft + 368, guiTop + 36, 45, 20, toggleLabel); + if (showingChained) toggleBtn.setTextColor(0xFFFF55); + else if (showingBuiltIn) toggleBtn.setTextColor(0x55FFFF); + else toggleBtn.setTextColor(0xFFFFFF); + addButton(toggleBtn); if (showingChained) { // Chained view buttons - addButton(new GuiNpcButton(BTN_ADD, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - addButton(new GuiNpcButton(BTN_REMOVE, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); + addButton(new GuiNpcButton(BTN_ADD, guiLeft + 368, guiTop + 60, 45, 20, "gui.add")); + addButton(new GuiNpcButton(BTN_REMOVE, guiLeft + 368, guiTop + 84, 45, 20, "gui.remove")); getButton(BTN_REMOVE).setEnabled(selected != null && !selected.isEmpty() && chainedData.containsKey(selected)); - addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit")); + addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 108, 45, 20, "gui.edit")); getButton(BTN_EDIT).setEnabled(selected != null && !selected.isEmpty() && selectedChain != null); } else if (!showingBuiltIn) { // Custom view buttons - addButton(new GuiNpcButton(BTN_ADD, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - addButton(new GuiNpcButton(BTN_REMOVE, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); + addButton(new GuiNpcButton(BTN_ADD, guiLeft + 368, guiTop + 60, 45, 20, "gui.add")); + addButton(new GuiNpcButton(BTN_REMOVE, guiLeft + 368, guiTop + 84, 45, 20, "gui.remove")); getButton(BTN_REMOVE).setEnabled(selected != null && !selected.isEmpty() && customData.containsKey(selected)); if (!currentIsBuiltIn) { - addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit")); + addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 108, 45, 20, "gui.edit")); getButton(BTN_EDIT).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); } } @@ -300,6 +310,13 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface int id = guibutton.id; + // Fullscreen + if (id == 66) { + previewExecutor.stop(); + mc.displayGuiScreen(new GuiAbilityDirectory(npc)); + return; + } + // User type filter: Both → NPC → Player → Both if (id == BTN_USER_TYPE_FILTER) { userTypeFilter = (userTypeFilter + 1) % 3; diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index 077c9fd9f..c431c9c01 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -12,7 +12,7 @@ import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.renderer.AnimationHelper; import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.global.GuiNPCManageLinked; +import net.minecraft.client.gui.GuiScreen; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiMenuTopButton; @@ -37,7 +37,7 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe private int tab = -1; private int colorPicked = 0; - public SubGuiLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { + public SubGuiLinkedItem(GuiScreen parent, LinkedItem linkedItem) { this.linkedItem = linkedItem; this.parent = parent; this.originalName = linkedItem.name; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiDirectoryCategorized.java b/src/main/java/noppes/npcs/client/gui/util/GuiDirectoryCategorized.java new file mode 100644 index 000000000..f8d3f316b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiDirectoryCategorized.java @@ -0,0 +1,755 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.controllers.data.Category; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.CustomNpcResourceListener; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.SubGuiEditText; +import noppes.npcs.constants.EnumScrollData; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Vector; + +/** + * Fullscreen categorized manager base class (Cloner-inspired layout). + * Left=categories with icons, Center=items, Right=preview+details. + * Top bar has item operations (Add/Remove/Clone/Move) + search. + */ +public abstract class GuiDirectoryCategorized extends GuiDirectory + implements IScrollData, IGuiData, GuiYesNoCallback, ISubGuiListener { + + // ========== Scrolls ========== + protected GuiCustomScrollIcons catScroll = new GuiCustomScrollIcons(this, 2); + protected GuiCustomScroll itemScroll = new GuiCustomScroll(this, 0); + + // ========== Data ========== + protected HashMap catData = new HashMap<>(); + protected HashMap itemData = new HashMap<>(); + + // ========== Selection ========== + protected String prevCatName = ""; + protected String prevItemName = ""; + protected int selectedCatId = -1; + + // ========== Search ========== + protected String catSearch = ""; + protected String itemSearch = ""; + + // ========== Preview ========== + protected float zoomed = 60; + protected float rotation; + protected int previewX, previewY, previewW, previewH; + + // ========== Move Workflow ========== + protected int movePhase = 0; // 0=off, 1=selecting items, 2=selecting destination + protected HashSet moveSelection = new HashSet<>(); + protected static final int MAX_MOVE_ITEMS = 5; + + // ========== New Item Category ========== + protected int pendingNewItemCatId = -1; + + // ========== Category Data Loaded ========== + private boolean itemDataLoaded = false; + + // ========== Collapse ========== + protected boolean leftCollapsed = false; + private static final ResourceLocation DIRECTORY_ICON = new ResourceLocation("customnpcs", "textures/gui/directory.png"); + + // ========== Scroll Reset Flag ========== + private boolean resetItemScroll = false; + + // ========== Nav list data ========== + private List navNames = new ArrayList<>(); + private List navIcons = new ArrayList<>(); + + // ========== Constructor ========== + public GuiDirectoryCategorized() { + super(); + leftPanelPercent = 0.15f; + rightPanelPercent = 0.35f; + minLeftPanelW = 120; + minRightPanelW = 150; + requestCategoryList(); + } + + /** + * Whether this GUI currently has a category panel. + * Subclasses override to return false for built-in modes. + */ + protected boolean hasCategories() { return true; } + + @Override + protected void computeLayout() { + if (leftCollapsed || !hasCategories()) { + leftPanelPercent = 0.0f; + minLeftPanelW = 0; + } else if (leftPanelPercent <= 0) { + leftPanelPercent = 0.15f; + minLeftPanelW = 120; + } + super.computeLayout(); + + // Force 50/50 split between center and right when left panel is hidden + if (leftPanelW <= 0) { + int available = usableW - 3 * gap; + rightPanelW = available / 2; + contentW = available - rightPanelW; + contentX = originX + gap; + rightX = contentX + contentW + gap; + } + } + + @Override + protected void drawPanels() { + if (leftPanelW <= 0) { + if (rightPanelW > 0) { + GuiUtil.drawRectD(rightX - 1, contentY - 1, rightX + rightPanelW + 1, originY + usableH + 1, panelBorder); + } + } else { + super.drawPanels(); + } + } + + // ========== Abstract hooks ========== + protected abstract String getTitle(); + protected abstract void requestCategoryList(); + protected abstract void requestItemsInCategory(int catId); + protected abstract void requestItemData(int itemId); + protected abstract void onSaveCategory(Category cat); + protected abstract void onRemoveCategory(int catId); + protected abstract void onAddItem(int catId); + protected abstract void onRemoveItem(int itemId); + protected abstract void onEditItem(); + protected abstract void onCloneItem(); + protected abstract void onItemReceived(NBTTagCompound compound); + protected abstract boolean hasSelectedItem(); + protected abstract int getSelectedItemId(); + + protected GuiScreen getWindowedVariant() { return null; } + protected void drawItemPreview(int centerX, int centerY, int mouseX, int mouseY, float partialTicks) {} + protected void drawItemDetails(int x, int y, int w) {} + protected void saveCurrentItem() {} + + protected void setPrevItemName(String name) { + this.prevItemName = name; + } + + /** + * Hook for subclasses to add buttons at the START of the top bar (before Add/Remove/Clone/Move). + * Return the new x position after adding buttons. + */ + protected int initExtraTopBarButtons(int x, int topBtnY) { + return x; + } + + // ========== Top Bar ========== + @Override + protected void initTopBar(int topBtnY) { + int x = originX + 2; + + // Collapse/Expand button + GuiNpcButton collapseBtn = new GuiNpcButton(19, x, topBtnY, btnH, btnH, ""); + collapseBtn.enabled = hasCategories() && movePhase == 0; + addButton(collapseBtn); + x += btnH + 2; + + x = initExtraTopBarButtons(x, topBtnY); + int topBtnW = 55; + + // ADD + GuiNpcButton addBtn = new GuiNpcButton(50, x, topBtnY, topBtnW, btnH, "gui.add"); + addBtn.enabled = selectedCatId >= 0 && movePhase == 0; + addButton(addBtn); + x += topBtnW + 2; + + // MOVE + String moveLabel = movePhase > 0 ? "Moving" : "Move"; + GuiNpcButton moveBtn = new GuiNpcButton(54, x, topBtnY, topBtnW, btnH, moveLabel); + moveBtn.enabled = selectedCatId >= 0 && !leftCollapsed && hasCategories(); + addButton(moveBtn); + x += topBtnW + 2; + + // Confirm (only during move phase 1) + if (movePhase == 1) { + GuiNpcButton confirmBtn = new GuiNpcButton(55, x, topBtnY, topBtnW, btnH, "Confirm"); + confirmBtn.enabled = !moveSelection.isEmpty(); + addButton(confirmBtn); + x += topBtnW + 2; + } + + // Right side: close, minimize + int closeX = originX + usableW - btnH - 2; + addButton(new GuiNpcButton(17, closeX, topBtnY, btnH, btnH, "X")); + int minimizeX = closeX - btnH - 2; + if (getWindowedVariant() != null) { + addButton(new GuiNpcButton(18, minimizeX, topBtnY, btnH, btnH, "-")); + } + + // Search field is now under the center panel — see initCenterPanel() + } + + // ========== Left Panel: Category Navigation ========== + @Override + protected void initLeftPanel() { + if (leftCollapsed || !hasCategories()) return; + boolean catSelected = selectedCatId > 0; // non-Uncategorized selected + int bottomRows = catSelected ? 3 : 2; + int bottomH = bottomRows * (btnH + gap); + int navH = contentH - bottomH; + + buildNavList(); + + int savedCatScrollY = catScroll.scrollY; + catScroll.clear(); + catScroll.colors.clear(); + catScroll.setSize(leftPanelW, navH); + catScroll.guiLeft = originX; + catScroll.guiTop = contentY; + catScroll.setListWithIcons(navNames, navIcons); + catScroll.setSelected(prevCatName); + catScroll.scrollY = Math.max(0, Math.min(savedCatScrollY, catScroll.maxScrollY)); + + // Phase 2: ensure scroll is clickable for destination selection + if (movePhase == 2) { + catScroll.setSelectable(true); + } + + addScroll(catScroll); + + // Category search + int searchY = contentY + navH + gap; + addTextField(new GuiNpcTextField(3, this, fontRendererObj, originX, searchY, leftPanelW, btnH, catSearch)); + + // Add category button + int addY = searchY + btnH + gap; + GuiNpcButton addCatBtn = new GuiNpcButton(40, originX, addY, leftPanelW, btnH, "gui.addCategory"); + if (movePhase > 0) addCatBtn.enabled = false; + addButton(addCatBtn); + + // Rename / Remove (non-Uncategorized only) + if (catSelected) { + int crudY = addY + btnH + gap; + int halfW = (leftPanelW - gap) / 2; + GuiNpcButton renameBtn = new GuiNpcButton(41, originX, crudY, halfW, btnH, "gui.edit"); + if (movePhase > 0) renameBtn.enabled = false; + addButton(renameBtn); + + GuiNpcButton removeBtn = new GuiNpcButton(42, originX + halfW + gap, crudY, halfW, btnH, "gui.remove"); + removeBtn.enabled = movePhase == 0 && isCategoryEmpty(selectedCatId); + addButton(removeBtn); + } + } + + // ========== Center Panel: Items ========== + @Override + protected void initCenterPanel() { + int searchH = btnH + gap; + int scrollH = contentH - (movePhase == 0 ? searchH : 0); + + int savedItemScrollY = resetItemScroll ? 0 : itemScroll.scrollY; + resetItemScroll = false; + + itemScroll.clear(); + itemScroll.colors.clear(); + itemScroll.setSize(contentW, scrollH); + itemScroll.guiLeft = contentX; + itemScroll.guiTop = contentY; + + if (movePhase == 1) { + itemScroll.multipleSelection = true; + itemScroll.setSelectable(true); + itemScroll.setSelectedList(moveSelection); + } else if (movePhase == 2) { + itemScroll.multipleSelection = true; + itemScroll.setSelectedList(moveSelection); + itemScroll.setSelectable(false); + for (String name : moveSelection) { + itemScroll.colors.put(name, 0xFFFF55); + } + } else { + itemScroll.multipleSelection = false; + itemScroll.setSelectable(true); + } + + itemScroll.setList(getItemSearchList()); + if (movePhase == 0) { + itemScroll.setSelected(prevItemName); + } + itemScroll.scrollY = Math.max(0, Math.min(savedItemScrollY, itemScroll.maxScrollY)); + addScroll(itemScroll); + + // Search field under center panel + if (movePhase == 0) { + int searchY = contentY + scrollH + gap; + addTextField(new GuiNpcTextField(1, this, fontRendererObj, contentX, searchY, contentW, btnH, itemSearch)); + } + } + + // ========== Right Panel: Preview + Edit/Copy/Remove ========== + @Override + protected void initRightPanel(int startY) { + // Preview area — reserve space for 3 buttons + ID label at bottom + int bottomH = (btnH + gap) * 2 + 14; // edit+copy row, remove row, ID label + previewX = rightX; + previewY = contentY; + previewW = rightPanelW; + previewH = contentH - bottomH - gap; + + // Bottom buttons: Edit + Copy on one row, Remove below + int btnY = contentY + contentH - btnH * 2 - gap; + int halfW = (rightPanelW - gap) / 2; + + GuiNpcButton editBtn = new GuiNpcButton(51, rightX, btnY, halfW, btnH, "gui.edit"); + editBtn.enabled = hasSelectedItem() && movePhase == 0; + addButton(editBtn); + + GuiNpcButton cloneBtn = new GuiNpcButton(52, rightX + halfW + gap, btnY, halfW, btnH, "gui.copy"); + cloneBtn.enabled = hasSelectedItem() && movePhase == 0; + addButton(cloneBtn); + + int removeY = btnY + btnH + gap; + GuiNpcButton removeBtn = new GuiNpcButton(53, rightX, removeY, rightPanelW, btnH, "gui.remove"); + removeBtn.enabled = hasSelectedItem() && movePhase == 0; + removeBtn.setTextColor(0xFF5555); + addButton(removeBtn); + } + + // ========== Drawing ========== + @Override + protected void drawOverlay(int mouseX, int mouseY, float partialTicks) { + // Draw collapse button icon + GuiNpcButton collapseBtn = getButton(19); + if (collapseBtn != null && collapseBtn.visible) { + mc.getTextureManager().bindTexture(DIRECTORY_ICON); + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + int iconSize = 16; + int iconX = collapseBtn.xPosition + (collapseBtn.width - iconSize) / 2; + int iconY = collapseBtn.yPosition + (collapseBtn.height - iconSize) / 2; + int texY; + if (!collapseBtn.enabled) { + texY = 32; + } else { + boolean hovered = mouseX >= collapseBtn.xPosition && mouseY >= collapseBtn.yPosition + && mouseX < collapseBtn.xPosition + collapseBtn.width + && mouseY < collapseBtn.yPosition + collapseBtn.height; + texY = hovered ? 16 : 0; + } + func_152125_a(iconX, iconY, 0, texY, 16, 16, iconSize, iconSize, 256, 256); + GL11.glDisable(GL11.GL_BLEND); + } + + if (!hasSubGui()) { + // Draw preview background + GuiUtil.drawRectD(previewX, previewY, previewX + previewW, previewY + previewH, 0xA0101010); + + // Zoom/rotation in preview area + if (isMouseOverPreview(mouseX, mouseY)) { + float wheel = Mouse.getDWheel() * 0.035f; + if (wheel != 0) { + zoomed += wheel; + zoomed = Math.max(5, Math.min(200, zoomed)); + } + if (Mouse.isButtonDown(0) || Mouse.isButtonDown(1)) { + rotation -= Mouse.getDX() * 0.75f; + } + } + + // Draw preview content + if (hasSelectedItem()) { + int centerX = previewX + previewW / 2; + int centerY = previewY + (int)(previewH * 0.75f); + drawItemPreview(centerX, centerY, mouseX, mouseY, partialTicks); + } + + // Draw details text + if (hasSelectedItem()) { + drawItemDetails(previewX + 4, previewY + 4, previewW - 8); + } + + // Draw ID (skip if -1, e.g. abilities use names not IDs) + if (hasSelectedItem() && getSelectedItemId() >= 0) { + String idStr = "ID: " + getSelectedItemId(); + int idY = contentY + contentH - btnH - 14; + fontRendererObj.drawString(idStr, previewX + 4, idY, 0xFFFFFF, true); + } + } + + // Move phase overlay text + if (movePhase == 1) { + String text = "Select Items to Move (max " + MAX_MOVE_ITEMS + ")"; + int textW = fontRendererObj.getStringWidth(text); + fontRendererObj.drawStringWithShadow(text, width / 2 - textW / 2, height - pad - 10, 0x55FF55); + } else if (movePhase == 2) { + String text = "Select Destination Category"; + int textW = fontRendererObj.getStringWidth(text); + fontRendererObj.drawStringWithShadow(text, width / 2 - textW / 2, height - pad - 10, 0xFF5555); + } + } + + protected boolean isMouseOverPreview(int mouseX, int mouseY) { + return mouseX >= previewX && mouseX <= previewX + previewW + && mouseY >= previewY && mouseY <= previewY + previewH; + } + + // ========== Nav List Builder ========== + private void buildNavList() { + navNames.clear(); + navIcons.clear(); + String searchLower = catSearch.toLowerCase(); + + // Uncategorized always first + String uncatName = "Uncategorized"; + if (searchLower.isEmpty() || uncatName.toLowerCase().contains(searchLower)) { + navNames.add(uncatName); + navIcons.add(GuiCustomScrollIcons.ICON_TAB); + } + + // Other categories + for (String name : catData.keySet()) { + if (name.equals(uncatName)) continue; + if (searchLower.isEmpty() || name.toLowerCase().contains(searchLower)) { + navNames.add(name); + navIcons.add(GuiCustomScrollIcons.ICON_FOLDER); + } + } + } + + protected boolean isCategoryEmpty(int catId) { + if (!itemDataLoaded) return false; + return itemData == null || itemData.isEmpty(); + } + + // ========== Search ========== + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + + GuiNpcTextField itemSearchField = getTextField(1); + if (itemSearchField != null) { + String newText = itemSearchField.getText().toLowerCase(); + if (!itemSearch.equals(newText)) { + itemSearch = newText; + itemScroll.setList(getItemSearchList()); + } + } + + GuiNpcTextField catSearchField = getTextField(3); + if (catSearchField != null) { + String newText = catSearchField.getText().toLowerCase(); + if (!catSearch.equals(newText)) { + catSearch = newText; + buildNavList(); + catScroll.setListWithIcons(navNames, navIcons); + catScroll.setSelected(prevCatName); + } + } + } + + protected List getItemSearchList() { + if (selectedCatId < 0) return new ArrayList<>(); + if (itemSearch.isEmpty()) return new ArrayList<>(itemData.keySet()); + List list = new ArrayList<>(); + for (String name : itemData.keySet()) { + if (name.toLowerCase().contains(itemSearch)) list.add(name); + } + return list; + } + + // ========== Actions ========== + @Override + protected void actionPerformed(GuiButton guibutton) { + int id = guibutton.id; + + // Close + if (id == 17) close(); + + // Collapse/Expand + if (id == 19 && hasCategories() && movePhase == 0) { + leftCollapsed = !leftCollapsed; + initGui(); + return; + } + + // Minimize + if (id == 18) { + GuiScreen windowed = getWindowedVariant(); + if (windowed != null) { + Minecraft.getMinecraft().displayGuiScreen(windowed); + return; + } + } + + // ===== Item operations ===== + + // Add item + if (id == 50 && selectedCatId >= 0 && movePhase == 0) { + pendingNewItemCatId = selectedCatId > 0 ? selectedCatId : -1; + onAddItem(selectedCatId); + requestItemsInCategory(selectedCatId); + } + + // Edit item + if (id == 51 && hasSelectedItem() && movePhase == 0) { + onEditItem(); + } + + // Clone item + if (id == 52 && hasSelectedItem() && movePhase == 0) { + onCloneItem(); + requestItemsInCategory(selectedCatId); + } + + // Remove item + if (id == 53 && hasSelectedItem() && movePhase == 0) { + GuiYesNo guiyesno = new GuiYesNo(this, itemScroll.getSelected(), StatCollector.translateToLocal("gui.delete"), 2); + displayGuiScreen(guiyesno); + } + + // Move button + if (id == 54) { + if (movePhase == 0) { + // Start move phase 1 + movePhase = 1; + moveSelection.clear(); + initGui(); + } else { + // Cancel move + movePhase = 0; + moveSelection.clear(); + initGui(); + } + } + + // Confirm move (transition phase 1 → 2) + if (id == 55 && movePhase == 1 && !moveSelection.isEmpty()) { + movePhase = 2; + initGui(); + } + + // ===== Left panel category operations ===== + + // Add category + if (id == 40 && movePhase == 0) { + String name = "New"; + while (catData.containsKey(name)) name += "_"; + Category cat = new Category(-1, name); + onSaveCategory(cat); + } + + // Rename category + if (id == 41 && selectedCatId > 0 && movePhase == 0) { + setSubGui(new SubGuiEditText(prevCatName)); + } + + // Remove category + if (id == 42 && selectedCatId > 0 && movePhase == 0) { + GuiYesNo guiyesno = new GuiYesNo(this, prevCatName, StatCollector.translateToLocal("gui.delete"), 5); + displayGuiScreen(guiyesno); + } + } + + // ========== Scroll Events ========== + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + // Category scroll + if (guiCustomScroll.id == 2) { + String selected = catScroll.getSelected(); + if (selected == null) return; + + // Move phase 2: destination selected + if (movePhase == 2) { + if (!selected.equals(prevCatName)) { + Integer destCatId = catData.get(selected); + if (destCatId != null) { + executeMoveItems(destCatId); + } + } + return; + } + + // Normal category selection + if (!selected.equals(prevCatName)) { + pendingNewItemCatId = -1; + itemDataLoaded = false; + Integer catId = catData.get(selected); + if (catId != null) { + selectedCatId = catId; + itemScroll.selected = -1; + prevItemName = ""; + requestItemsInCategory(selectedCatId); + prevCatName = selected; + resetItemScroll = true; + initGui(); + } + } + } + + // Item scroll + if (guiCustomScroll.id == 0) { + pendingNewItemCatId = -1; + if (movePhase == 1) { + // Enforce max selection during move + if (moveSelection.size() > MAX_MOVE_ITEMS) { + // Remove excess — keep only first MAX_MOVE_ITEMS + HashSet trimmed = new HashSet<>(); + int count = 0; + for (String s : moveSelection) { + if (count++ >= MAX_MOVE_ITEMS) break; + trimmed.add(s); + } + moveSelection.clear(); + moveSelection.addAll(trimmed); + itemScroll.setSelectedList(moveSelection); + } + // Update confirm button state + if (getButton(55) != null) { + getButton(55).enabled = !moveSelection.isEmpty(); + } + return; + } + + String selected = itemScroll.getSelected(); + if (selected != null && !selected.equals(prevItemName)) { + Integer itemId = itemData.get(selected); + if (itemId != null) { + requestItemData(itemId); + prevItemName = selected; + } + } + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll.id == 0 && hasSelectedItem() && movePhase == 0) { + onEditItem(); + } + } + + // ========== Move Execution ========== + protected abstract void sendMovePacket(int itemId, int destCatId); + + protected void executeMoveItems(int destCatId) { + for (String name : moveSelection) { + Integer itemId = itemData.get(name); + if (itemId != null) { + sendMovePacket(itemId, destCatId); + } + } + movePhase = 0; + moveSelection.clear(); + // Refresh current category items + if (selectedCatId >= 0) { + requestItemsInCategory(selectedCatId); + } + initGui(); + } + + // ========== Data from Server ========== + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.CATEGORY_LIST) { + // Category list arrived + String name = catScroll.getSelected(); + this.catData = data; + buildNavList(); + catScroll.setListWithIcons(navNames, navIcons); + if (name != null) catScroll.setSelected(name); + else catScroll.setSelected(prevCatName); + + // Auto-select Uncategorized on first load + if (selectedCatId < 0 && catData.containsKey("Uncategorized")) { + selectedCatId = catData.get("Uncategorized"); + prevCatName = "Uncategorized"; + catScroll.setSelected(prevCatName); + requestItemsInCategory(selectedCatId); + } + initGui(); + } else if (type == EnumScrollData.CATEGORY_GROUP) { + // Items within selected category + String name = itemScroll.getSelected(); + this.itemData = data; + this.itemDataLoaded = true; + itemScroll.setList(getItemSearchList()); + if (name != null) itemScroll.setSelected(name); + initGui(); + } + // Ignore OPTIONAL and other types silently + } + + @Override + public void setGuiData(NBTTagCompound compound) { + onItemReceived(compound); + if (pendingNewItemCatId > 0 && hasSelectedItem() && getSelectedItemId() >= 0) { + sendMovePacket(getSelectedItemId(), pendingNewItemCatId); + if (selectedCatId >= 0) requestItemsInCategory(selectedCatId); + } + pendingNewItemCatId = -1; + initGui(); + } + + // ========== SubGui ========== + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiEditText) { + if (!((SubGuiEditText) subgui).cancelled && selectedCatId > 0) { + String name = ((SubGuiEditText) subgui).text; + if (name != null && !name.isEmpty() && !catData.containsKey(name)) { + Category cat = new Category(selectedCatId, name); + onSaveCategory(cat); + catData.remove(prevCatName); + catData.put(name, selectedCatId); + prevCatName = name; + } + } + } + onSubGuiClosed(subgui); + } + + protected void onSubGuiClosed(SubGuiInterface subgui) {} + + // ========== Confirm Dialog ========== + @Override + public void confirmClicked(boolean result, int id) { + NoppesUtil.openGUI(player, this); + if (!result) return; + + // Remove category + if (id == 5 && selectedCatId > 0) { + onRemoveCategory(selectedCatId); + selectedCatId = -1; + prevCatName = ""; + itemData.clear(); + catScroll.selected = -1; + } + + // Remove item + if (id == 2 && itemScroll.getSelected() != null && itemData.containsKey(itemScroll.getSelected())) { + onRemoveItem(itemData.get(itemScroll.getSelected())); + itemData.clear(); + prevItemName = ""; + if (selectedCatId >= 0) requestItemsInCategory(selectedCatId); + } + initGui(); + } + + @Override + public void setSelected(String selected) {} + + @Override + public void save() {} +} diff --git a/src/main/java/noppes/npcs/constants/EnumCategoryType.java b/src/main/java/noppes/npcs/constants/EnumCategoryType.java new file mode 100644 index 000000000..444611a49 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumCategoryType.java @@ -0,0 +1,13 @@ +package noppes.npcs.constants; + +/** + * Identifies which controller a category operation targets. + * Used by category packets to route to the correct CategoryManager. + */ +public class EnumCategoryType { + public static final int EFFECT = 1; + public static final int ANIMATION = 2; + public static final int LINKED_ITEM = 3; + public static final int ABILITY = 4; + public static final int CHAINED_ABILITY = 5; +} diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index 3f680c188..bfa6e09a7 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -11,5 +11,7 @@ public enum EnumScrollData { BUILTIN_ABILITIES, ANIMATIONS, BUILTIN_ANIMATIONS, - CHAINED_ABILITIES + CHAINED_ABILITIES, + CATEGORY_GROUP, + CATEGORY_LIST } diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index e9a64bfe1..2d9758734 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; @@ -47,6 +48,7 @@ public class AnimationController implements IAnimationHandler { public static AnimationController Instance = new AnimationController(); private int lastUsedID = 0; + public CategoryManager categoryManager = new CategoryManager(); public LinkedHashMap registeredAnimations = new LinkedHashMap<>(); @@ -164,45 +166,62 @@ public class AnimationController implements IAnimationHandler { File dir = getDir(); if (!dir.exists()) { dir.mkdir(); - } else { - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - try { - Animation animation = new Animation(); - animation.readFromNBT(NBTJsonUtil.LoadFile(file)); - animation.name = file.getName().substring(0, file.getName().length() - 5); + return; + } - if (animation.id == -1) { - animation.id = getUnusedId(); - } + categoryManager.loadCategories(dir); - int originalID = animation.id; - int setID = animation.id; - while (bootOrder.containsKey(setID) || animations.containsKey(setID)) { - if (bootOrder.containsKey(setID)) - if (bootOrder.get(setID).equals(animation.name)) - break; + // Load uncategorized animations (root level .json files) + loadAnimationsFromDir(dir, CategoryManager.UNCATEGORIZED_ID); - setID++; - } - - animation.id = setID; - if (originalID != setID) { - LogWriter.info("Found Animation ID Mismatch: " + animation.name + ", New ID: " + setID); - animation.save(); - } - - animations.put(animation.id, animation); - } catch (Exception e) { - LogWriter.error("Error loading: " + file.getAbsolutePath(), e); - } - } + // Load categorized animations (subdirectories) + for (Map.Entry entry : categoryManager.getCategories().entrySet()) { + File catDir = categoryManager.getCategoryDir(entry.getKey()); + loadAnimationsFromDir(catDir, entry.getKey()); } saveAnimationMap(); } + private void loadAnimationsFromDir(File dir, int catId) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + Animation animation = new Animation(); + animation.readFromNBT(NBTJsonUtil.LoadFile(file)); + animation.name = file.getName().substring(0, file.getName().length() - 5); + + if (animation.id == -1) { + animation.id = getUnusedId(); + } + + int originalID = animation.id; + int setID = animation.id; + while (bootOrder.containsKey(setID) || animations.containsKey(setID)) { + if (bootOrder.containsKey(setID)) + if (bootOrder.get(setID).equals(animation.name)) + break; + + setID++; + } + + animation.id = setID; + if (originalID != setID) { + LogWriter.info("Found Animation ID Mismatch: " + animation.name + ", New ID: " + setID); + animation.save(); + } + + animations.put(animation.id, animation); + categoryManager.registerItem(animation.id, catId); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + public void addAnimationRegister(String namespace, Register.Animations register) { registeredAnimations.put(namespace, register); } @@ -265,7 +284,7 @@ public class AnimationController implements IAnimationHandler { saveAnimationMap(); // Save Animation File - File dir = this.getDir(); + File dir = categoryManager.getItemDir(animation.getID()); if (!dir.exists()) dir.mkdirs(); @@ -295,46 +314,29 @@ public class AnimationController implements IAnimationHandler { public void delete(String name) { Animation delete = getAnimationFromName(name); if (delete != null) { - // Reject deleting built-in animations - if (delete instanceof BuiltInAnimation) { - LogWriter.info("Cannot delete built-in animation: " + name); - return; - } - - Animation foundAnimation = this.animations.remove(delete.getID()); - if (foundAnimation != null && foundAnimation.name != null) { - File dir = this.getDir(); - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - if (file.getName().equals(foundAnimation.name + ".json")) { - file.delete(); - break; - } - } - - saveAnimationMap(); - } + delete(delete.getID()); } } public void delete(int id) { - // Only user animations have valid IDs (>= 0) if (id < 0 || !this.animations.containsKey(id)) return; + // Reject deleting built-in animations + Animation anim = this.animations.get(id); + if (anim instanceof BuiltInAnimation) { + LogWriter.info("Cannot delete built-in animation: " + anim.name); + return; + } + Animation foundAnimation = this.animations.remove(id); if (foundAnimation != null && foundAnimation.name != null) { - File dir = this.getDir(); - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - if (file.getName().equals(foundAnimation.name + ".json")) { - file.delete(); - break; - } + File dir = categoryManager.getItemDir(id); + File file = new File(dir, foundAnimation.name + ".json"); + if (file.exists()) { + file.delete(); } - + categoryManager.removeItem(id); saveAnimationMap(); } } @@ -553,5 +555,29 @@ public class AnimationController implements IAnimationHandler { } //////////////////////////////////////////////////////// + // CATEGORY HELPERS //////////////////////////////////////////////////////// + + public Map getCategoryScrollData() { + return categoryManager.getCategoryScrollData(); + } + + public void moveItemToCategory(int itemId, int catId) { + Animation animation = animations.get(itemId); + if (animation == null) return; + categoryManager.moveItem(itemId, animation.name + ".json", catId); + saveAnimationMap(); + } + + public Map getItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, animations.keySet()); + for (int itemId : itemIds) { + Animation animation = animations.get(itemId); + if (animation != null) { + map.put(animation.name, animation.id); + } + } + return map; + } } diff --git a/src/main/java/noppes/npcs/controllers/CategoryManager.java b/src/main/java/noppes/npcs/controllers/CategoryManager.java new file mode 100644 index 000000000..a4d14def4 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/CategoryManager.java @@ -0,0 +1,248 @@ +package noppes.npcs.controllers; + +import noppes.npcs.controllers.data.Category; + +import java.io.File; +import java.util.*; +import java.util.Arrays; + +/** + * Filesystem-based category manager for organizing items into named groups. + * Categories are subdirectories within the controller's base directory. + * Items without a category mapping are "Uncategorized" (root-level files). + * + * One instance per controller (Effect, Animation, LinkedItem, Form, Aura, Outline, etc.) + */ +public class CategoryManager { + public static final int UNCATEGORIZED_ID = 0; + public static final String UNCATEGORIZED_NAME = "Uncategorized"; + + private File baseDir; + private final HashMap categories = new HashMap<>(); + private final HashMap itemCategoryMap = new HashMap<>(); + private int lastUsedCatID = 0; + + /** + * Scan the base directory for subdirectories and create Category objects. + * Call at the start of controller loading, before loading items. + */ + public void loadCategories(File baseDir) { + loadCategories(baseDir, new String[0]); + } + + /** + * Scan the base directory for subdirectories and create Category objects, + * excluding any subdirectories whose names match excludeDirs. + */ + public void loadCategories(File baseDir, String... excludeDirs) { + this.baseDir = baseDir; + categories.clear(); + itemCategoryMap.clear(); + lastUsedCatID = 0; + + if (!baseDir.exists()) return; + + Set excludeSet = new HashSet<>(Arrays.asList(excludeDirs)); + File[] files = baseDir.listFiles(); + if (files == null) return; + for (File sub : files) { + if (!sub.isDirectory()) continue; + if (excludeSet.contains(sub.getName())) continue; + lastUsedCatID++; + categories.put(lastUsedCatID, new Category(lastUsedCatID, sub.getName())); + } + } + + /** + * Register an item as belonging to a category. + * Call when loading items from a subdirectory. + */ + public void registerItem(int itemId, int catId) { + if (catId > UNCATEGORIZED_ID) { + itemCategoryMap.put(itemId, catId); + } + } + + public void removeItem(int itemId) { + itemCategoryMap.remove(itemId); + } + + public File getItemDir(int itemId) { + int catId = itemCategoryMap.getOrDefault(itemId, UNCATEGORIZED_ID); + return getCategoryDir(catId); + } + + public File getCategoryDir(int catId) { + if (catId == UNCATEGORIZED_ID) return baseDir; + Category cat = categories.get(catId); + if (cat != null) return new File(baseDir, cat.title); + return baseDir; + } + + public HashMap getCategories() { + return categories; + } + + // ========== Category CRUD ========== + + public Category createCategory(String name) { + while (hasCategoryName(name)) name += "_"; + lastUsedCatID++; + Category cat = new Category(lastUsedCatID, name); + categories.put(cat.id, cat); + File dir = new File(baseDir, name); + if (!dir.exists()) dir.mkdirs(); + return cat; + } + + public void saveCategory(Category cat) { + if (cat.id <= 0) return; + Category existing = categories.get(cat.id); + if (existing != null && !existing.title.equals(cat.title)) { + String newTitle = cat.title; + while (hasCategoryName(newTitle)) newTitle += "_"; + File oldDir = new File(baseDir, existing.title); + File newDir = new File(baseDir, newTitle); + if (oldDir.exists() && !newDir.exists()) { + oldDir.renameTo(newDir); + } + cat.title = newTitle; + } + categories.put(cat.id, cat); + } + + public boolean removeCategory(int catId) { + return removeCategory(catId, null); + } + + public boolean removeCategory(int catId, Set allItemIds) { + if (catId <= UNCATEGORIZED_ID) return false; + Category cat = categories.get(catId); + if (cat == null) return false; + + // Server-side guard: refuse to delete if category has items + if (allItemIds != null) { + if (!isCategoryEmpty(catId, allItemIds)) return false; + } else { + // Without item set, check if any items are mapped to this category + if (itemCategoryMap.containsValue(catId)) return false; + } + + // Also check filesystem — directory must be empty or non-existent + File dir = new File(baseDir, cat.title); + if (dir.exists()) { + String[] contents = dir.list(); + if (contents != null && contents.length > 0) { + return false; + } + dir.delete(); + } + + categories.remove(catId); + Iterator> it = itemCategoryMap.entrySet().iterator(); + while (it.hasNext()) { + if (it.next().getValue() == catId) { + it.remove(); + } + } + return true; + } + + public void moveItem(int itemId, String fileName, int destCatId) { + int oldCatId = itemCategoryMap.getOrDefault(itemId, UNCATEGORIZED_ID); + if (oldCatId == destCatId) return; + + File oldDir = getCategoryDir(oldCatId); + File newDir = getCategoryDir(destCatId); + if (!newDir.exists()) newDir.mkdirs(); + + File oldFile = new File(oldDir, fileName); + File newFile = new File(newDir, fileName); + if (oldFile.exists()) { + oldFile.renameTo(newFile); + } + + if (destCatId == UNCATEGORIZED_ID) { + itemCategoryMap.remove(itemId); + } else { + itemCategoryMap.put(itemId, destCatId); + } + } + + // ========== Query ========== + + public boolean hasCategoryName(String name) { + for (Category cat : categories.values()) { + if (cat.title.equalsIgnoreCase(name)) return true; + } + return false; + } + + public boolean isCategoryEmpty(int catId, Set allItemIds) { + if (catId == UNCATEGORIZED_ID) { + for (int id : allItemIds) { + if (!itemCategoryMap.containsKey(id)) return false; + } + return true; + } + for (Map.Entry entry : itemCategoryMap.entrySet()) { + if (entry.getValue() == catId && allItemIds.contains(entry.getKey())) return false; + } + return true; + } + + public Category getCategory(int catId) { + return categories.get(catId); + } + + public int getItemCategory(int itemId) { + return itemCategoryMap.getOrDefault(itemId, UNCATEGORIZED_ID); + } + + public List getItemsInCategory(int catId, Set allItemIds) { + List result = new ArrayList<>(); + for (int itemId : allItemIds) { + int assignedCat = itemCategoryMap.getOrDefault(itemId, UNCATEGORIZED_ID); + if (assignedCat == catId) { + result.add(itemId); + } + } + return result; + } + + // ========== Scroll Data ========== + + public Map getCategoryScrollData() { + Map map = new HashMap<>(); + map.put(UNCATEGORIZED_NAME, UNCATEGORIZED_ID); + for (Category cat : categories.values()) { + map.put(cat.title, cat.id); + } + return map; + } + + /** + * Search all directories (root + subdirectories) for a file and delete it. + */ + public void deleteFile(String fileName) { + if (baseDir == null || !baseDir.exists()) return; + + File file = new File(baseDir, fileName); + if (file.exists()) { + file.delete(); + return; + } + + File[] subs = baseDir.listFiles(); + if (subs != null) { + for (File sub : subs) { + if (!sub.isDirectory()) continue; + File subFile = new File(sub, fileName); + if (subFile.exists()) { + subFile.delete(); + return; + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/CustomEffectController.java b/src/main/java/noppes/npcs/controllers/CustomEffectController.java index c5a1f9948..0938c2aa1 100644 --- a/src/main/java/noppes/npcs/controllers/CustomEffectController.java +++ b/src/main/java/noppes/npcs/controllers/CustomEffectController.java @@ -26,6 +26,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -43,6 +44,7 @@ public class CustomEffectController implements ICustomEffectHandler { public HashMap customEffectScriptHandlers = new HashMap<>(); private HashMap bootOrder; + public CategoryManager categoryManager = new CategoryManager(); private int lastUsedID = 0; public ConcurrentHashMap> playerEffects = new ConcurrentHashMap<>(); @@ -119,16 +121,13 @@ public class CustomEffectController implements ICustomEffectHandler { CustomEffect foundEffect = getCustomEffects().remove(effect.getID()); customEffectScriptHandlers.remove(effect.getID()); if (foundEffect != null && foundEffect.name != null) { - File dir = this.getDir(); - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - if (file.getName().equalsIgnoreCase(foundEffect.name + ".json")) { - file.delete(); - SyncController.syncRemove(EnumSyncType.CUSTOM_EFFECTS, foundEffect.getID()); - break; - } + File dir = categoryManager.getItemDir(id); + File file = new File(dir, foundEffect.name + ".json"); + if (file.exists()) { + file.delete(); } + categoryManager.removeItem(id); + SyncController.syncRemove(EnumSyncType.CUSTOM_EFFECTS, foundEffect.getID()); saveEffectLoadMap(); } } @@ -328,40 +327,58 @@ public class CustomEffectController implements ICustomEffectHandler { File dir = getDir(); if (!dir.exists()) { dir.mkdir(); - } else { - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - try { - CustomEffect effect = new CustomEffect(); - effect.readFromNBT(NBTJsonUtil.LoadFile(file)); - effect.name = file.getName().substring(0, file.getName().length() - 5); - if (effect.id == -1) { - effect.id = getUnusedId(); - } - int originalID = effect.id; - int setID = effect.id; - while (bootOrder.containsKey(setID) || getCustomEffects().containsKey(setID)) { - if (bootOrder.containsKey(setID)) - if (bootOrder.get(setID).equalsIgnoreCase(effect.name)) - break; - setID++; - } - effect.id = setID; - if (originalID != setID) { - LogWriter.info("Found Custom Effect ID Mismatch: " + effect.name + ", New ID: " + setID); - effect.save(); - } - getCustomEffects().put(effect.id, effect); - } catch (Exception e) { - LogWriter.error("Error loading: " + file.getAbsolutePath(), e); - } - } + return; } + + categoryManager.loadCategories(dir); + + // Load uncategorized effects (root level .json files) + loadEffectsFromDir(dir, CategoryManager.UNCATEGORIZED_ID); + + // Load categorized effects (subdirectories) + for (Map.Entry entry : categoryManager.getCategories().entrySet()) { + File catDir = categoryManager.getCategoryDir(entry.getKey()); + loadEffectsFromDir(catDir, entry.getKey()); + } + this.registerEffectMap(0, getCustomEffects()); saveEffectLoadMap(); } + private void loadEffectsFromDir(File dir, int catId) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + CustomEffect effect = new CustomEffect(); + effect.readFromNBT(NBTJsonUtil.LoadFile(file)); + effect.name = file.getName().substring(0, file.getName().length() - 5); + if (effect.id == -1) { + effect.id = getUnusedId(); + } + int originalID = effect.id; + int setID = effect.id; + while (bootOrder.containsKey(setID) || getCustomEffects().containsKey(setID)) { + if (bootOrder.containsKey(setID)) + if (bootOrder.get(setID).equalsIgnoreCase(effect.name)) + break; + setID++; + } + effect.id = setID; + if (originalID != setID) { + LogWriter.info("Found Custom Effect ID Mismatch: " + effect.name + ", New ID: " + setID); + effect.save(); + } + getCustomEffects().put(effect.id, effect); + categoryManager.registerItem(effect.id, catId); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + public HashMap getEffectMap(int index) { return indexMapper.get(index); } @@ -370,7 +387,7 @@ public class CustomEffectController implements ICustomEffectHandler { return new File(CustomNpcs.getWorldSaveDirectory(), "customeffects"); } - private void saveEffectLoadMap() { + public void saveEffectLoadMap() { try { File saveDir = getMapDir(); File file = new File(saveDir, "customeffects.dat_new"); @@ -453,12 +470,7 @@ public class CustomEffectController implements ICustomEffectHandler { } public void deleteEffectFile(String prevName) { - File dir = this.getDir(); - if (!dir.exists()) - dir.mkdirs(); - File file2 = new File(dir, prevName + ".json"); - if (file2.exists()) - file2.delete(); + categoryManager.deleteFile(prevName + ".json"); } // API Versions @@ -490,21 +502,7 @@ public class CustomEffectController implements ICustomEffectHandler { public void deleteEffect(String name) { ICustomEffect effect = getEffect(name); if (effect != null) { - CustomEffect foundEffect = getCustomEffects().remove(effect.getID()); - customEffectScriptHandlers.remove(effect.getID()); - if (foundEffect != null && foundEffect.name != null) { - File dir = this.getDir(); - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - if (file.getName().equalsIgnoreCase(foundEffect.name + ".json")) { - file.delete(); - SyncController.syncRemove(EnumSyncType.CUSTOM_EFFECTS, foundEffect.getID()); - break; - } - } - saveEffectLoadMap(); - } + delete(effect.getID()); } } @@ -632,6 +630,33 @@ public class CustomEffectController implements ICustomEffectHandler { return get(id, index); } + //////////////////////////////////////////////////////// + // CATEGORY HELPERS + //////////////////////////////////////////////////////// + + public Map getCategoryScrollData() { + return categoryManager.getCategoryScrollData(); + } + + public void moveItemToCategory(int itemId, int catId) { + CustomEffect effect = getCustomEffects().get(itemId); + if (effect == null) return; + categoryManager.moveItem(itemId, effect.name + ".json", catId); + saveEffectLoadMap(); + } + + public Map getItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, getCustomEffects().keySet()); + for (int itemId : itemIds) { + CustomEffect effect = getCustomEffects().get(itemId); + if (effect != null) { + map.put(effect.name, effect.id); + } + } + return map; + } + @Override public ICustomEffect saveEffect(ICustomEffect customEffect) { if (customEffect.getID() < 0) { @@ -648,7 +673,7 @@ public class CustomEffectController implements ICustomEffectHandler { saveEffectLoadMap(); - File dir = this.getDir(); + File dir = categoryManager.getItemDir(customEffect.getID()); if (!dir.exists()) dir.mkdirs(); diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index f318fb279..f8f6c4af6 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -20,6 +20,8 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.zip.GZIPInputStream; public class LinkedItemController implements ILinkedItemHandler { @@ -30,6 +32,7 @@ public class LinkedItemController implements ILinkedItemHandler { public HashMap linkedItemsScripts = new HashMap<>(); private HashMap bootOrder; + public CategoryManager categoryManager = new CategoryManager(); private LinkedItemController() { } @@ -121,45 +124,62 @@ public class LinkedItemController implements ILinkedItemHandler { File dir = getDir(); if (!dir.exists()) { dir.mkdir(); - } else { - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - try { - LinkedItem linkedItem = new LinkedItem(); - linkedItem.readFromNBT(NBTJsonUtil.LoadFile(file)); - linkedItem.name = file.getName().substring(0, file.getName().length() - 5); + return; + } - if (linkedItem.id == -1) { - linkedItem.id = getUnusedId(); - } + categoryManager.loadCategories(dir); - int originalID = linkedItem.id; - int setID = linkedItem.id; - while (bootOrder.containsKey(setID) || linkedItems.containsKey(setID)) { - if (bootOrder.containsKey(setID)) - if (bootOrder.get(setID).equals(linkedItem.name)) - break; + // Load uncategorized items (root level .json files) + loadItemsFromDir(dir, CategoryManager.UNCATEGORIZED_ID); - setID++; - } - - linkedItem.id = setID; - if (originalID != setID) { - LogWriter.info("Found Linked Item ID Mismatch: " + linkedItem.name + ", New ID: " + setID); - linkedItem.save(); - } - - linkedItems.put(linkedItem.id, linkedItem); - } catch (Exception e) { - LogWriter.error("Error loading: " + file.getAbsolutePath(), e); - } - } + // Load categorized items (subdirectories) + for (Map.Entry entry : categoryManager.getCategories().entrySet()) { + File catDir = categoryManager.getCategoryDir(entry.getKey()); + loadItemsFromDir(catDir, entry.getKey()); } saveLinkedItemsMap(); } + private void loadItemsFromDir(File dir, int catId) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) + continue; + try { + LinkedItem linkedItem = new LinkedItem(); + linkedItem.readFromNBT(NBTJsonUtil.LoadFile(file)); + linkedItem.name = file.getName().substring(0, file.getName().length() - 5); + + if (linkedItem.id == -1) { + linkedItem.id = getUnusedId(); + } + + int originalID = linkedItem.id; + int setID = linkedItem.id; + while (bootOrder.containsKey(setID) || linkedItems.containsKey(setID)) { + if (bootOrder.containsKey(setID)) + if (bootOrder.get(setID).equals(linkedItem.name)) + break; + + setID++; + } + + linkedItem.id = setID; + if (originalID != setID) { + LogWriter.info("Found Linked Item ID Mismatch: " + linkedItem.name + ", New ID: " + setID); + linkedItem.save(); + } + + linkedItems.put(linkedItem.id, linkedItem); + categoryManager.registerItem(linkedItem.id, catId); + } catch (Exception e) { + LogWriter.error("Error loading: " + file.getAbsolutePath(), e); + } + } + } + private File getDir() { return new File(CustomNpcs.getWorldSaveDirectory(), "linkeditems"); } @@ -200,7 +220,7 @@ public class LinkedItemController implements ILinkedItemHandler { saveLinkedItemsMap(); // Save Linked Item File - File dir = this.getDir(); + File dir = categoryManager.getItemDir(linkedItem.getId()); if (!dir.exists()) dir.mkdirs(); @@ -231,27 +251,19 @@ public class LinkedItemController implements ILinkedItemHandler { if (linkedItem != null) { LinkedItem foundItem = remove(id); if (foundItem != null && foundItem.name != null) { - File dir = this.getDir(); - for (File file : dir.listFiles()) { - if (!file.isFile() || !file.getName().endsWith(".json")) - continue; - if (file.getName().equalsIgnoreCase(foundItem.name + ".json")) { - file.delete(); - break; - } + File dir = categoryManager.getItemDir(id); + File file = new File(dir, foundItem.name + ".json"); + if (file.exists()) { + file.delete(); } + categoryManager.removeItem(id); saveLinkedItemsMap(); } } } public void deleteLinkedItemFile(String prevName) { - File dir = this.getDir(); - if (!dir.exists()) - dir.mkdirs(); - File file2 = new File(dir, prevName + ".json"); - if (file2.exists()) - file2.delete(); + categoryManager.deleteFile(prevName + ".json"); } @@ -350,5 +362,29 @@ public class LinkedItemController implements ILinkedItemHandler { } //////////////////////////////////////////////////////// + // CATEGORY HELPERS //////////////////////////////////////////////////////// + + public Map getCategoryScrollData() { + return categoryManager.getCategoryScrollData(); + } + + public void moveItemToCategory(int itemId, int catId) { + LinkedItem item = linkedItems.get(itemId); + if (item == null) return; + categoryManager.moveItem(itemId, item.name + ".json", catId); + saveLinkedItemsMap(); + } + + public Map getItemsByCategoryScrollData(int catId) { + Map map = new HashMap<>(); + List itemIds = categoryManager.getItemsInCategory(catId, linkedItems.keySet()); + for (int itemId : itemIds) { + LinkedItem item = linkedItems.get(itemId); + if (item != null) { + map.put(item.name, item.id); + } + } + return map; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/Category.java b/src/main/java/noppes/npcs/controllers/data/Category.java new file mode 100644 index 000000000..cbab80085 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/Category.java @@ -0,0 +1,31 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Simple category data class for the generic category system. + * Used by CategoryManager to organize items into named groups. + */ +public class Category { + public int id = -1; + public String title = ""; + + public Category() { + } + + public Category(int id, String title) { + this.id = id; + this.title = title; + } + + public void readNBT(NBTTagCompound compound) { + id = compound.getInteger("CatID"); + title = compound.getString("CatTitle"); + } + + public NBTTagCompound writeNBT(NBTTagCompound compound) { + compound.setInteger("CatID", id); + compound.setString("CatTitle", title); + return compound; + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index e23ef2d11..11241c318 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -155,6 +155,9 @@ gui.deselectAll=Deselect All gui.loading=Loading gui.off=Off gui.on=On +gui.fullscreen=FULL +gui.fullscreen.tooltip=Designed for Fullscreen Mode +gui.addCategory=Add Category gui.sure=Are you sure? gui.available=Requirements Met gui.items=Items diff --git a/src/main/resources/assets/customnpcs/textures/gui/directory.png b/src/main/resources/assets/customnpcs/textures/gui/directory.png new file mode 100644 index 0000000000000000000000000000000000000000..fc88425976b7a9d9221684967faef79bc57c7215 GIT binary patch literal 547 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;Kn_c~qpu?a z!^VE@KZ&eBK4*bPWHAE+-(e7DJf6QI1*l<%r;B4qMcmsP3j>b^NVHtsdo;jFhS$aY zBC}Sj3y`qleGtTS)Q07qrdspKH}7s4+4uK;<$lIOQ?v`1jjOStRu53o+!q`7Zq7@z+ZX zI`89w9(m`%2W3u%qbdvxCa7GX_lFbs|C)W~-wOGM$um7b-tu(yb6Mw<&;$Vee`=Qi literal 0 HcmV?d00001