From 0512588a7a2fa4a8814002d78c23ff0440a36876 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 18 Feb 2026 04:02:51 -0500 Subject: [PATCH] Add Chained Abilities --- src/api | 2 +- .../kamkeel/npcs/command/AbilityCommand.java | 163 +++- .../kamkeel/npcs/command/CommandKamkeel.java | 4 + .../{data/ability => }/AbilityController.java | 264 ++++++- .../npcs/controllers/SyncController.java | 12 +- .../controllers/data/ability/Ability.java | 37 +- .../data/ability/AbilityAction.java | 97 ++- .../controllers/data/ability/AbilitySlot.java | 1 + .../data/ability/ChainedAbility.java | 34 +- .../ability/ChainedAbilityController.java | 319 -------- .../data/ability/ChainedAbilityEntry.java | 140 +++- .../npcs/entity/EntityEnergyProjectile.java | 11 +- .../kamkeel/npcs/network/PacketHandler.java | 8 + .../npcs/network/enums/EnumRequestPacket.java | 4 + .../ability/BuiltInAbilityGetPacket.java | 2 +- .../ability/ChainedAbilitiesGetPacket.java | 51 ++ .../ability/ChainedAbilityGetPacket.java | 68 ++ .../ability/ChainedAbilityRemovePacket.java | 69 ++ .../ability/ChainedAbilitySavePacket.java | 79 ++ .../ability/CustomAbilityGetPacket.java | 2 +- .../ability/CustomAbilityRemovePacket.java | 2 +- .../ability/CustomAbilitySavePacket.java | 2 +- src/main/java/kamkeel/npcs/util/Register.java | 5 +- .../noppes/npcs/AbstractDataAbilities.java | 19 +- src/main/java/noppes/npcs/CustomNpcs.java | 6 +- src/main/java/noppes/npcs/DataAbilities.java | 3 +- .../java/noppes/npcs/NoppesUtilServer.java | 13 +- .../noppes/npcs/ScriptPlayerEventHandler.java | 2 +- .../noppes/npcs/client/ClientTickHandler.java | 2 +- .../client/gui/advanced/GuiNPCAbilities.java | 725 +++++++++++++----- .../gui/advanced/SubGuiAbilityEditMode.java | 14 +- .../gui/advanced/SubGuiAbilityLoad.java | 2 +- .../gui/advanced/SubGuiAbilityLoadMode.java | 14 +- .../advanced/SubGuiAbilitySaveConfirm.java | 6 + .../gui/advanced/SubGuiAbilitySelect.java | 162 ++++ .../gui/advanced/SubGuiAbilityTypeSelect.java | 2 +- .../gui/advanced/SubGuiChainSelect.java | 92 +++ .../advanced/SubGuiChainedAbilityConfig.java | 607 +++++++++++++++ .../advanced/SubGuiChainedEntrySource.java | 58 ++ .../gui/advanced/SubGuiLoadTypeChoice.java | 47 ++ .../gui/advanced/SubGuiNpcSlotPicker.java | 113 +++ .../gui/advanced/SubGuiSimpleChoice.java | 23 + .../gui/global/GuiNpcManageAbilities.java | 381 ++++++--- .../util/IChainedAbilityConfigCallback.java | 10 + .../noppes/npcs/constants/EnumScrollData.java | 3 +- .../controllers/data/PlayerAbilityData.java | 11 +- .../java/noppes/npcs/scripted/NpcAPI.java | 4 +- .../scripted/entity/ScriptEnergySweeper.java | 9 + .../scripted/wrapper/ScriptDataAbilities.java | 13 +- .../wrapper/ScriptPlayerAbilityData.java | 20 +- .../assets/customnpcs/lang/en_US.lang | 18 + 51 files changed, 2955 insertions(+), 800 deletions(-) rename src/main/java/kamkeel/npcs/controllers/{data/ability => }/AbilityController.java (76%) delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityController.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java create mode 100644 src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java diff --git a/src/api b/src/api index 010849fe8..28c0ca840 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 010849fe8fb67c0359d7ebdb7bcd5c3c957fe3db +Subproject commit 28c0ca840f3d726c07b47aafa29d97b51a69a0cd diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 3c7a0e728..5c5a69f88 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -1,13 +1,14 @@ package kamkeel.npcs.command; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; -import kamkeel.npcs.controllers.data.ability.ChainedAbilityController; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; import kamkeel.npcs.controllers.data.ability.UserType; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayerMP; import noppes.npcs.NoppesUtilServer; import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerAbilityData; import noppes.npcs.controllers.data.PlayerData; import java.util.ArrayList; @@ -78,7 +79,6 @@ public class AbilityCommand extends CommandKamkeelBase { ) public void reload(ICommandSender sender, String[] args) { AbilityController.Instance.load(); - ChainedAbilityController.Instance.load(); sendResult(sender, "Abilities and chained abilities reloaded from disk."); } @@ -112,7 +112,7 @@ public class AbilityCommand extends CommandKamkeelBase { return; } - String key = args[0]; + String key = joinArgs(args, 0); Ability ability = AbilityController.Instance.resolveAbility(key); if (ability == null) { sendError(sender, "No ability found for: " + key); @@ -165,26 +165,24 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Unlock an ability for a player", + desc = "Give an ability to a player", usage = " " ) - public void unlock(ICommandSender sender, String[] args) { + public void give(ICommandSender sender, String[] args) { if (args.length < 2) { - sendError(sender, "Usage: /kam ability unlock "); + sendError(sender, "Usage: /kam ability give "); return; } String playerName = args[0]; String abilityKey = joinArgs(args, 1); - // Find player EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); if (player == null) { sendError(sender, "Player not found: " + playerName); return; } - // Resolve ability Ability ability = AbilityController.Instance.resolveAbility(abilityKey); if (ability == null) { sendError(sender, "Ability not found: " + abilityKey); @@ -194,17 +192,15 @@ public class AbilityCommand extends CommandKamkeelBase { // Use the ability's canonical ID for storage (registry key for built-in, UUID for custom) String canonicalKey = ability.getId(); if (canonicalKey == null || canonicalKey.isEmpty()) { - canonicalKey = abilityKey; // Fallback to user-provided key + canonicalKey = abilityKey; } String displayName = ability.getDisplayName(); - // Check if ability allows players if (!ability.getAllowedBy().allowsPlayer()) { sendError(sender, "Ability '\u00A7b" + displayName + "\u00A7c' is NPC-only and cannot be given to players."); return; } - // Get player data and unlock PlayerData data = PlayerDataController.Instance.getPlayerData(player); if (data.abilityData.hasUnlockedAbility(canonicalKey)) { sendError(sender, "Player already has ability: " + displayName); @@ -212,30 +208,28 @@ public class AbilityCommand extends CommandKamkeelBase { } data.abilityData.unlockAbility(canonicalKey); - sendResult(sender, "Unlocked ability '\u00A7b" + displayName + "\u00A77' for player \u00A7a" + playerName); + sendResult(sender, "Gave ability '\u00A7b" + displayName + "\u00A77' to player \u00A7a" + playerName); } @SubCommand( - desc = "Lock (revoke) an ability from a player", + desc = "Remove an ability from a player", usage = " " ) - public void lock(ICommandSender sender, String[] args) { + public void remove(ICommandSender sender, String[] args) { if (args.length < 2) { - sendError(sender, "Usage: /kam ability lock "); + sendError(sender, "Usage: /kam ability remove "); return; } String playerName = args[0]; String abilityKey = joinArgs(args, 1); - // Find player EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); if (player == null) { sendError(sender, "Player not found: " + playerName); return; } - // Resolve ability to get canonical key (registry key for built-in, UUID for custom) Ability ability = AbilityController.Instance.resolveAbility(abilityKey); String canonicalKey = abilityKey; String displayName = abilityKey; @@ -244,7 +238,6 @@ public class AbilityCommand extends CommandKamkeelBase { displayName = ability.getDisplayName(); } - // Get player data and lock PlayerData data = PlayerDataController.Instance.getPlayerData(player); if (!data.abilityData.hasUnlockedAbility(canonicalKey)) { sendError(sender, "Player doesn't have ability: " + displayName); @@ -252,7 +245,98 @@ public class AbilityCommand extends CommandKamkeelBase { } data.abilityData.lockAbility(canonicalKey); - sendResult(sender, "Locked ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName); + sendResult(sender, "Removed ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName); + } + + @SubCommand( + desc = "Give a chained ability to a player", + usage = " " + ) + public void giveChain(ICommandSender sender, String[] args) { + if (args.length < 2) { + sendError(sender, "Usage: /kam ability giveChain "); + return; + } + + String playerName = args[0]; + String chainKey = joinArgs(args, 1); + + EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); + if (player == null) { + sendError(sender, "Player not found: " + playerName); + return; + } + + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainKey); + if (chain == null) { + sendError(sender, "Chained ability not found: " + chainKey); + return; + } + + if (!chain.getAllowedBy().allowsPlayer()) { + sendError(sender, "Chained ability '\u00A7b" + chain.getDisplayName() + "\u00A7c' is NPC-only and cannot be given to players."); + return; + } + + String chainId = chain.getId(); + if (chainId == null || chainId.isEmpty()) { + sendError(sender, "Chained ability has no ID: " + chain.getDisplayName()); + return; + } + + // Store by UUID: chain: + String storageKey = PlayerAbilityData.CHAIN_PREFIX + chainId; + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data.abilityData.hasUnlockedAbility(storageKey)) { + sendError(sender, "Player already has chained ability: " + chain.getDisplayName()); + return; + } + + data.abilityData.unlockAbility(storageKey); + sendResult(sender, "Gave chained ability '\u00A7b" + chain.getDisplayName() + "\u00A77' to player \u00A7a" + playerName); + } + + @SubCommand( + desc = "Remove a chained ability from a player", + usage = " " + ) + public void removeChain(ICommandSender sender, String[] args) { + if (args.length < 2) { + sendError(sender, "Usage: /kam ability removeChain "); + return; + } + + String playerName = args[0]; + String chainKey = joinArgs(args, 1); + + EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); + if (player == null) { + sendError(sender, "Player not found: " + playerName); + return; + } + + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainKey); + if (chain == null) { + sendError(sender, "Chained ability not found: " + chainKey); + return; + } + + String chainId = chain.getId(); + if (chainId == null || chainId.isEmpty()) { + sendError(sender, "Chained ability has no ID: " + chain.getDisplayName()); + return; + } + + // Storage uses UUID: chain: + String storageKey = PlayerAbilityData.CHAIN_PREFIX + chainId; + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (!data.abilityData.hasUnlockedAbility(storageKey)) { + sendError(sender, "Player doesn't have chained ability: " + chain.getDisplayName()); + return; + } + + data.abilityData.lockAbility(storageKey); + sendResult(sender, "Removed chained ability '\u00A7b" + chain.getDisplayName() + "\u00A77' from player \u00A7a" + playerName); } @SubCommand( @@ -286,14 +370,23 @@ public class AbilityCommand extends CommandKamkeelBase { int selected = data.abilityData.getSelectedIndex(); for (int i = 0; i < abilityKeys.length; i++) { String key = abilityKeys[i]; - Ability ability = AbilityController.Instance.resolveAbility(key); - String displayName = ability != null ? ability.getDisplayName() : key; String prefix = (i == selected) ? "\u00A7e> " : " "; - // Show display name and key if different - if (!displayName.equals(key)) { - sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]"); + + if (key.startsWith(PlayerAbilityData.CHAIN_PREFIX)) { + // Chained ability + String chainName = key.substring(PlayerAbilityData.CHAIN_PREFIX.length()); + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainName); + String displayName = chain != null ? chain.getDisplayName() : chainName; + sendResult(sender, prefix + "\u00A7d[Chain] \u00A7b" + displayName); } else { - sendResult(sender, prefix + "\u00A7b" + displayName); + // Regular ability + Ability ability = AbilityController.Instance.resolveAbility(key); + String displayName = ability != null ? ability.getDisplayName() : key; + if (!displayName.equals(key)) { + sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]"); + } else { + sendResult(sender, prefix + "\u00A7b" + displayName); + } } } } @@ -310,7 +403,7 @@ public class AbilityCommand extends CommandKamkeelBase { /** * Get list of player-usable ability names (for tab completion). * Resolves UUIDs to names so tab completion shows human-readable names. - * Used by CommandKamkeel for usage token. + * Used by CommandKamkeel for {@code } usage token. */ public static List getPlayerAbilityNames() { Set keys = AbilityController.Instance.getPlayerAbilityKeys(); @@ -322,4 +415,20 @@ public class AbilityCommand extends CommandKamkeelBase { Collections.sort(names); return names; } + + /** + * Get list of player-usable chained ability names (for tab completion). + * Used by CommandKamkeel for {@code } usage token. + */ + public static List getPlayerChainNames() { + List names = new ArrayList<>(); + for (String chainName : AbilityController.Instance.getChainedAbilityNamesSet()) { + ChainedAbility chain = AbilityController.Instance.getChainedAbility(chainName); + if (chain != null && chain.getAllowedBy().allowsPlayer()) { + names.add(chainName); + } + } + Collections.sort(names); + return names; + } } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index 91356c825..0f0e0dbbf 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import static kamkeel.npcs.command.AbilityCommand.getPlayerAbilityNames; +import static kamkeel.npcs.command.AbilityCommand.getPlayerChainNames; import static kamkeel.npcs.command.EffectCommand.getSortedEffectNames; public class CommandKamkeel extends CommandBase { @@ -139,6 +140,9 @@ public class CommandKamkeel extends CommandBase { } else if (usage.equals("")) { List keys = getPlayerAbilityNames(); return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()])); + } else if (usage.equals("")) { + List keys = getPlayerChainNames(); + return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()])); } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java similarity index 76% rename from src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java rename to src/main/java/kamkeel/npcs/controllers/AbilityController.java index 2af31f73a..13101f599 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -1,7 +1,14 @@ -package kamkeel.npcs.controllers.data.ability; +package kamkeel.npcs.controllers; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import kamkeel.npcs.controllers.data.ability.IAbilityExtender; +import kamkeel.npcs.controllers.data.ability.IAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.ability.type.*; -import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome; @@ -47,6 +54,11 @@ public class AbilityController implements IAbilityHandler { Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE); + // ── Chained Abilities ────────────────────────────────────────────────── + private final Map chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility + private final Map chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility + private int chainedAbilityRevision = 0; + // ── Derived State ──────────────────────────────────────────────────────── private final Set builtInTypeIds = new HashSet<>(); @@ -240,6 +252,8 @@ public class AbilityController implements IAbilityHandler { LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)"); } LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); + + loadChainedAbilities(); } private boolean nameFileExists(File dir, String name) { @@ -381,6 +395,229 @@ public class AbilityController implements IAbilityHandler { return dir; } + // ═══════════════════════════════════════════════════════════════════════════ + // CHAINED ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + + private void loadChainedAbilities() { + chainedAbilities.clear(); + chainedAbilitiesById.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 key = filename.substring(0, filename.length() - 5); + + NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt); + + chain.setName(key); + chainedAbilities.put(key, chain); + + String uuid = chain.getId(); + if (uuid != null && !uuid.isEmpty()) { + chainedAbilitiesById.put(uuid, chain); + } + } catch (Exception e) { + LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e); + } + } + } + + chainedAbilityRevision++; + LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities"); + } + + public boolean saveChainedAbility(ChainedAbility chain) { + if (chain == null) return false; + + String name = chain.getName(); + if (name == null || name.isEmpty()) return false; + + String uuid = chain.getId(); + boolean isNew = (uuid == null || uuid.isEmpty()); + + if (isNew) { + uuid = UUID.randomUUID().toString(); + chain.setId(uuid); + + while (hasChainedAbilityName(name)) { + name = name + "_"; + } + chain.setName(name); + } else { + ChainedAbility existing = chainedAbilitiesById.get(uuid); + if (existing != null) { + String oldName = existing.getName(); + if (!oldName.equals(name)) { + String testName = name; + while (chainedAbilities.containsKey(testName) && !testName.equals(oldName)) { + testName = testName + "_"; + } + if (!testName.equals(name)) { + name = testName; + chain.setName(name); + } + + File oldFile = new File(getChainedDir(), oldName + ".json"); + if (oldFile.exists()) { + oldFile.delete(); + } + chainedAbilities.remove(oldName); + } + } + } + + File dir = getChainedDir(); + File fileNew = new File(dir, name + ".json_new"); + File fileCurrent = new File(dir, name + ".json"); + + try { + NBTTagCompound nbt = chain.writeNBT(); + NBTJsonUtil.SaveFile(fileNew, nbt); + + if (fileCurrent.exists()) { + fileCurrent.delete(); + } + fileNew.renameTo(fileCurrent); + if (fileNew.exists()) { + fileNew.delete(); + } + + chainedAbilities.put(name, chain); + chainedAbilitiesById.put(uuid, chain); + chainedAbilityRevision++; + LogWriter.info("Saved chained ability: " + name + " [" + uuid + "]"); + SyncController.syncAllChainedAbilities(); + return true; + } catch (Exception e) { + LogWriter.error("Error saving chained ability: " + name, e); + return false; + } + } + + public boolean deleteChainedAbility(String name) { + if (name == null || name.isEmpty()) return false; + + ChainedAbility removed = chainedAbilities.remove(name); + if (removed == null) return false; + + String uuid = removed.getId(); + if (uuid != null && !uuid.isEmpty()) { + chainedAbilitiesById.remove(uuid); + } + + File dir = getChainedDir(); + File file = new File(dir, name + ".json"); + if (file.exists()) { + file.delete(); + } + + chainedAbilityRevision++; + LogWriter.info("Deleted chained ability: " + name); + SyncController.syncAllChainedAbilities(); + return true; + } + + public ChainedAbility resolveChainedAbility(String key) { + if (key == null || key.isEmpty()) return null; + + ChainedAbility byId = chainedAbilitiesById.get(key); + if (byId != null) return byId.deepCopy(); + + ChainedAbility chain = chainedAbilities.get(key); + if (chain != null) return chain.deepCopy(); + + for (Map.Entry entry : chainedAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue().deepCopy(); + } + } + + return null; + } + + public boolean canResolveChainedAbility(String key) { + if (key == null || key.isEmpty()) return false; + if (chainedAbilitiesById.containsKey(key)) return true; + if (chainedAbilities.containsKey(key)) return true; + for (String name : chainedAbilities.keySet()) { + if (name.equalsIgnoreCase(key)) return true; + } + return false; + } + + @Override + public ChainedAbility getChainedAbility(String name) { + ChainedAbility chain = chainedAbilities.get(name); + return chain != null ? chain.deepCopy() : null; + } + + public ChainedAbility getChainedAbilityByUUID(String uuid) { + return chainedAbilitiesById.get(uuid); + } + + public Set getChainedAbilityNamesSet() { + return new LinkedHashSet<>(chainedAbilities.keySet()); + } + + public Map getChainedAbilities() { + return chainedAbilities; + } + + @Override + public String[] getChainedAbilityNames() { + return chainedAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasChainedAbilityName(String name) { + return chainedAbilities.containsKey(name); + } + + @Override + public boolean deleteChainedAbilityByName(String name) { + return deleteChainedAbility(name); + } + + @Override + public boolean saveChainedAbility(noppes.npcs.api.ability.IChainedAbility chain) { + if (chain instanceof ChainedAbility) { + return saveChainedAbility((ChainedAbility) chain); + } + return false; + } + + public int getChainedAbilityRevision() { + return chainedAbilityRevision; + } + + public void setChainedAbilities(Map synced) { + chainedAbilities.clear(); + chainedAbilitiesById.clear(); + chainedAbilities.putAll(synced); + for (ChainedAbility chain : synced.values()) { + String uuid = chain.getId(); + if (uuid != null && !uuid.isEmpty()) { + chainedAbilitiesById.put(uuid, chain); + } + } + chainedAbilityRevision++; + } + + private File getChainedDir() { + File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities" + File.separator + "chained"); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir; + } + // ═══════════════════════════════════════════════════════════════════════════ // RESOLUTION // ═══════════════════════════════════════════════════════════════════════════ @@ -525,11 +762,7 @@ public class AbilityController implements IAbilityHandler { Ability ability = resolveAbility(key); if (ability != null) return ability; - if (ChainedAbilityController.Instance != null) { - return ChainedAbilityController.Instance.resolve(key); - } - - return null; + return resolveChainedAbility(key); } /** @@ -537,8 +770,7 @@ public class AbilityController implements IAbilityHandler { */ public boolean canResolveAction(String key) { if (canResolveAbility(key)) return true; - return ChainedAbilityController.Instance != null - && ChainedAbilityController.Instance.canResolve(key); + return canResolveChainedAbility(key); } // ═══════════════════════════════════════════════════════════════════════════ @@ -715,18 +947,4 @@ public class AbilityController implements IAbilityHandler { return deleteCustomAbility(name); } - @Override - public String[] getChainedAbilityNames() { - return ChainedAbilityController.Instance.getNames().toArray(new String[0]); - } - - @Override - public boolean hasChainedAbilityName(String name) { - return ChainedAbilityController.Instance.hasName(name); - } - - @Override - public boolean deleteChainedAbilityByName(String name) { - return ChainedAbilityController.Instance.delete(name); - } } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 6789e1227..014568af3 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -40,9 +40,7 @@ import noppes.npcs.controllers.data.QuestCategory; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.ChainedAbility; -import kamkeel.npcs.controllers.data.ability.ChainedAbilityController; import java.io.IOException; import java.util.EnumMap; @@ -356,7 +354,7 @@ public class SyncController { public static NBTTagCompound chainedAbilitiesNBT() { NBTTagList list = new NBTTagList(); NBTTagCompound compound = new NBTTagCompound(); - for (Map.Entry entry : ChainedAbilityController.Instance.getChainedAbilities().entrySet()) { + for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { NBTTagCompound chainNBT = entry.getValue().writeNBT(); chainNBT.setString("ChainedAbilityId", entry.getKey()); list.appendTag(chainNBT); @@ -651,11 +649,11 @@ public class SyncController { LinkedHashMap sync = new LinkedHashMap<>(); for (int i = 0; i < list.tagCount(); i++) { NBTTagCompound nbt = list.getCompoundTagAt(i); - String uuid = nbt.getString("CustomAbilityId"); + String name = nbt.getString("CustomAbilityId"); Ability ability = AbilityController.Instance.fromNBT(nbt); if (ability != null) { - ability.setId(uuid); - sync.put(uuid, ability); + ability.setName(name); + sync.put(name, ability); } } AbilityController.Instance.setCustomAbilities(sync); @@ -672,7 +670,7 @@ public class SyncController { chain.setName(id); sync.put(id, chain); } - ChainedAbilityController.Instance.setChainedAbilities(sync); + AbilityController.Instance.setChainedAbilities(sync); break; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 854766529..54529b7f3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -2,6 +2,7 @@ package kamkeel.npcs.controllers.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -1219,12 +1220,10 @@ public abstract class Ability implements IAbility, IAbilityAction { } nbt.setTag("effects", effectList); - // Type-specific (skip for built-in abilities with fixed configs) - if (!builtIn) { - NBTTagCompound typeNBT = new NBTTagCompound(); - writeTypeNBT(typeNBT); - nbt.setTag("typeData", typeNBT); - } + // Type-specific + NBTTagCompound typeNBT = new NBTTagCompound(); + writeTypeNBT(typeNBT); + nbt.setTag("typeData", typeNBT); return nbt; } @@ -1234,16 +1233,20 @@ public abstract class Ability implements IAbility, IAbilityAction { name = nbt.getString("name"); displayName = nbt.getString("displayName"); typeId = nbt.getString("typeId"); - weight = nbt.getInteger("weight"); - enabled = nbt.getBoolean("enabled"); - targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode")); + weight = nbt.hasKey("weight") ? nbt.getInteger("weight") : 10; + enabled = nbt.hasKey("enabled") ? nbt.getBoolean("enabled") : true; + try { + targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode")); + } catch (Exception e) { + targetingMode = TargetingMode.AGGRO_TARGET; + } minRange = nbt.getFloat("minRange"); maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); - syncWindupWithAnimation = nbt.getBoolean("syncWindup"); - dazedTicks = nbt.getInteger("recovery"); - interruptible = nbt.getBoolean("interruptible"); + syncWindupWithAnimation = nbt.hasKey("syncWindup") ? nbt.getBoolean("syncWindup") : true; + dazedTicks = nbt.hasKey("recovery") ? nbt.getInteger("recovery") : 80; + interruptible = nbt.hasKey("interruptible") ? nbt.getBoolean("interruptible") : true; lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase")); @@ -1257,7 +1260,7 @@ public abstract class Ability implements IAbility, IAbilityAction { windUpAnimationName = nbt.getString("windUpAnimationName"); activeAnimationName = nbt.getString("activeAnimationName"); dazedAnimationName = nbt.getString("dazedAnimationName"); - showTelegraph = nbt.getBoolean("showTelegraph"); + showTelegraph = nbt.hasKey("showTelegraph") ? nbt.getBoolean("showTelegraph") : true; try { telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType")); } catch (Exception e) { @@ -1299,8 +1302,8 @@ public abstract class Ability implements IAbility, IAbilityAction { } } - // Type-specific (skip for built-in abilities with fixed configs) - if (!builtIn) { + // Type-specific + if (nbt.hasKey("typeData")) { readTypeNBT(nbt.getCompoundTag("typeData")); } } @@ -1328,9 +1331,11 @@ public abstract class Ability implements IAbility, IAbilityAction { /** * Get the display name for this ability. * Returns displayName if set, otherwise falls back to name. + * Converts & color codes to § for rendering. */ public String getDisplayName() { - return (displayName != null && !displayName.isEmpty()) ? displayName : name; + String result = (displayName != null && !displayName.isEmpty()) ? displayName : name; + return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : ""; } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java index 6d721ff81..e42c52980 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java @@ -1,5 +1,6 @@ package kamkeel.npcs.controllers.data.ability; +import kamkeel.npcs.controllers.AbilityController; import net.minecraft.nbt.NBTTagCompound; /** @@ -14,11 +15,13 @@ public class AbilityAction { public enum SlotType { INLINE_ABILITY, ABILITY_REFERENCE, - CHAIN_REFERENCE + CHAIN_REFERENCE, + INLINE_CHAIN } private SlotType slotType; private Ability inlineAbility; + private ChainedAbility inlineChain; private String referenceId; /** Per-slot enabled override for reference slots (null = use master's default). */ @@ -55,6 +58,13 @@ public class AbilityAction { return slot; } + public static AbilityAction inlineChain(ChainedAbility chain) { + AbilityAction slot = new AbilityAction(); + slot.slotType = SlotType.INLINE_CHAIN; + slot.inlineChain = chain; + return slot; + } + // ═══════════════════════════════════════════════════════════════════ // QUERIES // ═══════════════════════════════════════════════════════════════════ @@ -64,13 +74,21 @@ public class AbilityAction { } public boolean isReference() { - return slotType != SlotType.INLINE_ABILITY; + return slotType == SlotType.ABILITY_REFERENCE || slotType == SlotType.CHAIN_REFERENCE; } public boolean isChainReference() { return slotType == SlotType.CHAIN_REFERENCE; } + public boolean isInlineChain() { + return slotType == SlotType.INLINE_CHAIN; + } + + public boolean isChain() { + return slotType == SlotType.CHAIN_REFERENCE || slotType == SlotType.INLINE_CHAIN; + } + public boolean isAbilityReference() { return slotType == SlotType.ABILITY_REFERENCE; } @@ -92,6 +110,9 @@ public class AbilityAction { case INLINE_ABILITY: return inlineAbility; + case INLINE_CHAIN: + return inlineChain; + case ABILITY_REFERENCE: { AbilityController ctrl = AbilityController.Instance; if (ctrl == null) return null; @@ -109,13 +130,13 @@ public class AbilityAction { } case CHAIN_REFERENCE: { - ChainedAbilityController ctrl = ChainedAbilityController.Instance; + AbilityController ctrl = AbilityController.Instance; if (ctrl == null) return null; - int rev = ctrl.getRevision(); + int rev = ctrl.getChainedAbilityRevision(); if (cachedAction != null && cachedRevision == rev) { return cachedAction; } - ChainedAbility resolved = ctrl.resolve(referenceId); + ChainedAbility resolved = ctrl.resolveChainedAbility(referenceId); if (resolved != null && enabledOverride != null) { resolved.setEnabled(enabledOverride); } @@ -154,6 +175,10 @@ public class AbilityAction { if (inlineAbility != null) { inlineAbility.setEnabled(enabled); } + } else if (slotType == SlotType.INLINE_CHAIN) { + if (inlineChain != null) { + inlineChain.setEnabled(enabled); + } } else { this.enabledOverride = enabled; if (cachedAction != null) { @@ -170,22 +195,41 @@ public class AbilityAction { return inlineAbility; } + public ChainedAbility getInlineChain() { + return inlineChain; + } + /** - * Convert this reference slot to an inline slot by copying the resolved ability. - * Only works for ability references (chains cannot be inlined). + * Convert this reference slot to an inline slot by copying the resolved data. + * Works for ability references (→ INLINE_ABILITY) and chain references (→ INLINE_CHAIN). + * Chain entries are kept as-is (not auto-converted to inline). */ public boolean convertToInline() { - if (slotType != SlotType.ABILITY_REFERENCE) return false; + if (slotType == SlotType.ABILITY_REFERENCE) { + Ability resolved = getAbility(); + if (resolved == null) return false; - Ability resolved = getAbility(); - if (resolved == null) return false; + inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); + slotType = SlotType.INLINE_ABILITY; + referenceId = null; + cachedAction = null; + cachedRevision = -1; + return true; + } - inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); - slotType = SlotType.INLINE_ABILITY; - referenceId = null; - cachedAction = null; - cachedRevision = -1; - return true; + if (slotType == SlotType.CHAIN_REFERENCE) { + ChainedAbility resolved = getChainedAbility(); + if (resolved == null) return false; + + inlineChain = resolved.deepCopy(); + slotType = SlotType.INLINE_CHAIN; + referenceId = null; + cachedAction = null; + cachedRevision = -1; + return true; + } + + return false; } // ═══════════════════════════════════════════════════════════════════ @@ -194,6 +238,14 @@ public class AbilityAction { public NBTTagCompound writeNBT() { switch (slotType) { + case INLINE_CHAIN: { + NBTTagCompound nbt = new NBTTagCompound(); + if (inlineChain != null) { + nbt.setTag("InlineChain", inlineChain.writeNBT()); + } + return nbt; + } + case CHAIN_REFERENCE: { NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("ChainReference", referenceId); @@ -221,7 +273,14 @@ public class AbilityAction { public static AbilityAction fromNBT(NBTTagCompound nbt) { if (nbt == null) return null; - // Chain reference (new format) + // Inline chain (new format) + if (nbt.hasKey("InlineChain")) { + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(nbt.getCompoundTag("InlineChain")); + return inlineChain(chain); + } + + // Chain reference if (nbt.hasKey("ChainReference")) { AbilityAction slot = chainReference(nbt.getString("ChainReference")); if (nbt.hasKey("RefEnabled")) { @@ -230,7 +289,7 @@ public class AbilityAction { return slot; } - // Ability reference (existing format) + // Ability reference if (nbt.hasKey("Reference")) { AbilityAction slot = abilityReference(nbt.getString("Reference")); if (nbt.hasKey("RefEnabled")) { @@ -239,7 +298,7 @@ public class AbilityAction { return slot; } - // Inline ability (existing format) + // Inline ability Ability ability = AbilityController.Instance.fromNBT(nbt); if (ability == null) return null; return inline(ability); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java index 212cd7d16..07295aaf7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -1,5 +1,6 @@ package kamkeel.npcs.controllers.data.ability; +import kamkeel.npcs.controllers.AbilityController; import net.minecraft.nbt.NBTTagCompound; /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java index dd45de17d..17b4f89d9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java @@ -1,5 +1,6 @@ package kamkeel.npcs.controllers.data.ability; +import kamkeel.npcs.controllers.AbilityController; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -81,9 +82,11 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { /** * Get the display name for this chained ability. * Returns displayName if set, otherwise falls back to name. + * Converts & color codes to § for rendering. */ public String getDisplayName() { - return (displayName != null && !displayName.isEmpty()) ? displayName : name; + String result = (displayName != null && !displayName.isEmpty()) ? displayName : name; + return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : ""; } public String getRawDisplayName() { @@ -178,7 +181,18 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { @Override public String getEntryReference(int index) { if (index < 0 || index >= entries.size()) return null; - return entries.get(index).getAbilityReference(); + ChainedAbilityEntry entry = entries.get(index); + if (entry.isInline()) { + Ability a = entry.getInlineAbility(); + return a != null ? a.getName() : ""; + } + return entry.getAbilityReference(); + } + + @Override + public boolean isEntryInline(int index) { + if (index < 0 || index >= entries.size()) return false; + return entries.get(index).isInline(); } @Override @@ -273,11 +287,17 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { if (controller != null) { for (int i = 0; i < entries.size(); i++) { ChainedAbilityEntry entry = entries.get(i); - String ref = entry.getAbilityReference(); - if (ref == null || ref.isEmpty()) { - errors.add("Entry " + (i + 1) + ": ability reference is empty"); - } else if (!controller.canResolveAbility(ref)) { - errors.add("Entry " + (i + 1) + ": cannot resolve ability '" + ref + "'"); + if (entry.isInline()) { + if (entry.getInlineAbility() == null) { + errors.add("Entry " + (i + 1) + ": inline ability is null"); + } + } else { + String ref = entry.getAbilityReference(); + if (ref == null || ref.isEmpty()) { + errors.add("Entry " + (i + 1) + ": ability reference is empty"); + } else if (!controller.canResolveAbility(ref)) { + errors.add("Entry " + (i + 1) + ": cannot resolve ability '" + ref + "'"); + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityController.java deleted file mode 100644 index 775ea0d7e..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityController.java +++ /dev/null @@ -1,319 +0,0 @@ -package kamkeel.npcs.controllers.data.ability; - -import kamkeel.npcs.controllers.SyncController; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.util.NBTJsonUtil; - -import java.io.File; -import java.util.*; - -/** - * Global registry and storage for {@link ChainedAbility} definitions. - * Follows the same patterns as {@link AbilityController} for custom abilities: - * JSON file storage in {@code abilities/chained/}, dual-index by name and UUID, - * revision tracking for cache invalidation, and client sync. - */ -public class ChainedAbilityController { - - public static ChainedAbilityController Instance = new ChainedAbilityController(); - - private final Map chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility - private final Map chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility - - /** Incremented on any change; used for cache invalidation. */ - private int revision = 0; - - // ═══════════════════════════════════════════════════════════════════ - // LOAD / SAVE - // ═══════════════════════════════════════════════════════════════════ - - /** - * Load all chained abilities from disk. - */ - public void load() { - chainedAbilities.clear(); - chainedAbilitiesById.clear(); - int migrated = 0; - - File dir = getDir(); - - // Legacy migration: move files from old chained_abilities/ directory - File legacyDir = new File(CustomNpcs.getWorldSaveDirectory(), "chained_abilities"); - if (legacyDir.exists() && legacyDir.isDirectory()) { - File[] legacyFiles = legacyDir.listFiles(); - if (legacyFiles != null) { - for (File legacyFile : legacyFiles) { - if (!legacyFile.isFile() || !legacyFile.getName().endsWith(".json")) continue; - File dest = new File(dir, legacyFile.getName()); - if (!dest.exists()) { - if (legacyFile.renameTo(dest)) { - LogWriter.info("Migrated chained ability file: chained_abilities/" + legacyFile.getName() + " -> abilities/chained/" + legacyFile.getName()); - migrated++; - } - } - } - // Remove legacy directory if empty - if (legacyDir.list() != null && legacyDir.list().length == 0) { - legacyDir.delete(); - } - } - } - - 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 key = filename.substring(0, filename.length() - 5); - - NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); - ChainedAbility chain = new ChainedAbility(); - chain.readNBT(nbt); - - // Ensure chain has a UUID; generate one for legacy entries - String uuid = chain.getId(); - if (uuid == null || uuid.isEmpty() || uuid.equals(key)) { - uuid = UUID.randomUUID().toString(); - chain.setId(uuid); - // Re-save file with the new UUID - try { - NBTJsonUtil.SaveFile(file, chain.writeNBT()); - } catch (Exception ex) { - LogWriter.error("Failed to save UUID for chained ability: " + key, ex); - } - migrated++; - } - - // Use filename as authoritative name key - chain.setName(key); - chainedAbilities.put(key, chain); - chainedAbilitiesById.put(uuid, chain); - } catch (Exception e) { - LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e); - } - } - } - - revision++; - if (migrated > 0) { - LogWriter.info("Migrated " + migrated + " chained abilities (legacy dir or missing UUIDs)"); - } - LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities"); - } - - /** - * Save a chained ability to disk. Handles UUID assignment, renames, and uniqueness. - */ - public boolean save(ChainedAbility chain) { - if (chain == null) return false; - - String name = chain.getName(); - if (name == null || name.isEmpty()) return false; - - String uuid = chain.getId(); - boolean isNew = (uuid == null || uuid.isEmpty()); - - if (isNew) { - // Generate UUID for new chain - uuid = UUID.randomUUID().toString(); - chain.setId(uuid); - - // Ensure unique name - while (hasName(name)) { - name = name + "_"; - } - chain.setName(name); - } else { - // Existing chain — check for rename via UUID lookup - ChainedAbility existing = chainedAbilitiesById.get(uuid); - if (existing != null) { - String oldName = existing.getName(); - if (!oldName.equals(name)) { - // Name changed! Ensure new name is unique (excluding self) - String testName = name; - while (chainedAbilities.containsKey(testName) && !testName.equals(oldName)) { - testName = testName + "_"; - } - if (!testName.equals(name)) { - name = testName; - chain.setName(name); - } - - // Delete old file and remove old map entry - File oldFile = new File(getDir(), oldName + ".json"); - if (oldFile.exists()) { - oldFile.delete(); - } - chainedAbilities.remove(oldName); - } - } - } - - File dir = getDir(); - File fileNew = new File(dir, name + ".json_new"); - File fileCurrent = new File(dir, name + ".json"); - - try { - NBTTagCompound nbt = chain.writeNBT(); - NBTJsonUtil.SaveFile(fileNew, nbt); - - if (fileCurrent.exists()) { - fileCurrent.delete(); - } - fileNew.renameTo(fileCurrent); - if (fileNew.exists()) { - fileNew.delete(); - } - - chainedAbilities.put(name, chain); - chainedAbilitiesById.put(uuid, chain); - revision++; - LogWriter.info("Saved chained ability: " + name + " [" + uuid + "]"); - SyncController.syncAllChainedAbilities(); - return true; - } catch (Exception e) { - LogWriter.error("Error saving chained ability: " + name, e); - return false; - } - } - - /** - * Delete a chained ability by name. - */ - public boolean delete(String name) { - if (name == null || name.isEmpty()) return false; - - ChainedAbility removed = chainedAbilities.remove(name); - if (removed == null) return false; - - // Also remove from UUID index - String uuid = removed.getId(); - if (uuid != null && !uuid.isEmpty()) { - chainedAbilitiesById.remove(uuid); - } - - File dir = getDir(); - File file = new File(dir, name + ".json"); - if (file.exists()) { - file.delete(); - } - - revision++; - LogWriter.info("Deleted chained ability: " + name); - SyncController.syncAllChainedAbilities(); - return true; - } - - // ═══════════════════════════════════════════════════════════════════ - // RESOLUTION - // ═══════════════════════════════════════════════════════════════════ - - /** - * Resolve a chained ability by key, returning a deep copy. - * Checks UUID first, then exact name, then case-insensitive name. - */ - public ChainedAbility resolve(String key) { - if (key == null || key.isEmpty()) return null; - - // UUID lookup (most common for persistent references) - ChainedAbility byId = chainedAbilitiesById.get(key); - if (byId != null) return byId.deepCopy(); - - // Exact name match - ChainedAbility chain = chainedAbilities.get(key); - if (chain != null) return chain.deepCopy(); - - // Case-insensitive name match - for (Map.Entry entry : chainedAbilities.entrySet()) { - if (entry.getKey().equalsIgnoreCase(key)) { - return entry.getValue().deepCopy(); - } - } - - return null; - } - - /** - * Check if a key can be resolved without creating a deep copy. - */ - public boolean canResolve(String key) { - if (key == null || key.isEmpty()) return false; - - if (chainedAbilitiesById.containsKey(key)) return true; - - if (chainedAbilities.containsKey(key)) return true; - - for (String name : chainedAbilities.keySet()) { - if (name.equalsIgnoreCase(key)) return true; - } - - return false; - } - - // ═══════════════════════════════════════════════════════════════════ - // QUERIES - // ═══════════════════════════════════════════════════════════════════ - - public ChainedAbility get(String name) { - return chainedAbilities.get(name); - } - - public ChainedAbility getByUUID(String uuid) { - return chainedAbilitiesById.get(uuid); - } - - public Set getNames() { - return new LinkedHashSet<>(chainedAbilities.keySet()); - } - - public Map getChainedAbilities() { - return chainedAbilities; - } - - public boolean hasName(String name) { - return chainedAbilities.containsKey(name); - } - - public boolean hasKey(String key) { - return chainedAbilities.containsKey(key) || chainedAbilitiesById.containsKey(key); - } - - public int getRevision() { - return revision; - } - - // ═══════════════════════════════════════════════════════════════════ - // CLIENT SYNC - // ═══════════════════════════════════════════════════════════════════ - - /** - * Set chained abilities from sync payload (client-side). - */ - public void setChainedAbilities(Map synced) { - chainedAbilities.clear(); - chainedAbilitiesById.clear(); - chainedAbilities.putAll(synced); - for (ChainedAbility chain : synced.values()) { - String uuid = chain.getId(); - if (uuid != null && !uuid.isEmpty()) { - chainedAbilitiesById.put(uuid, chain); - } - } - revision++; - } - - // ═══════════════════════════════════════════════════════════════════ - // STORAGE - // ═══════════════════════════════════════════════════════════════════ - - private File getDir() { - File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities" + File.separator + "chained"); - if (!dir.exists()) { - dir.mkdirs(); - } - return dir; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java index ace05d69d..887d85571 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java @@ -1,26 +1,74 @@ package kamkeel.npcs.controllers.data.ability; +import kamkeel.npcs.controllers.AbilityController; import net.minecraft.nbt.NBTTagCompound; /** * A single entry in a {@link ChainedAbility}, representing one ability * to execute in sequence along with its delay configuration. + *

+ * Entries can be either a reference to a registered ability (by name/key) + * or an inline ability that is owned directly by this entry. */ public class ChainedAbilityEntry { - /** Reference key (built-in name or custom ability name). */ + public enum EntryType { REFERENCE, INLINE } + + private EntryType entryType = EntryType.REFERENCE; + + /** Reference key (built-in name or custom ability name). Used when entryType == REFERENCE. */ private String abilityReference = ""; + /** Inline ability data. Used when entryType == INLINE. */ + private Ability inlineAbility = null; + /** Delay in ticks BEFORE this ability starts (after previous entry completes). */ private int delayTicks = 0; public ChainedAbilityEntry() {} public ChainedAbilityEntry(String abilityReference, int delayTicks) { + this.entryType = EntryType.REFERENCE; this.abilityReference = abilityReference != null ? abilityReference : ""; this.delayTicks = Math.max(0, delayTicks); } + // ═══════════════════════════════════════════════════════════════════ + // FACTORY METHODS + // ═══════════════════════════════════════════════════════════════════ + + public static ChainedAbilityEntry reference(String ref, int delay) { + ChainedAbilityEntry entry = new ChainedAbilityEntry(); + entry.entryType = EntryType.REFERENCE; + entry.abilityReference = ref != null ? ref : ""; + entry.delayTicks = Math.max(0, delay); + return entry; + } + + public static ChainedAbilityEntry inline(Ability ability, int delay) { + ChainedAbilityEntry entry = new ChainedAbilityEntry(); + entry.entryType = EntryType.INLINE; + entry.inlineAbility = ability; + entry.delayTicks = Math.max(0, delay); + return entry; + } + + // ═══════════════════════════════════════════════════════════════════ + // TYPE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public EntryType getEntryType() { + return entryType; + } + + public boolean isInline() { + return entryType == EntryType.INLINE; + } + + public boolean isReference() { + return entryType == EntryType.REFERENCE; + } + // ═══════════════════════════════════════════════════════════════════ // GETTERS / SETTERS // ═══════════════════════════════════════════════════════════════════ @@ -33,6 +81,14 @@ public class ChainedAbilityEntry { this.abilityReference = abilityReference != null ? abilityReference : ""; } + public Ability getInlineAbility() { + return inlineAbility; + } + + public void setInlineAbility(Ability ability) { + this.inlineAbility = ability; + } + public int getDelayTicks() { return delayTicks; } @@ -42,13 +98,77 @@ public class ChainedAbilityEntry { } // ═══════════════════════════════════════════════════════════════════ - // NBT + // RESOLUTION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Resolve this entry to an Ability instance. + * Inline entries return their ability directly. + * Reference entries resolve via AbilityController. + */ + public Ability resolve() { + if (entryType == EntryType.INLINE) { + return inlineAbility; + } + AbilityController ctrl = AbilityController.Instance; + if (ctrl == null) return null; + return ctrl.resolveAbility(abilityReference); + } + + // ═══════════════════════════════════════════════════════════════════ + // CONVERSION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Convert a reference entry to inline by cloning the resolved ability. + * Returns false if already inline or the reference cannot be resolved. + */ + public boolean convertToInline() { + if (entryType != EntryType.REFERENCE) return false; + + AbilityController ctrl = AbilityController.Instance; + if (ctrl == null) return false; + + Ability resolved = ctrl.resolveAbility(abilityReference); + if (resolved == null) return false; + + inlineAbility = ctrl.fromNBT(resolved.writeNBT()); + entryType = EntryType.INLINE; + abilityReference = ""; + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEEP COPY + // ═══════════════════════════════════════════════════════════════════ + + /** + * Create a deep copy of this entry. Inline abilities are copied via NBT round-trip. + */ + public ChainedAbilityEntry deepCopy() { + ChainedAbilityEntry copy = new ChainedAbilityEntry(); + copy.entryType = this.entryType; + copy.abilityReference = this.abilityReference; + copy.delayTicks = this.delayTicks; + if (this.inlineAbility != null) { + copy.inlineAbility = AbilityController.Instance.fromNBT(this.inlineAbility.writeNBT()); + } + return copy; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT (backwards compatible) // ═══════════════════════════════════════════════════════════════════ public NBTTagCompound writeNBT() { NBTTagCompound nbt = new NBTTagCompound(); - nbt.setString("Reference", abilityReference); nbt.setInteger("Delay", delayTicks); + + if (entryType == EntryType.INLINE && inlineAbility != null) { + nbt.setTag("InlineAbility", inlineAbility.writeNBT()); + } else { + nbt.setString("Reference", abilityReference); + } return nbt; } @@ -56,8 +176,20 @@ public class ChainedAbilityEntry { if (nbt == null) return null; ChainedAbilityEntry entry = new ChainedAbilityEntry(); - entry.abilityReference = nbt.getString("Reference"); entry.delayTicks = Math.max(0, nbt.getInteger("Delay")); + + // Inline entry (new format) + if (nbt.hasKey("InlineAbility")) { + NBTTagCompound abilityNBT = nbt.getCompoundTag("InlineAbility"); + entry.entryType = EntryType.INLINE; + entry.inlineAbility = AbilityController.Instance != null + ? AbilityController.Instance.fromNBT(abilityNBT) : null; + } else { + // Reference entry (old + new format) + entry.entryType = EntryType.REFERENCE; + entry.abilityReference = nbt.getString("Reference"); + } + return entry; } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 8f58fada7..b4e63bace 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -2,7 +2,7 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.data.*; @@ -56,6 +56,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { refs.add(new WeakReference<>(projectile)); } + public static void clearAllProjectiles() { + activeProjectiles.clear(); + } + public static List getActiveProjectiles(int ownerEntityId) { List> refs = activeProjectiles.get(ownerEntityId); if (refs == null) return Collections.emptyList(); @@ -455,8 +459,9 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { Math.pow(target.posZ - posZ, 2) ); - if (dist <= getExplosionRadius()) { - float falloff = 1.0f - (float) (dist / getExplosionRadius()) * getExplosionDamageFalloff(); + float radius = getExplosionRadius(); + if (radius > 0 && dist <= radius) { + float falloff = 1.0f - (float) (dist / radius) * getExplosionDamageFalloff(); applyDamage(target, getDamage() * falloff, getKnockback() * falloff); } } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index dee4a3a94..eca69684d 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -98,6 +98,10 @@ 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.BuiltInAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.ChainedAbilitiesGetPacket; +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.CustomAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; @@ -519,6 +523,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket()); REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket()); REQUEST_PACKET.registerPacket(new BuiltInAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new ChainedAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new ChainedAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new ChainedAbilitySavePacket()); + REQUEST_PACKET.registerPacket(new ChainedAbilityRemovePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 25ba2bba6..e0f3451a1 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -206,6 +206,10 @@ public enum EnumRequestPacket { CustomAbilityRemove, CustomAbilitySave, BuiltInAbilityGet, + ChainedAbilitiesGet, + ChainedAbilityGet, + ChainedAbilitySave, + ChainedAbilityRemove, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java index 6e565f3de..14f22d983 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java new file mode 100644 index 000000000..97ccfa52d --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.network.packets.request.ability; + +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 noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to get list of all chained ability names. + */ +public final class ChainedAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|ChainedAbilitiesGet"; + + public ChainedAbilitiesGetPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.ChainedAbilitiesGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java new file mode 100644 index 000000000..9772bc4ad --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java @@ -0,0 +1,68 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +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.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; + +import java.io.IOException; + +/** + * Request packet to get a specific chained ability by name. + */ +public final class ChainedAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|ChainedAbilityGet"; + + private String name; + + public ChainedAbilityGetPacket() { + } + + public ChainedAbilityGetPacket(String name) { + this.name = name; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ChainedAbilityGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, name); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String chainName = ByteBufUtils.readString(in); + ChainedAbility chain = AbilityController.Instance.getChainedAbility(chainName); + if (chain != null) { + NBTTagCompound compound = chain.writeNBT(); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java new file mode 100644 index 000000000..b373d6331 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java @@ -0,0 +1,69 @@ +package kamkeel.npcs.network.packets.request.ability; + +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.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to remove a chained ability by name. + */ +public final class ChainedAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|ChainedAbilityRemove"; + + private String name; + + public ChainedAbilityRemovePacket() { + } + + public ChainedAbilityRemovePacket(String name) { + this.name = name; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ChainedAbilityRemove; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, name); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String chainName = ByteBufUtils.readString(in); + AbilityController.Instance.deleteChainedAbility(chainName); + NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java new file mode 100644 index 000000000..aaaf8d6a9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java @@ -0,0 +1,79 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +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.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to save a chained ability. + */ +public final class ChainedAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|ChainedAbilitySave"; + + private NBTTagCompound chainNBT; + + public ChainedAbilitySavePacket() { + } + + public ChainedAbilitySavePacket(NBTTagCompound chainNBT) { + this.chainNBT = chainNBT; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ChainedAbilitySave; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, chainNBT); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NBTTagCompound compound = ByteBufUtils.readNBT(in); + if (compound == null) return; + ChainedAbility chain = new ChainedAbility(); + chain.readNBT(compound); + + if (chain.getName() != null && !chain.getName().isEmpty()) { + AbilityController.Instance.saveChainedAbility(chain); + NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, chain.writeNBT()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java index a4757a20c..e53d53d49 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java index 487d5f346..ae65c374f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java @@ -3,7 +3,7 @@ package kamkeel.npcs.network.packets.request.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java index f42ae5ada..b1662e246 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index bd2ef983c..4a0d2bb78 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -1,7 +1,7 @@ package kamkeel.npcs.util; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityVariant; import noppes.npcs.LogWriter; import noppes.npcs.controllers.AnimationController; @@ -68,8 +68,7 @@ public class Register { } public AbilityVariant registerVariant(String typeId, String variantName, String group, Consumer configurator) { - registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator)); - return new AbilityVariant(variantName, group, configurator); + return registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator)); } public AbilityVariant registerVariant(String typeId, String factoryName, Supplier factory) { diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 89c0b4f51..341ed4e08 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -1,7 +1,7 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.ChainedAbility; import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry; @@ -352,6 +352,11 @@ public abstract class AbstractDataAbilities { cooldownEndTime = 0; } + /** Set the cooldown end time directly. */ + public void setCooldownEndTime(long endTime) { + cooldownEndTime = endTime; + } + // ═══════════════════════════════════════════════════════════════════ // POSITION LOCKING // ═══════════════════════════════════════════════════════════════════ @@ -500,7 +505,10 @@ public abstract class AbstractDataAbilities { // Fire execute event (cancelable) if (fireExecuteEvent(currentAbility, target)) { - return; // Cancelled + // Script cancelled — abort cleanly instead of leaving stuck in ACTIVE + currentAbility.interrupt(); + handleAbilityCompletion(target); + return; } // Call onExecute @@ -638,7 +646,10 @@ public abstract class AbstractDataAbilities { // Fire execute event (cancelable) if (fireExecuteEvent(ability, target)) { - return; // Cancelled + // Script cancelled — abort cleanly instead of leaving stuck in ACTIVE + ability.interrupt(); + handleAbilityCompletion(target); + return; } // Call onExecute @@ -741,7 +752,7 @@ public abstract class AbstractDataAbilities { } ChainedAbilityEntry entry = currentChain.getEntries().get(chainEntryIndex); - Ability ability = AbilityController.Instance.resolveAbility(entry.getAbilityReference()); + Ability ability = entry.resolve(); if (ability == null) { // Broken reference - complete chain completeChain(); diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index e1dcf489b..b063c790d 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -25,8 +25,7 @@ import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.TelegraphController; -import kamkeel.npcs.controllers.data.ability.AbilityController; -import kamkeel.npcs.controllers.data.ability.ChainedAbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; import kamkeel.npcs.network.PacketHandler; @@ -356,7 +355,6 @@ public class CustomNpcs { new SpawnController(); new LinkedNpcController(); AbilityController.Instance.load(); - ChainedAbilityController.Instance.load(); TelegraphController.init(); LinkedItemController.getInstance().load(); @@ -423,6 +421,8 @@ public class CustomNpcs { AuctionController.Instance.save(); } + kamkeel.npcs.entity.EntityEnergyProjectile.clearAllProjectiles(); + if (FMLCommonHandler.instance().getSide().isClient()) clientJaninoCompiler = null; globalJaninoCompiler = null; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index af623b741..29e4822b1 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -1,11 +1,10 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.AbilityAction; import kamkeel.npcs.controllers.data.ability.ChainedAbility; -import kamkeel.npcs.controllers.data.ability.ChainedAbilityController; import kamkeel.npcs.controllers.data.ability.IAbilityAction; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index bbb19c97d..521c0cb02 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -1,9 +1,9 @@ package noppes.npcs; import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.packets.data.ParticlePacket; import kamkeel.npcs.network.packets.data.PlayerDataInfoPacket; @@ -807,7 +807,7 @@ public class NoppesUtilServer { public static void sendCustomAbilitiesData(EntityPlayerMP player) { HashMap map = new HashMap(); - kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance; + AbilityController controller = AbilityController.Instance; if (controller != null) { int index = 0; for (String name : controller.getCustomAbilityNames()) { @@ -819,6 +819,15 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES); } + public static void sendChainedAbilitiesData(EntityPlayerMP player) { + HashMap map = new HashMap(); + int index = 0; + for (String name : AbilityController.Instance.getChainedAbilityNamesSet()) { + map.put(name, index++); + } + sendScrollData(player, map, EnumScrollData.CHAINED_ABILITIES); + } + public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException { return setNpcDialog(slot, dialogId, getEditingNpc(player)); } diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index c0405c193..a0c2ad0cc 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -9,7 +9,7 @@ import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.util.AttributeAttackUtil; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index d993762ee..05af02800 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -6,7 +6,7 @@ import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.client.renderer.lightning.LightningBolt; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 4afa9dec4..8e0db5e71 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -1,9 +1,11 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityAction; import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; @@ -21,6 +23,7 @@ import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback; import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IScrollData; @@ -36,35 +39,76 @@ import java.util.*; * GUI for managing NPC abilities. * Shows list of assigned abilities with add/remove/edit controls. */ -public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, ICustomScrollListener, IGuiData, ITextfieldListener, ISubGuiListener, IAbilityConfigCallback { +public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, ICustomScrollListener, IGuiData, ITextfieldListener, ISubGuiListener, IAbilityConfigCallback, IChainedAbilityConfigCallback { + // ── Button IDs ──────────────────────────────────────────────────────────── + private static final int BTN_SCROLL_TYPE = 50; + private static final int BTN_ADD_ABILITY = 70; + private static final int BTN_REMOVE = 71; + private static final int BTN_EDIT = 72; + private static final int BTN_MOVE_UP = 73; + private static final int BTN_MOVE_DOWN = 74; + private static final int BTN_LOAD = 75; + private static final int BTN_SAVE_PRESET = 76; + private static final int BTN_TOGGLE_SLOT = 77; + private static final int BTN_ADD_CHAIN = 78; + private static final int BTN_ENABLED = 100; + + // ── Scroll IDs ──────────────────────────────────────────────────────────── + private static final int SCROLL_TYPES = 0; + private static final int SCROLL_NPC = 1; + + // ── TextField / Label IDs ───────────────────────────────────────────────── + private static final int TF_MIN_COOLDOWN = 101; + private static final int TF_MAX_COOLDOWN = 102; + private static final int TF_SEARCH = 4; + private static final int LBL_TYPES = 1; + private static final int LBL_NPC = 2; + private static final int LBL_MIN_COOLDOWN = 101; + private static final int LBL_MAX_COOLDOWN = 102; + + // ── Scroll lists ────────────────────────────────────────────────────────── private GuiCustomScroll availableTypesScroll; private GuiCustomScroll npcAbilitiesScroll; - // All available ability types (typeId -> index) + // ── Available type data ─────────────────────────────────────────────────── private final HashMap allAbilityTypes = new HashMap<>(); private final HashMap filteredAbilityTypes = new HashMap<>(); - - // Display name to typeId mapping for the available types scroll private final HashMap displayNameToTypeId = new HashMap<>(); - // NPC's current action slots + // ── NPC slot data ───────────────────────────────────────────────────────── private final List npcSlots = new ArrayList<>(); + private final List rowMapping = new ArrayList<>(); // [slotIdx, entryIdx], entryIdx=-1 = header/standalone - // Settings + // ── NPC settings ────────────────────────────────────────────────────────── private boolean abilitiesEnabled = false; private int minCooldown = 20; private int maxCooldown = 60; + // ── Selection state ─────────────────────────────────────────────────────── private String search = ""; private int selectedAbilityIndex = -1; + private int selectedSlotIndex = -1; + private int selectedEntryIndex = -1; - // Existing preset names for duplicate checking + // ── Pending: chain editing ──────────────────────────────────────────────── + private ChainedAbility pendingChain = null; + + // ── Pending: inline chain-entry editing ────────────────────────────────── + private boolean editingChainEntry = false; + private int editChainSlotIdx = -1; + private int editChainEntryIdx = -1; + private boolean editingChainEntryParent = false; + + // ── Pending: save-as-preset ─────────────────────────────────────────────── + private int pendingSaveSlotIdx = -1; + private int pendingSaveEntryIdx = -1; // -1 for standalone ability private final Set existingPresetNames = new HashSet<>(); - // Pending variant selection + // ── Pending: variant selection ──────────────────────────────────────────── private String pendingTypeId = null; + // ── Static view state (persists across GUI opens) ───────────────────────── public static int modIndex = 0; public static ScrollType scrollType = ScrollType.CNPC; @@ -103,17 +147,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC int y = guiTop + 10; - addButton(new GuiNpcButton(50, guiLeft + 5, y, 140, 20, scrollType.toString())); + addButton(new GuiNpcButton(BTN_SCROLL_TYPE, guiLeft + 5, y, 140, 20, scrollType.toString())); // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) - addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 210, y + 5)); - GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown); + addLabel(new GuiNpcLabel(LBL_MIN_COOLDOWN, "ability.minCooldown", guiLeft + 210, y + 5)); + GuiNpcTextField minField = new GuiNpcTextField(TF_MIN_COOLDOWN, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown); minField.setIntegersOnly(); minField.setMinMaxDefault(0, 10000, 20); addTextField(minField); - addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 320, y + 5)); - GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown); + addLabel(new GuiNpcLabel(LBL_MAX_COOLDOWN, "ability.maxCooldown", guiLeft + 320, y + 5)); + GuiNpcTextField maxField = new GuiNpcTextField(TF_MAX_COOLDOWN, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown); maxField.setIntegersOnly(); maxField.setMinMaxDefault(0, 10000, 60); addTextField(maxField); @@ -121,13 +165,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC y += 28; // Enabled checkbox - addButton(new GuiNpcButton(100, guiLeft + 334, y + 145, 76, 20, new String[]{"gui.disabled", "gui.enabled"}, abilitiesEnabled ? 1 : 0)); - getButton(100).packedFGColour = abilitiesEnabled ? 0x00FF00 : 0xFF0000; + addButton(new GuiNpcButton(BTN_ENABLED, guiLeft + 334, y + 145, 76, 20, new String[]{"gui.disabled", "gui.enabled"}, abilitiesEnabled ? 1 : 0)); + getButton(BTN_ENABLED).packedFGColour = abilitiesEnabled ? 0x00FF00 : 0xFF0000; // Left scroll: available ability types - addLabel(new GuiNpcLabel(1, "ability.availableTypes", guiLeft + 5, y)); + addLabel(new GuiNpcLabel(LBL_TYPES, "ability.availableTypes", guiLeft + 5, y)); if (availableTypesScroll == null) { - availableTypesScroll = new GuiCustomScroll(this, 0); + availableTypesScroll = new GuiCustomScroll(this, SCROLL_TYPES); availableTypesScroll.setSize(140, 130); } availableTypesScroll.guiLeft = guiLeft + 5; @@ -136,12 +180,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addScroll(availableTypesScroll); // Search bar for types - addTextField(new GuiNpcTextField(4, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search)); + addTextField(new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search)); // Right scroll: NPC's abilities (widened to use available space) - addLabel(new GuiNpcLabel(2, "ability.npcAbilities", guiLeft + 210, y)); + addLabel(new GuiNpcLabel(LBL_NPC, "ability.npcAbilities", guiLeft + 210, y)); if (npcAbilitiesScroll == null) { - npcAbilitiesScroll = new GuiCustomScroll(this, 1); + npcAbilitiesScroll = new GuiCustomScroll(this, SCROLL_NPC); npcAbilitiesScroll.setSize(200, 130); } else { npcAbilitiesScroll.setSize(200, 130); @@ -153,43 +197,70 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Center buttons: Add/Remove int centerX = guiLeft + 158; - addButton(new GuiNpcButton(70, centerX, y + 15, 40, 20, ">>>")); // Add - addButton(new GuiNpcButton(71, centerX, y + 37, 40, 20, "<<<")); // Remove - getButton(71).setEnabled(selectedAbilityIndex >= 0); + addButton(new GuiNpcButton(BTN_ADD_ABILITY, centerX, y + 15, 40, 20, ">>>")); // Add + addButton(new GuiNpcButton(BTN_REMOVE, centerX, y + 37, 40, 20, "<<<")); // Remove + getButton(BTN_REMOVE).setEnabled(selectedAbilityIndex >= 0); // Load button (under the add/remove arrows) - addButton(new GuiNpcButton(75, centerX, y + 60, 40, 20, "gui.load")); + addButton(new GuiNpcButton(BTN_LOAD, centerX, y + 60, 40, 20, "gui.load")); - // On/Off toggle for selected ability (under Load button) - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility(); - if (selectedAbility != null) { - GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20, - new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0); - addButton(toggleBtn); + // On/Off toggle for selected slot (under Load button) — chains and abilities + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() && selectedEntryIndex == -1) { + AbilityAction selectedSlot = npcSlots.get(selectedSlotIndex); + boolean isEnabled = false; + if (selectedSlot.isChain()) { + ChainedAbility chain = selectedSlot.isInlineChain() ? selectedSlot.getInlineChain() : selectedSlot.getChainedAbility(); + isEnabled = chain != null && chain.isEnabled(); + } else { + Ability ability = selectedSlot.getAbility(); + isEnabled = ability != null && ability.isEnabled(); + } + GuiNpcButton toggleBtn = new GuiNpcButton(BTN_TOGGLE_SLOT, centerX, y + 112, 40, 20, + new String[]{"gui.off", "gui.on"}, isEnabled ? 1 : 0); + addButton(toggleBtn); + } + + // Check if selected slot is editable + boolean selectedIsBuiltIn = false; + boolean selectedIsChainEntry = selectedEntryIndex >= 0; + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() && !selectedIsChainEntry) { + AbilityAction sel = npcSlots.get(selectedSlotIndex); + if (!sel.isChain()) { + Ability a = sel.getAbility(); + if (a != null && a.isBuiltIn()) selectedIsBuiltIn = true; } } - // Check if selected ability is built-in (non-editable) - boolean selectedIsBuiltIn = false; - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - Ability sel = npcSlots.get(selectedAbilityIndex).getAbility(); - if (sel != null && sel.isBuiltIn()) selectedIsBuiltIn = true; - } - // Right side buttons: Edit and Up/Down carrots with Save button - addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit")); - getButton(72).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn); + addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 210, y + 145, 55, 20, "gui.edit")); + getButton(BTN_EDIT).setEnabled(selectedSlotIndex >= 0 && !selectedIsBuiltIn); // Up/Down carrot buttons - addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<")); - addButton(new GuiNpcButton(74, guiLeft + 292, y + 145, 20, 20, ">")); - getButton(73).setEnabled(selectedAbilityIndex > 0); - getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1); + addButton(new GuiNpcButton(BTN_MOVE_UP, guiLeft + 270, y + 145, 20, 20, "<")); + addButton(new GuiNpcButton(BTN_MOVE_DOWN, guiLeft + 292, y + 145, 20, 20, ">")); + getButton(BTN_MOVE_UP).setEnabled(selectedSlotIndex > 0 && selectedEntryIndex == -1); + getButton(BTN_MOVE_DOWN).setEnabled(selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() - 1 && selectedEntryIndex == -1); - // Save button (to the right of carrots, only enabled when ability is selected) - addButton(new GuiNpcButton(76, centerX, y + 82, 40, 20, "gui.save")); - getButton(76).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn); + // Save button — enabled for inline abilities (standalone or chain entry), disabled for references/chains + addButton(new GuiNpcButton(BTN_SAVE_PRESET, centerX, y + 82, 40, 20, "gui.save")); + boolean canSave = false; + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() && !selectedIsBuiltIn) { + AbilityAction saveSlot = npcSlots.get(selectedSlotIndex); + if (selectedEntryIndex >= 0) { + // Chain entry: only inline entries can be saved + ChainedAbility saveChain = saveSlot.isInlineChain() ? saveSlot.getInlineChain() : null; + if (saveChain != null && selectedEntryIndex < saveChain.getEntries().size()) { + canSave = saveChain.getEntries().get(selectedEntryIndex).isInline(); + } + } else if (!saveSlot.isChain() && !saveSlot.isReference()) { + // Standalone inline ability + canSave = true; + } + } + getButton(BTN_SAVE_PRESET).setEnabled(canSave); + + // Add Chain button + addButton(new GuiNpcButton(BTN_ADD_CHAIN, centerX, y + 135, 40, 20, "ability.addChain")); } @Override @@ -197,7 +268,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC int id = guibutton.id; // Enabled toggle - if (id == 100) { + if (id == BTN_ENABLED) { abilitiesEnabled = ((GuiNpcButton) guibutton).getValue() == 1; initGui(); save(); @@ -205,7 +276,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } // Add ability - if (id == 70) { + if (id == BTN_ADD_ABILITY) { if (availableTypesScroll.hasSelected()) { String displayName = availableTypesScroll.getSelected(); String typeId = displayNameToTypeId.get(displayName); @@ -234,11 +305,23 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Remove ability - if (id == 71) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - npcSlots.remove(selectedAbilityIndex); + // Remove slot or chain entry + if (id == BTN_REMOVE) { + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) { + if (selectedEntryIndex >= 0) { + // Remove entry from chain — not the whole slot + AbilityAction slot = npcSlots.get(selectedSlotIndex); + ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : null; + if (chain != null && selectedEntryIndex < chain.getEntries().size()) { + chain.removeEntry(selectedEntryIndex); + } + } else { + npcSlots.remove(selectedSlotIndex); + } + selectedSlotIndex = -1; + selectedEntryIndex = -1; selectedAbilityIndex = -1; + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); updateNpcAbilitiesList(); initGui(); save(); @@ -246,12 +329,45 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Edit ability - if (id == 72) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - AbilityAction slot = npcSlots.get(selectedAbilityIndex); - if (slot.isReference()) { - // Reference slot - ask clone-and-modify vs modify-parent + // Edit ability or chain + if (id == BTN_EDIT) { + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) { + AbilityAction slot = npcSlots.get(selectedSlotIndex); + + if (slot.isChain() && selectedEntryIndex == -1) { + // Chain header selected + if (slot.isChainReference()) { + // Reference chain — offer Clone & Modify / Edit Parent + setSubGui(new SubGuiAbilityEditMode()); + } else { + // Inline chain — open chain config directly + ChainedAbility chain = slot.getInlineChain(); + if (chain != null) { + pendingChain = chain; + setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots)); + } + } + } else if (slot.isChain() && selectedEntryIndex >= 0) { + // Chain entry selected + ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : slot.getChainedAbility(); + if (chain != null && selectedEntryIndex < chain.getEntries().size()) { + ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex); + if (entry.isInline() && slot.isInlineChain()) { + // Inline entry in inline chain — edit directly + Ability a = entry.getInlineAbility(); + if (a != null) { + editingChainEntry = true; + editChainSlotIdx = selectedSlotIndex; + editChainEntryIdx = selectedEntryIndex; + a.setNpcInlineEdit(true); + setSubGui(a.createConfigGui(this)); + } + } else { + // Reference entry or entry in reference chain — Clone & Modify / Edit Parent + setSubGui(new SubGuiAbilityEditMode()); + } + } + } else if (slot.isReference()) { setSubGui(new SubGuiAbilityEditMode()); } else { Ability ability = slot.getAbility(); @@ -265,14 +381,14 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } // Move up (carrot left = earlier in list) - if (id == 73) { - if (selectedAbilityIndex > 0) { - AbilityAction temp = npcSlots.get(selectedAbilityIndex); - npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1)); - npcSlots.set(selectedAbilityIndex - 1, temp); - selectedAbilityIndex--; + if (id == BTN_MOVE_UP) { + if (selectedSlotIndex > 0 && selectedEntryIndex == -1) { + AbilityAction temp = npcSlots.get(selectedSlotIndex); + npcSlots.set(selectedSlotIndex, npcSlots.get(selectedSlotIndex - 1)); + npcSlots.set(selectedSlotIndex - 1, temp); + selectedSlotIndex--; + selectedAbilityIndex = selectedSlotIndex; updateNpcAbilitiesList(); - selectAbilityByIndex(selectedAbilityIndex); initGui(); save(); } @@ -280,30 +396,53 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } // Move down (carrot right = later in list) - if (id == 74) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) { - AbilityAction temp = npcSlots.get(selectedAbilityIndex); - npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1)); - npcSlots.set(selectedAbilityIndex + 1, temp); - selectedAbilityIndex++; + if (id == BTN_MOVE_DOWN) { + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() - 1 && selectedEntryIndex == -1) { + AbilityAction temp = npcSlots.get(selectedSlotIndex); + npcSlots.set(selectedSlotIndex, npcSlots.get(selectedSlotIndex + 1)); + npcSlots.set(selectedSlotIndex + 1, temp); + selectedSlotIndex++; + selectedAbilityIndex = selectedSlotIndex; updateNpcAbilitiesList(); - selectAbilityByIndex(selectedAbilityIndex); initGui(); save(); } return; } - // Load ability (opens load dialog to add ability directly to NPC) - if (id == 75) { - setSubGui(new SubGuiAbilityLoad(this)); + // Load ability or chain (opens type choice first) + if (id == BTN_LOAD) { + setSubGui(new SubGuiLoadTypeChoice()); return; } - // Save selected ability as custom preset - if (id == 76) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - Ability abilityToSave = npcSlots.get(selectedAbilityIndex).getAbility(); + // Save selected ability as custom preset (converts inline → reference) + if (id == BTN_SAVE_PRESET) { + Ability abilityToSave = null; + pendingSaveSlotIdx = -1; + pendingSaveEntryIdx = -1; + + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) { + AbilityAction saveSlot = npcSlots.get(selectedSlotIndex); + + if (selectedEntryIndex >= 0) { + // Saving a chain entry's inline ability + ChainedAbility chain = saveSlot.isInlineChain() ? saveSlot.getInlineChain() : null; + if (chain != null && selectedEntryIndex < chain.getEntries().size()) { + ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex); + if (entry.isInline()) { + abilityToSave = entry.getInlineAbility(); + pendingSaveSlotIdx = selectedSlotIndex; + pendingSaveEntryIdx = selectedEntryIndex; + } + } + } else if (!saveSlot.isChain() && !saveSlot.isReference()) { + // Saving a standalone inline ability + abilityToSave = saveSlot.getAbility(); + pendingSaveSlotIdx = selectedSlotIndex; + pendingSaveEntryIdx = -1; + } + if (abilityToSave != null) { if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { abilityToSave.setNpcInlineEdit(true); @@ -316,24 +455,30 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Toggle selected ability on/off - if (id == 77) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - AbilityAction slot = npcSlots.get(selectedAbilityIndex); - Ability ability = slot.getAbility(); - if (ability != null) { - boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1; - slot.setEnabled(enabled); - updateNpcAbilitiesList(); - selectAbilityByIndex(selectedAbilityIndex); - initGui(); - save(); - } + // Toggle selected slot on/off + if (id == BTN_TOGGLE_SLOT) { + if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) { + AbilityAction slot = npcSlots.get(selectedSlotIndex); + boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1; + slot.setEnabled(enabled); + updateNpcAbilitiesList(); + initGui(); + save(); } return; } - if (id == 50) { + // Add Chain button + if (id == BTN_ADD_CHAIN) { + ChainedAbility newChain = new ChainedAbility(); + newChain.setId(UUID.randomUUID().toString()); + newChain.setName("NewChain"); + pendingChain = newChain; + setSubGui(new SubGuiChainedAbilityConfig(newChain, this, true, npcSlots)); + return; + } + + if (id == BTN_SCROLL_TYPE) { if (scrollType != ScrollType.MODDED) { ScrollType[] values = ScrollType.values(); ScrollType next = values[(scrollType.ordinal() + 1) % values.length]; @@ -438,10 +583,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void keyTyped(char c, int i) { super.keyTyped(c, i); - if (getTextField(4) != null && getTextField(4).isFocused()) { - if (search.equals(getTextField(4).getText())) + if (getTextField(TF_SEARCH) != null && getTextField(TF_SEARCH).isFocused()) { + if (search.equals(getTextField(TF_SEARCH).getText())) return; - search = getTextField(4).getText().toLowerCase(); + search = getTextField(TF_SEARCH).getText().toLowerCase(); availableTypesScroll.setList(getFilteredTypeList()); availableTypesScroll.resetScroll(); } @@ -492,31 +637,34 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC private void updateNpcAbilitiesList() { List list = new ArrayList<>(); + rowMapping.clear(); + for (int i = 0; i < npcSlots.size(); i++) { - list.add(getAbilityListEntry(i)); - } + AbilityAction slot = npcSlots.get(i); - // Sort: enabled first, then numerically by slot index - Collections.sort(list, new Comparator() { - @Override - public int compare(String a, String b) { - boolean aEnabled = a.startsWith("\u00A7a"); - boolean bEnabled = b.startsWith("\u00A7a"); - if (aEnabled != bEnabled) return aEnabled ? -1 : 1; - return extractIndex(a) - extractIndex(b); - } + if (slot.isChain()) { + // Chain header row + list.add(getChainHeaderEntry(i, slot)); + rowMapping.add(new int[]{i, -1}); - private int extractIndex(String entry) { - String stripped = entry.replaceAll("\u00A7.", "").trim(); - int dotIndex = stripped.indexOf("."); - if (dotIndex > 0) { - try { - return Integer.parseInt(stripped.substring(0, dotIndex).replaceAll("[^0-9]", "")); - } catch (NumberFormatException e) { } + // Chain entry rows + ChainedAbility chain = slot.getChainedAbility(); + if (chain == null && slot.isInlineChain()) { + chain = slot.getInlineChain(); } - return Integer.MAX_VALUE; + if (chain != null) { + for (int e = 0; e < chain.getEntries().size(); e++) { + ChainedAbilityEntry entry = chain.getEntries().get(e); + list.add(getChainEntryDisplay(entry, e)); + rowMapping.add(new int[]{i, e}); + } + } + } else { + // Regular ability row + list.add(getAbilityListEntry(i)); + rowMapping.add(new int[]{i, -1}); } - }); + } if (npcAbilitiesScroll != null) { npcAbilitiesScroll.setUnsortedList(list); @@ -534,7 +682,6 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC Ability ability = slot.getAbility(); if (ability == null) { - // Broken reference String refId = slot.getReferenceId(); String shortId = refId != null && refId.length() > 8 ? refId.substring(0, 8) + "..." : refId; return "\u00A7c\u25CF\u00A7r " + (index + 1) + ". \u00A7c> [Missing: " + shortId + "]\u00A7r"; @@ -560,6 +707,33 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return colorPrefix + (index + 1) + ". " + nameDisplay; } + private String getChainHeaderEntry(int index, AbilityAction slot) { + ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : slot.getChainedAbility(); + String chainName = chain != null ? chain.getDisplayName() : "???"; + boolean enabled = chain != null && chain.isEnabled(); + String colorPrefix = enabled ? "\u00A7a\u25CF\u00A7r " : "\u00A7c\u25CF\u00A7r "; + if (slot.isChainReference()) { + return colorPrefix + (index + 1) + ". \u00A7e> [Chain] " + chainName + "\u00A7r"; + } + return colorPrefix + (index + 1) + ". \u00A7d[Chain] \u00A7r" + chainName; + } + + private String getChainEntryDisplay(ChainedAbilityEntry entry, int entryIndex) { + if (entry.isInline()) { + Ability a = entry.getInlineAbility(); + String name = a != null ? a.getDisplayName() : "???"; + return " \u00A78--- \u00A7r" + name; + } else { + String name = entry.getAbilityReference(); + if (name == null || name.isEmpty()) name = "???"; + Ability resolved = entry.resolve(); + if (resolved != null) { + name = resolved.getDisplayName(); + } + return " \u00A78--- \u00A7e> " + name + "\u00A7r"; + } + } + private void selectAbilityByIndex(int index) { if (index >= 0 && index < npcSlots.size()) { String entry = getAbilityListEntry(index); @@ -601,6 +775,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC loadedAbility.setId(UUID.randomUUID().toString()); npcSlots.add(AbilityAction.inline(loadedAbility)); selectedAbilityIndex = npcSlots.size() - 1; + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -616,6 +791,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC if (referenceId != null && !referenceId.isEmpty()) { npcSlots.add(AbilityAction.abilityReference(referenceId)); selectedAbilityIndex = npcSlots.size() - 1; + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -633,21 +809,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { if (guiCustomScroll == npcAbilitiesScroll) { - String selected = npcAbilitiesScroll.getSelected(); - if (selected != null) { - // Parse index from "[●] 1. Name" format - strip color codes first - String stripped = selected.replaceAll("\u00A7.", "").trim(); - // Now format is "● 1. Name" - find the number after the circle - int dotIndex = stripped.indexOf("."); - if (dotIndex > 0) { - try { - // Extract just the number portion (skip "● " prefix) - String numPart = stripped.substring(0, dotIndex).replaceAll("[^0-9]", ""); - selectedAbilityIndex = Integer.parseInt(numPart) - 1; - } catch (NumberFormatException e) { - selectedAbilityIndex = -1; - } - } + int scrollIndex = npcAbilitiesScroll.selected; + if (scrollIndex >= 0 && scrollIndex < rowMapping.size()) { + int[] mapping = rowMapping.get(scrollIndex); + selectedSlotIndex = mapping[0]; + selectedEntryIndex = mapping[1]; + selectedAbilityIndex = selectedSlotIndex; + } else { + selectedSlotIndex = -1; + selectedEntryIndex = -1; + selectedAbilityIndex = -1; } initGui(); } @@ -655,24 +826,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - AbilityAction slot = npcSlots.get(selectedAbilityIndex); - Ability ability = slot.getAbility(); - if (ability != null && ability.isBuiltIn()) return; - if (slot.isReference()) { - setSubGui(new SubGuiAbilityEditMode()); - } else { - if (ability != null) { - ability.setNpcInlineEdit(true); - setSubGui(ability.createConfigGui(this)); - } - } + if (scroll == npcAbilitiesScroll && selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) { + // Double-click mirrors Edit button logic + buttonEvent(new GuiNpcButton(BTN_EDIT, 0, 0, 0, 0, "")); } } @Override public void unFocused(GuiNpcTextField textField) { - if (textField.id == 101) { + if (textField.id == TF_MIN_COOLDOWN) { minCooldown = textField.getInteger(); // Ensure min <= max if (minCooldown > maxCooldown) { @@ -680,7 +842,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC initGui(); } save(); - } else if (textField.id == 102) { + } else if (textField.id == TF_MAX_COOLDOWN) { maxCooldown = textField.getInteger(); // Ensure min <= max if (maxCooldown < minCooldown) { @@ -693,70 +855,263 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityVariantSelect) { - SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui; - int idx = variantGui.getSelectedIndex(); - if (idx >= 0 && pendingTypeId != null) { - Ability newAbility = AbilityController.Instance.create(pendingTypeId); - if (newAbility != null) { - variantGui.getVariants().get(idx).apply(newAbility); - newAbility.setId(UUID.randomUUID().toString()); - npcSlots.add(AbilityAction.inline(newAbility)); - selectedAbilityIndex = npcSlots.size() - 1; + if (subgui instanceof SubGuiAbilityVariantSelect) { handleVariantSelectClosed((SubGuiAbilityVariantSelect) subgui); initGui(); return; } + if (subgui instanceof SubGuiChainedAbilityConfig) { handleChainConfigClosed(); initGui(); return; } + if (subgui instanceof SubGuiAbilityEditMode) { handleEditModeClosed((SubGuiAbilityEditMode) subgui); return; } + if (subgui instanceof SubGuiAbilitySaveConfirm) { handleSaveConfirmClosed((SubGuiAbilitySaveConfirm) subgui); initGui(); return; } + if (subgui instanceof SubGuiLoadTypeChoice) { handleLoadTypeChoiceClosed((SubGuiLoadTypeChoice) subgui); return; } + if (subgui instanceof SubGuiChainSelect) { handleChainSelectClosed((SubGuiChainSelect) subgui); return; } + if (subgui instanceof SubGuiAbilityConfig) { handleAbilityConfigClosed(); return; } + } + + private void handleVariantSelectClosed(SubGuiAbilityVariantSelect gui) { + 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()); + npcSlots.add(AbilityAction.inline(newAbility)); + selectedAbilityIndex = npcSlots.size() - 1; + selectedSlotIndex = selectedAbilityIndex; + selectedEntryIndex = -1; + updateNpcAbilitiesList(); + save(); + } + } + pendingTypeId = null; + } + + private void handleChainConfigClosed() { + if (pendingChain != null) { + boolean found = false; + for (int i = 0; i < npcSlots.size(); i++) { + AbilityAction slot = npcSlots.get(i); + if (slot.isInlineChain() && slot.getInlineChain() == pendingChain) { + found = true; + break; + } + } + if (!found) { + npcSlots.add(AbilityAction.inlineChain(pendingChain)); + selectedSlotIndex = npcSlots.size() - 1; + selectedEntryIndex = -1; + selectedAbilityIndex = selectedSlotIndex; + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); + } + pendingChain = null; + } + updateNpcAbilitiesList(); + save(); + } + + private void handleEditModeClosed(SubGuiAbilityEditMode gui) { + int mode = gui.getResult(); + if (mode < 0) return; // cancelled + + if (selectedSlotIndex < 0 || selectedSlotIndex >= npcSlots.size()) return; + AbilityAction slot = npcSlots.get(selectedSlotIndex); + + if (slot.isChain() && selectedEntryIndex == -1) { + // Chain header (must be CHAIN_REFERENCE) + if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + if (slot.convertToInline()) { + ChainedAbility chain = slot.getInlineChain(); + if (chain != null) { + pendingChain = chain; + setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots)); + } updateNpcAbilitiesList(); - selectAbilityByIndex(selectedAbilityIndex); save(); } + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + ChainedAbility chain = slot.getChainedAbility(); + if (chain != null) { + pendingChain = chain; + setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots)); + } } - pendingTypeId = null; - initGui(); - } else if (subgui instanceof SubGuiAbilityEditMode) { - int mode = ((SubGuiAbilityEditMode) subgui).getResult(); - if (mode < 0) return; // cancelled - - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - AbilityAction slot = npcSlots.get(selectedAbilityIndex); - if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { - // Convert reference to inline, then edit - if (slot.convertToInline()) { - Ability ability = slot.getAbility(); - if (ability != null) { - ability.setId(UUID.randomUUID().toString()); - ability.setNpcInlineEdit(true); - setSubGui(ability.createConfigGui(this)); + } else if (slot.isChain() && selectedEntryIndex >= 0) { + // Chain entry reference + if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + if (slot.isChainReference()) { + slot.convertToInline(); + } + ChainedAbility chain = slot.getInlineChain(); + if (chain != null && selectedEntryIndex < chain.getEntries().size()) { + ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex); + if (entry.convertToInline()) { + Ability a = entry.getInlineAbility(); + if (a != null) { + editingChainEntry = true; + editChainSlotIdx = selectedSlotIndex; + editChainEntryIdx = selectedEntryIndex; + a.setNpcInlineEdit(true); + setSubGui(a.createConfigGui(this)); } - updateNpcAbilitiesList(); - save(); } - } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { - // Edit the resolved ability directly (changes propagate to controller) - Ability ability = slot.getAbility(); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + } + updateNpcAbilitiesList(); + save(); + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : slot.getChainedAbility(); + if (chain != null && selectedEntryIndex < chain.getEntries().size()) { + ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex); + Ability resolved = entry.resolve(); + if (resolved != null) { + editingChainEntryParent = true; + setSubGui(resolved.createConfigGui(this)); } } } - } else if (subgui instanceof SubGuiAbilityConfig) { + } else if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + // Standalone ability reference → convert to inline + if (slot.convertToInline()) { + Ability ability = slot.getAbility(); + if (ability != null) { + ability.setId(UUID.randomUUID().toString()); + ability.setNpcInlineEdit(true); + setSubGui(ability.createConfigGui(this)); + } + updateNpcAbilitiesList(); + save(); + } + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } + } + + private void handleSaveConfirmClosed(SubGuiAbilitySaveConfirm gui) { + if (gui.wasSaved() && pendingSaveSlotIdx >= 0 && pendingSaveSlotIdx < npcSlots.size()) { + if (pendingSaveEntryIdx >= 0) { + // Chain entry: convert inline entry to reference + AbilityAction slot = npcSlots.get(pendingSaveSlotIdx); + ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : null; + if (chain != null && pendingSaveEntryIdx < chain.getEntries().size()) { + ChainedAbilityEntry entry = chain.getEntries().get(pendingSaveEntryIdx); + Ability a = entry.getInlineAbility(); + if (a != null && a.getName() != null && !a.getName().isEmpty()) { + chain.getEntries().set(pendingSaveEntryIdx, + ChainedAbilityEntry.reference(a.getName(), entry.getDelayTicks())); + } + } + } else { + // Standalone: convert inline slot to reference + AbilityAction slot = npcSlots.get(pendingSaveSlotIdx); + Ability a = slot.getAbility(); + if (a != null && a.getName() != null && !a.getName().isEmpty()) { + npcSlots.set(pendingSaveSlotIdx, AbilityAction.abilityReference(a.getName())); + } + } + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); updateNpcAbilitiesList(); save(); } + pendingSaveSlotIdx = -1; + pendingSaveEntryIdx = -1; + } + + private void handleLoadTypeChoiceClosed(SubGuiLoadTypeChoice gui) { + int result = gui.getResult(); + if (result == SubGuiLoadTypeChoice.RESULT_ABILITY) { + setSubGui(new SubGuiAbilityLoad(this)); + } else if (result == SubGuiLoadTypeChoice.RESULT_CHAIN) { + setSubGui(new SubGuiChainSelect()); + } + } + + private void handleChainSelectClosed(SubGuiChainSelect gui) { + String chainName = gui.getSelectedName(); + if (chainName != null) { + loadChainReference(chainName); + } + } + + private void handleAbilityConfigClosed() { + editingChainEntry = false; + editChainSlotIdx = -1; + editChainEntryIdx = -1; + editingChainEntryParent = false; + updateNpcAbilitiesList(); + save(); } /** * Called from SubGuiAbilityConfig when ability is saved. */ public void onAbilitySaved(Ability ability) { + // Check if we were editing a global parent ability via "Modify Parent" on a chain entry + if (editingChainEntryParent) { + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + editingChainEntryParent = false; + return; + } + + // Check if we were editing an ability inside a chain entry + if (editingChainEntry && editChainSlotIdx >= 0 && editChainSlotIdx < npcSlots.size()) { + AbilityAction slot = npcSlots.get(editChainSlotIdx); + ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : null; + if (chain != null && editChainEntryIdx >= 0 && editChainEntryIdx < chain.getEntries().size()) { + // Update the inline ability on the entry — it's already the same object reference, + // but refresh the list and save to persist + } + editingChainEntry = false; + editChainSlotIdx = -1; + editChainEntryIdx = -1; + updateNpcAbilitiesList(); + save(); + return; + } + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { AbilityAction slot = npcSlots.get(selectedAbilityIndex); if (slot.isReference()) { - // For reference slots, save back to controller PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); } else { - // For inline slots, replace with the updated ability npcSlots.set(selectedAbilityIndex, AbilityAction.inline(ability)); } updateNpcAbilitiesList(); save(); } } + + @Override + public void onChainedAbilitySaved(ChainedAbility chain) { + // Handled in subGuiClosed — pendingChain tracks the chain being edited + } + + /** + * Called from SubGuiAbilityLoad when a chain reference is loaded. + */ + public void loadChainReference(String referenceId) { + if (referenceId != null && !referenceId.isEmpty()) { + npcSlots.add(AbilityAction.chainReference(referenceId)); + selectedSlotIndex = npcSlots.size() - 1; + selectedEntryIndex = -1; + selectedAbilityIndex = selectedSlotIndex; + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); + updateNpcAbilitiesList(); + initGui(); + save(); + } + } + + /** + * Called from SubGuiAbilityLoad to load a chain as inline. + */ + public void loadChainInline(ChainedAbility chain) { + if (chain != null) { + npcSlots.add(AbilityAction.inlineChain(chain)); + selectedSlotIndex = npcSlots.size() - 1; + selectedEntryIndex = -1; + selectedAbilityIndex = selectedSlotIndex; + if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll(); + updateNpcAbilitiesList(); + initGui(); + save(); + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java index 4d21f12ca..9e3856a14 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java @@ -3,18 +3,15 @@ package noppes.npcs.client.gui.advanced; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.SubGuiInterface; /** * Dialog asking the user whether to clone-and-modify (convert to inline) or modify the parent ability. */ -public class SubGuiAbilityEditMode extends SubGuiInterface { +public class SubGuiAbilityEditMode extends SubGuiSimpleChoice { public static final int MODE_CLONE_MODIFY = 0; public static final int MODE_MODIFY_PARENT = 1; - private int result = -1; - public SubGuiAbilityEditMode() { setBackground("menubg.png"); xSize = 220; @@ -39,18 +36,13 @@ public class SubGuiAbilityEditMode extends SubGuiInterface { @Override public void buttonEvent(GuiButton guibutton) { if (guibutton.id == 0) { - result = MODE_CLONE_MODIFY; + setResult(MODE_CLONE_MODIFY); close(); } else if (guibutton.id == 1) { - result = MODE_MODIFY_PARENT; + setResult(MODE_MODIFY_PARENT); close(); } else if (guibutton.id == 2) { - result = -1; close(); } } - - public int getResult() { - return result; - } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 905faa1f4..595f5a773 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -1,7 +1,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java index 28a7bd0bb..5995c0e9e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java @@ -3,18 +3,15 @@ package noppes.npcs.client.gui.advanced; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.SubGuiInterface; /** * Dialog asking the user whether to Clone (inline copy) or Reference the loaded ability. */ -public class SubGuiAbilityLoadMode extends SubGuiInterface { +public class SubGuiAbilityLoadMode extends SubGuiSimpleChoice { public static final int MODE_CLONE = 0; public static final int MODE_REFERENCE = 1; - private int result = -1; - public SubGuiAbilityLoadMode() { setBackground("menubg.png"); xSize = 200; @@ -39,18 +36,13 @@ public class SubGuiAbilityLoadMode extends SubGuiInterface { @Override public void buttonEvent(GuiButton guibutton) { if (guibutton.id == 0) { - result = MODE_CLONE; + setResult(MODE_CLONE); close(); } else if (guibutton.id == 1) { - result = MODE_REFERENCE; + setResult(MODE_REFERENCE); close(); } else if (guibutton.id == 2) { - result = -1; close(); } } - - public int getResult() { - return result; - } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index b8a4e0e66..57a363e1f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -21,6 +21,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui private final Ability ability; private final IAbilityConfigCallback callback; private final Set existingNames; + private boolean saved = false; public SubGuiAbilitySaveConfirm(Ability ability) { this(ability, null, null); @@ -68,6 +69,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui } private void doSave() { + saved = true; PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); if (callback != null) { callback.onAbilitySaved(ability); @@ -75,6 +77,10 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui close(); } + public boolean wasSaved() { + return saved; + } + @Override public void subGuiClosed(SubGuiInterface subgui) { if (subgui instanceof SubGuiDuplicateNameConfirm) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java new file mode 100644 index 000000000..a1d235d51 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java @@ -0,0 +1,162 @@ +package noppes.npcs.client.gui.advanced; + +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Vector; + +/** + * Simple ability name picker. Shows all abilities (custom + built-in) in a + * single scroll list with search. Returns the selected ability name. + */ +public class SubGuiAbilitySelect extends SubGuiInterface implements ICustomScrollListener, IScrollData { + + private GuiCustomScroll scroll; + private HashMap customData = new HashMap<>(); + private HashMap builtInData = new HashMap<>(); + private String selectedName = null; + private String search = ""; + + public SubGuiAbilitySelect() { + setBackground("menubg.png"); + xSize = 220; + ySize = 216; + + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 5; + + addLabel(new GuiNpcLabel(0, "ability.select", guiLeft + 10, y)); + y += 14; + + if (scroll == null) { + scroll = new GuiCustomScroll(this, 0); + scroll.setSize(200, 140); + } + scroll.guiLeft = guiLeft + 10; + scroll.guiTop = y; + scroll.setList(getFilteredList()); + if (selectedName != null) { + scroll.setSelected(selectedName); + } + addScroll(scroll); + + y += 143; + + addTextField(new GuiNpcTextField(10, this, fontRendererObj, guiLeft + 10, y, 200, 20, search)); + + y += 24; + + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.select")); + addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel")); + + getButton(0).setEnabled(selectedName != null); + } + + private List getFilteredList() { + HashMap merged = new HashMap<>(); + merged.putAll(customData); + merged.putAll(builtInData); + + if (search.isEmpty()) { + return new ArrayList<>(merged.keySet()); + } + List list = new ArrayList<>(); + for (String name : merged.keySet()) { + if (name.toLowerCase().contains(search.toLowerCase())) { + list.add(name); + } + } + return list; + } + + @Override + public void buttonEvent(GuiButton guibutton) { + int id = guibutton.id; + + if (id == 0 && selectedName != null) { + close(); + } else if (id == 2) { + selectedName = null; + close(); + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(10) != null && getTextField(10).isFocused()) { + String newSearch = getTextField(10).getText(); + if (!search.equals(newSearch)) { + search = newSearch; + scroll.resetScroll(); + scroll.setList(getFilteredList()); + } + } + } + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { + if (scroll.id == 0) { + selectedName = scroll.getSelected(); + initGui(); + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + if (scroll.id == 0 && selection != null && !selection.isEmpty()) { + selectedName = selection; + close(); + } + } + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.CUSTOM_ABILITIES) { + customData = data; + } else if (type == EnumScrollData.BUILTIN_ABILITIES) { + builtInData = data; + } + if (scroll != null) { + scroll.setList(getFilteredList()); + if (selectedName != null) { + scroll.setSelected(selectedName); + } + } + initGui(); + } + + @Override + public void setSelected(String selected) { + this.selectedName = selected; + if (scroll != null) { + scroll.setSelected(selected); + } + } + + /** + * Get the selected ability name after the dialog closes. + * Returns null if cancelled. + */ + public String getSelectedName() { + return selectedName; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java index 5d810862f..99d1fff54 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java @@ -1,6 +1,6 @@ package noppes.npcs.client.gui.advanced; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.resources.I18n; import noppes.npcs.client.gui.util.GuiCustomScroll; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java new file mode 100644 index 000000000..f2d858551 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java @@ -0,0 +1,92 @@ +package noppes.npcs.client.gui.advanced; + +import kamkeel.npcs.controllers.AbilityController; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Selector for chained ability presets. Lists chain names from synced data. + * Returns the selected chain name via {@link #getSelectedName()}. + */ +public class SubGuiChainSelect extends SubGuiInterface implements ICustomScrollListener { + + private GuiCustomScroll scroll; + private String selectedName = null; + + public SubGuiChainSelect() { + setBackground("menubg.png"); + xSize = 220; + ySize = 200; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 5; + addLabel(new GuiNpcLabel(0, "ability.loadChain", guiLeft + 10, y)); + y += 14; + + List names = getChainNames(); + + if (scroll == null) { + scroll = new GuiCustomScroll(this, 0); + scroll.setSize(200, 130); + } + scroll.guiLeft = guiLeft + 10; + scroll.guiTop = y; + scroll.setList(names); + if (selectedName != null) { + scroll.setSelected(selectedName); + } + addScroll(scroll); + + y += 133; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.select")); + addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "gui.cancel")); + getButton(0).setEnabled(selectedName != null); + } + + private List getChainNames() { + Set names = AbilityController.Instance.getChainedAbilityNamesSet(); + return new ArrayList<>(names); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0 && selectedName != null) { + close(); + } else if (guibutton.id == 1) { + selectedName = null; + close(); + } + } + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll.id == 0) { + selectedName = guiCustomScroll.getSelected(); + initGui(); + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll.id == 0 && selection != null && !selection.isEmpty()) { + selectedName = selection; + close(); + } + } + + public String getSelectedName() { + return selectedName; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java new file mode 100644 index 000000000..83a091a87 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -0,0 +1,607 @@ +package noppes.npcs.client.gui.advanced; + +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityAction; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry; +import kamkeel.npcs.controllers.data.ability.Condition; +import kamkeel.npcs.controllers.data.ability.UserType; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.client.gui.builder.GuiFieldBuilder; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiScrollWindow; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * SubGui for editing a {@link ChainedAbility}. + * Three tabs: General, Entries, Conditions. + */ +public class SubGuiChainedAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener, IAbilityConfigCallback { + + // ── Declarative field ID ranges ────────────────────────────────────────── + private static final int DECLARATIVE_ID_START = 1000; + private static final int CLEAR_ID_START = 2000; + private static final int LABEL_ID_START = 3000; + + // ── Tab constants ───────────────────────────────────────────────────────── + private static final int TAB_GENERAL = 0; + private static final int TAB_ENTRIES = 1; + private static final int TAB_CONDITIONS = 2; + + // ── Tab button IDs ──────────────────────────────────────────────────────── + private static final int BTN_TAB_GENERAL = 90; + private static final int BTN_TAB_ENTRIES = 91; + private static final int BTN_TAB_CONDITIONS = 92; + private static final int BTN_CLOSE = -1000; + + // ── Entries tab: per-entry ID encoding ───────────────────────────────────── + // Actual ID = ENTRY_BASE + i * ENTRY_STRIDE + offset + // offset 0 = name/select button, 1 = delay text field, 2 = up, 3 = down, 4 = delete + private static final int ENTRY_BASE = 100; + private static final int ENTRY_END = 200; + private static final int ENTRY_STRIDE = 10; + private static final int BTN_ADD_ENTRY = 200; + + // ── Conditions tab: per-condition ID encoding ───────────────────────────── + // Actual ID = COND_BASE + i * COND_STRIDE + offset + // offset 0 = name/click, 1 = edit, 2 = delete + private static final int COND_BASE = 50; + private static final int COND_END = 80; + private static final int COND_STRIDE = 10; + private static final int BTN_ADD_COND = 80; + + // ── Layout constants ────────────────────────────────────────────────────── + private static final int L_LABEL_X = 5; + private static final int ROW_H = 24; + private static final int MAX_ENTRIES = 10; + + private final ChainedAbility chain; + private final IChainedAbilityConfigCallback callback; + private final boolean npcContext; + private final List npcSlots; + private int activeTab = TAB_GENERAL; + + private List fieldDefs; + private List entries; + private List conditions; + + private GuiFieldBuilder builder; + private float[] tabScrollY = new float[3]; + + // Entry editing + private int editingEntryIndex = -1; + + // Condition editing + private int editingConditionIndex = -1; + + // Save only on explicit X button, not ESC + private boolean saveOnClose = false; + + // Track consumed NPC slot indices for deferred removal on save + private final List consumedSlotIndices = new ArrayList<>(); + + public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback) { + this(chain, callback, false, null); + } + + public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback, + boolean npcContext, List npcSlots) { + this.chain = chain; + this.callback = callback; + this.npcContext = npcContext; + this.npcSlots = npcSlots; + this.entries = new ArrayList<>(chain.getEntries()); + this.conditions = new ArrayList<>(chain.getConditions()); + + buildFieldDefs(); + + setBackground("menubg.png", 217); + xSize = 356; + ySize = 200; + } + + private void buildFieldDefs() { + fieldDefs = new ArrayList<>(); + + fieldDefs.add(FieldDef.stringField("gui.name", chain::getName, chain::setName).tab("General")); + fieldDefs.add(FieldDef.stringField("gui.displayName", chain::getRawDisplayName, chain::setDisplayName).tab("General")); + fieldDefs.add(FieldDef.boolField("gui.enabled", chain::isEnabled, chain::setEnabled).tab("General")); + fieldDefs.add(FieldDef.intField("ability.weight", chain::getWeight, chain::setWeight).range(1, 100).tab("General")); + fieldDefs.add(FieldDef.boolField("ability.windUpAll", chain::isWindUpAll, chain::setWindUpAll).tab("General")); + fieldDefs.add(FieldDef.intField("ability.cooldown", chain::getCooldownTicks, chain::setCooldownTicks).range(0, 12000).tab("General")); + fieldDefs.add(FieldDef.row( + FieldDef.floatField("ability.minRange", chain::getMinRange, chain::setMinRange).range(0, 64), + FieldDef.floatField("ability.maxRange", chain::getMaxRange, chain::setMaxRange).range(0, 64) + ).tab("General")); + fieldDefs.add(FieldDef.enumField("ability.allowedBy", UserType.class, chain::getAllowedBy, chain::setAllowedBy).tab("General")); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INIT GUI + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void initGui() { + // Save scroll position + GuiScrollWindow oldSw = getScrollableGui(0); + if (oldSw != null && activeTab < tabScrollY.length) { + tabScrollY[activeTab] = oldSw.nextScrollY; + } + + super.initGui(); + + // Tab buttons + GuiMenuTopButton generalTab = new GuiMenuTopButton(BTN_TAB_GENERAL, guiLeft + 4, guiTop - 17, "menu.general"); + generalTab.active = (activeTab == TAB_GENERAL); + addTopButton(generalTab); + + GuiMenuTopButton entriesTab = new GuiMenuTopButton(BTN_TAB_ENTRIES, generalTab, "ability.entries"); + entriesTab.active = (activeTab == TAB_ENTRIES); + addTopButton(entriesTab); + + GuiMenuTopButton conditionsTab = new GuiMenuTopButton(BTN_TAB_CONDITIONS, entriesTab, "ability.conditions"); + conditionsTab.active = (activeTab == TAB_CONDITIONS); + addTopButton(conditionsTab); + + GuiMenuTopButton closeBtn = new GuiMenuTopButton(BTN_CLOSE, guiLeft + xSize - 22, guiTop - 17, "X"); + addTopButton(closeBtn); + + // Scroll window dimensions + int swX = guiLeft + 4; + int swY = guiTop + 5; + int swW = xSize - 8; + int swH = ySize - 10; + + if (activeTab == TAB_GENERAL) { + buildGeneralTab(swX, swY, swW, swH); + } else if (activeTab == TAB_ENTRIES) { + buildEntriesTab(swX, swY, swW, swH); + } else if (activeTab == TAB_CONDITIONS) { + buildConditionsTab(swX, swY, swW, swH); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // GENERAL TAB + // ═══════════════════════════════════════════════════════════════════════════ + + private void buildGeneralTab(int swX, int swY, int swW, int swH) { + List tabFields = new ArrayList<>(); + for (FieldDef def : fieldDefs) { + if ("General".equals(def.getTab()) && def.isVisible()) { + tabFields.add(def); + } + } + + builder = new GuiFieldBuilder(this, fontRendererObj); + builder.startIds(DECLARATIVE_ID_START, CLEAR_ID_START, LABEL_ID_START); + builder.startY(5); + + GuiScrollWindow sw = builder.buildScrollWindow(tabFields, swX, swY, swW, swH); + restoreScroll(sw); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ENTRIES TAB + // ═══════════════════════════════════════════════════════════════════════════ + + private void buildEntriesTab(int swX, int swY, int swW, int swH) { + GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); + sw.backgroundColor = 0x88000000; + addScrollableGui(0, sw); + + int y = 5; + int labelCounter = 0; + + for (int i = 0; i < entries.size() && i < MAX_ENTRIES; i++) { + ChainedAbilityEntry entry = entries.get(i); + + // Index label + sw.addLabel(new GuiNpcLabel(labelCounter++, (i + 1) + ".", L_LABEL_X, y + 5, 0xFFFFFF)); + + // Ability reference/inline button + String btnLabel; + if (entry.isInline()) { + Ability a = entry.getInlineAbility(); + btnLabel = a != null ? a.getDisplayName() : "\u00A7cEmpty"; + } else { + String refName = entry.getAbilityReference(); + if (refName == null || refName.isEmpty()) { + btnLabel = StatCollector.translateToLocal("gui.select") + "..."; + } else { + Ability resolved = entry.resolve(); + btnLabel = "\u00A7e> " + (resolved != null ? resolved.getDisplayName() : refName); + } + } + sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE, L_LABEL_X + 15, y, 140, 20, btnLabel)); + + // Delay label + field + sw.addLabel(new GuiNpcLabel(labelCounter++, "ability.delay", L_LABEL_X + 160, y + 5, 0xAAAAAA)); + GuiNpcTextField delayField = new GuiNpcTextField(ENTRY_BASE + i * ENTRY_STRIDE + 1, this, fontRendererObj, + L_LABEL_X + 195, y, 40, 20, String.valueOf(entry.getDelayTicks())); + delayField.setIntegersOnly(); + delayField.setMinMaxDefault(0, 6000, 0); + sw.addTextField(delayField); + + // Up button + if (i > 0) { + sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 2, L_LABEL_X + 240, y, 20, 20, "\u2191")); + } + + // Down button + if (i < entries.size() - 1) { + sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 3, L_LABEL_X + 263, y, 20, 20, "\u2193")); + } + + // Delete button + sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 4, L_LABEL_X + 288, y, 20, 20, "X")); + + y += ROW_H; + } + + // Add entry button + if (entries.size() < MAX_ENTRIES) { + sw.addButton(new GuiNpcButton(BTN_ADD_ENTRY, L_LABEL_X, y, 80, 20, "gui.add")); + y += ROW_H; + } + + sw.maxScrollY = Math.max(y - swH, 0); + restoreScroll(sw); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CONDITIONS TAB + // ═══════════════════════════════════════════════════════════════════════════ + + private void buildConditionsTab(int swX, int swY, int swW, int swH) { + GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); + sw.backgroundColor = 0x88000000; + addScrollableGui(0, sw); + + int y = 5; + int labelCounter = 0; + + sw.addLabel(new GuiNpcLabel(labelCounter++, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); + y += 15; + + for (int i = 0; i < conditions.size() && i < 3; i++) { + Condition cond = conditions.get(i); + String condName = getConditionDisplayName(cond); + sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE, L_LABEL_X, y, 140, 20, condName)); + sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE + 1, L_LABEL_X + 145, y, 40, 20, "gui.edit")); + sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE + 2, L_LABEL_X + 190, y, 20, 20, "X")); + y += 22; + } + + if (conditions.size() < 3) { + sw.addButton(new GuiNpcButton(BTN_ADD_COND, L_LABEL_X, y, 50, 20, "gui.add")); + y += ROW_H; + } + + sw.maxScrollY = Math.max(y - swH, 0); + restoreScroll(sw); + } + + private String getConditionDisplayName(Condition cond) { + if (cond == null) return "None"; + String typeId = cond.getTypeId(); + switch (typeId) { + case "hp_above": + return StatCollector.translateToLocal("condition.hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; + case "hp_below": + return StatCollector.translateToLocal("condition.hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; + case "target_hp_above": + return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; + case "target_hp_below": + return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; + case "hit_count": + Condition.ConditionHitCount hc = (Condition.ConditionHitCount) cond; + return StatCollector.translateToLocal("condition.hit_count") + ": " + hc.getRequiredHits() + "/" + hc.getWithinTicks() + "t"; + default: + return typeId; + } + } + + private float getConditionThreshold(Condition cond) { + NBTTagCompound nbt = cond.writeNBT(); + return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SCROLL RESTORE + // ═══════════════════════════════════════════════════════════════════════════ + + private void restoreScroll(GuiScrollWindow sw) { + if (activeTab < tabScrollY.length) { + float restored = Math.min(tabScrollY[activeTab], sw.maxScrollY); + sw.nextScrollY = restored; + sw.scrollY = restored; + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // BUTTON EVENTS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void buttonEvent(GuiButton guibutton) { + int id = guibutton.id; + + // Tab switching + if (id == BTN_TAB_GENERAL) { activeTab = TAB_GENERAL; initGui(); return; } + if (id == BTN_TAB_ENTRIES) { activeTab = TAB_ENTRIES; initGui(); return; } + if (id == BTN_TAB_CONDITIONS) { activeTab = TAB_CONDITIONS; initGui(); return; } + if (id == BTN_CLOSE) { saveOnClose = true; close(); return; } + + // Entries tab + if (activeTab == TAB_ENTRIES) { + if (handleEntryButton(id)) return; + } + + // Conditions tab + if (activeTab == TAB_CONDITIONS) { + if (handleConditionButton(id)) return; + } + + // General tab — declarative field handling + if (activeTab == TAB_GENERAL && builder != null) { + if (builder.handleButtonEvent(id, guibutton)) { + if (!hasSubGui()) { + initGui(); + } + return; + } + } + } + + private boolean handleEntryButton(int id) { + // Add entry + if (id == BTN_ADD_ENTRY) { + if (entries.size() < MAX_ENTRIES) { + // Both contexts use entry source selector; hide NPC option in global context + setSubGui(new SubGuiChainedEntrySource(npcContext)); + } + return true; + } + + // Per-entry buttons (ENTRY_BASE to ENTRY_END) + if (id >= ENTRY_BASE && id < ENTRY_END) { + int entryIndex = (id - ENTRY_BASE) / ENTRY_STRIDE; + int action = (id - ENTRY_BASE) % ENTRY_STRIDE; + + if (entryIndex < 0 || entryIndex >= entries.size()) return false; + + switch (action) { + case 0: // Entry name button — reference: select ability; inline: edit config + ChainedAbilityEntry clickedEntry = entries.get(entryIndex); + if (clickedEntry.isReference()) { + editingEntryIndex = entryIndex; + setSubGui(new SubGuiAbilitySelect()); + } else if (clickedEntry.isInline() && clickedEntry.getInlineAbility() != null) { + editingEntryIndex = entryIndex; + setSubGui(new SubGuiAbilityConfig(clickedEntry.getInlineAbility(), this)); + } + return true; + case 2: // Up + if (entryIndex > 0) { + ChainedAbilityEntry entry = entries.remove(entryIndex); + entries.add(entryIndex - 1, entry); + initGui(); + } + return true; + case 3: // Down + if (entryIndex < entries.size() - 1) { + ChainedAbilityEntry entry = entries.remove(entryIndex); + entries.add(entryIndex + 1, entry); + initGui(); + } + return true; + case 4: // Delete + entries.remove(entryIndex); + initGui(); + return true; + } + } + + return false; + } + + private boolean handleConditionButton(int id) { + if (id >= COND_BASE && id < COND_END) { + int condIndex = (id - COND_BASE) / COND_STRIDE; + int action = (id - COND_BASE) % COND_STRIDE; + if (action == 0 || action == 1) { + if (condIndex < conditions.size()) { + editingConditionIndex = condIndex; + setSubGui(new SubGuiConditionEdit(conditions.get(condIndex))); + } + } else if (action == 2) { + if (condIndex < conditions.size()) { + conditions.remove(condIndex); + initGui(); + } + } + return true; + } + if (id == BTN_ADD_COND) { + if (conditions.size() < 3) { + editingConditionIndex = conditions.size(); + setSubGui(new SubGuiConditionEdit(null)); + } + return true; + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TEXT FIELD EVENTS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void unFocused(GuiNpcTextField textField) { + int id = textField.id; + + // General tab — declarative fields + if (activeTab == TAB_GENERAL && builder != null && id >= DECLARATIVE_ID_START) { + if (builder.handleTextFieldEvent(id, textField)) { + initGui(); + } + return; + } + + // Entries tab — delay fields (ENTRY_BASE + i * ENTRY_STRIDE + 1) + if (activeTab == TAB_ENTRIES && id >= ENTRY_BASE + 1 && id < ENTRY_END) { + int entryIndex = (id - ENTRY_BASE - 1) / ENTRY_STRIDE; + if (entryIndex >= 0 && entryIndex < entries.size()) { + entries.get(entryIndex).setDelayTicks(textField.getInteger()); + } + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SUB GUI CLOSED + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + // General tab — declarative sub-gui handling + if (activeTab == TAB_GENERAL && builder != null) { + if (builder.handleSubGuiClosed(subgui)) { + initGui(); + return; + } + } + + if (subgui instanceof SubGuiAbilitySelect) { handleAbilitySelectClosed((SubGuiAbilitySelect) subgui); initGui(); return; } + if (subgui instanceof SubGuiChainedEntrySource) { handleEntrySourceClosed((SubGuiChainedEntrySource) subgui); return; } + if (subgui instanceof SubGuiNpcSlotPicker) { handleSlotPickerClosed((SubGuiNpcSlotPicker) subgui); initGui(); return; } + if (subgui instanceof SubGuiAbilityTypeSelect) { handleTypeSelectClosed((SubGuiAbilityTypeSelect) subgui); if (!hasSubGui()) initGui(); return; } + if (subgui instanceof SubGuiAbilityConfig) { editingEntryIndex = -1; initGui(); return; } + if (subgui instanceof SubGuiConditionEdit) { handleConditionEditClosed((SubGuiConditionEdit) subgui); initGui(); return; } + } + + private void handleAbilitySelectClosed(SubGuiAbilitySelect gui) { + String selectedName = gui.getSelectedName(); + if (selectedName != null) { + if (editingEntryIndex >= 0 && editingEntryIndex < entries.size()) { + int delay = entries.get(editingEntryIndex).getDelayTicks(); + entries.set(editingEntryIndex, ChainedAbilityEntry.reference(selectedName, delay)); + } else { + entries.add(ChainedAbilityEntry.reference(selectedName, 0)); + } + } + editingEntryIndex = -1; + } + + private void handleEntrySourceClosed(SubGuiChainedEntrySource gui) { + int source = gui.getResult(); + if (source == SubGuiChainedEntrySource.SOURCE_NPC_SLOTS) { + setSubGui(new SubGuiNpcSlotPicker(npcSlots)); + } else if (source == SubGuiChainedEntrySource.SOURCE_LOAD_PRESET) { + setSubGui(new SubGuiAbilitySelect()); + } else if (source == SubGuiChainedEntrySource.SOURCE_CREATE_NEW) { + setSubGui(new SubGuiAbilityTypeSelect()); + } + // Don't initGui — we're opening another SubGui + } + + private void handleSlotPickerClosed(SubGuiNpcSlotPicker gui) { + int slotIndex = gui.getSelectedIndex(); + if (slotIndex >= 0 && npcSlots != null && slotIndex < npcSlots.size()) { + AbilityAction consumed = npcSlots.get(slotIndex); + Ability ability = consumed.getAbility(); + if (ability != null) { + if (consumed.getSlotType() == AbilityAction.SlotType.INLINE_ABILITY) { + entries.add(ChainedAbilityEntry.inline(ability, 0)); + } else { + entries.add(ChainedAbilityEntry.reference(consumed.getReferenceId(), 0)); + } + consumedSlotIndices.add(slotIndex); + } + } + } + + private void handleTypeSelectClosed(SubGuiAbilityTypeSelect gui) { + String typeId = gui.getSelectedTypeId(); + if (typeId != null) { + Ability newAbility = AbilityController.Instance.create(typeId); + if (newAbility != null) { + newAbility.setId(java.util.UUID.randomUUID().toString()); + entries.add(ChainedAbilityEntry.inline(newAbility, 0)); + editingEntryIndex = entries.size() - 1; + setSubGui(new SubGuiAbilityConfig(newAbility, this)); + } + } + } + + private void handleConditionEditClosed(SubGuiConditionEdit gui) { + Condition result = gui.getResult(); + if (result != null && editingConditionIndex >= 0) { + if (editingConditionIndex < conditions.size()) { + conditions.set(editingConditionIndex, result); + } else { + conditions.add(result); + } + } + editingConditionIndex = -1; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CLOSE / APPLY + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void onAbilitySaved(Ability ability) { + // Inline ability mutated in-place; entry already holds the reference + } + + @Override + public void close() { + if (saveOnClose) { + GuiNpcTextField.unfocus(); + applyToChain(); + callback.onChainedAbilitySaved(chain); + } + super.close(); + } + + + private void applyToChain() { + // Entries + chain.getEntries().clear(); + for (ChainedAbilityEntry entry : entries) { + chain.addEntry(entry); + } + + // Conditions + chain.getConditions().clear(); + for (Condition c : conditions) { + chain.getConditions().add(c); + } + + // Remove consumed NPC slots (descending order to preserve indices) + if (npcSlots != null && !consumedSlotIndices.isEmpty()) { + Collections.sort(consumedSlotIndices, Collections.reverseOrder()); + for (int idx : consumedSlotIndices) { + if (idx < npcSlots.size()) { + npcSlots.remove(idx); + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java new file mode 100644 index 000000000..2c544a937 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java @@ -0,0 +1,58 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; + +/** + * Choice dialog for adding an entry to a chain. + * Options: From NPC Slots (optional), Load Preset, Create New. + */ +public class SubGuiChainedEntrySource extends SubGuiSimpleChoice { + + public static final int SOURCE_NONE = -1; + public static final int SOURCE_NPC_SLOTS = 0; + public static final int SOURCE_LOAD_PRESET = 1; + public static final int SOURCE_CREATE_NEW = 2; + + private final boolean showNpcOption; + + public SubGuiChainedEntrySource() { + this(true); + } + + public SubGuiChainedEntrySource(boolean showNpcOption) { + this.showNpcOption = showNpcOption; + setBackground("menubg.png"); + xSize = 200; + ySize = showNpcOption ? 110 : 88; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.entrySource", guiLeft + 10, y)); + + y += 20; + if (showNpcOption) { + addButton(new GuiNpcButton(0, guiLeft + 10, y, 180, 20, "ability.fromNpc")); + y += 22; + } + + addButton(new GuiNpcButton(1, guiLeft + 10, y, 180, 20, "ability.byReference")); + + y += 22; + addButton(new GuiNpcButton(2, guiLeft + 10, y, 180, 20, "ability.createNew")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + int id = guibutton.id; + if (id >= 0 && id <= 2) { + setResult(id); + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java new file mode 100644 index 000000000..cb3fd4cb3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java @@ -0,0 +1,47 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; + +/** + * Choice dialog for the Load button: Load Ability or Load Chain. + */ +public class SubGuiLoadTypeChoice extends SubGuiSimpleChoice { + + public static final int RESULT_NONE = -1; + public static final int RESULT_ABILITY = 0; + public static final int RESULT_CHAIN = 1; + + public SubGuiLoadTypeChoice() { + setBackground("menubg.png"); + xSize = 200; + ySize = 80; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.loadType", guiLeft + 10, y)); + + y += 20; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 85, 20, "ability.loadAbility")); + addButton(new GuiNpcButton(1, guiLeft + 105, y, 85, 20, "ability.loadChain")); + + y += 24; + addButton(new GuiNpcButton(2, guiLeft + 55, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + int id = guibutton.id; + if (id == 0 || id == 1) { + setResult(id); + close(); + } else if (id == 2) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java new file mode 100644 index 000000000..90086857f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java @@ -0,0 +1,113 @@ +package noppes.npcs.client.gui.advanced; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityAction; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.resources.I18n; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * Picker for selecting an NPC ability slot to consume into a chain entry. + * Shows standalone (non-chain) ability slots from the NPC's current list. + */ +public class SubGuiNpcSlotPicker extends SubGuiInterface implements ICustomScrollListener { + + private final List npcSlots; + private final List validIndices = new ArrayList<>(); + private GuiCustomScroll scroll; + private int selectedIndex = -1; + + public SubGuiNpcSlotPicker(List npcSlots) { + this.npcSlots = npcSlots; + setBackground("menubg.png"); + xSize = 220; + ySize = 200; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 5; + addLabel(new GuiNpcLabel(0, "ability.selectSlot", guiLeft + 10, y)); + y += 14; + + List displayList = new ArrayList<>(); + validIndices.clear(); + + for (int i = 0; i < npcSlots.size(); i++) { + AbilityAction slot = npcSlots.get(i); + // Only show standalone abilities (not chains) + if (slot.isChain()) continue; + + Ability ability = slot.getAbility(); + if (ability == null) continue; + + String typeName = I18n.format(ability.getTypeId()); + String customName = ability.getName(); + String display; + if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) { + display = (i + 1) + ". " + customName + " (" + typeName + ")"; + } else { + display = (i + 1) + ". " + typeName; + } + + if (slot.isReference()) { + display = "\u00A7e" + display + " (Ref)"; + } + + displayList.add(display); + validIndices.add(i); + } + + if (scroll == null) { + scroll = new GuiCustomScroll(this, 0); + scroll.setSize(200, 130); + } + scroll.guiLeft = guiLeft + 10; + scroll.guiTop = y; + scroll.setList(displayList); + addScroll(scroll); + + y += 133; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.select")); + addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "gui.cancel")); + getButton(0).setEnabled(selectedIndex >= 0); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0 && selectedIndex >= 0) { + close(); + } else if (guibutton.id == 1) { + selectedIndex = -1; + close(); + } + } + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll.id == 0 && guiCustomScroll.selected >= 0 && guiCustomScroll.selected < validIndices.size()) { + selectedIndex = validIndices.get(guiCustomScroll.selected); + initGui(); + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll.id == 0 && selectedIndex >= 0) { + close(); + } + } + + public int getSelectedIndex() { + return selectedIndex; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java new file mode 100644 index 000000000..1fb3e8fe8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java @@ -0,0 +1,23 @@ +package noppes.npcs.client.gui.advanced; + +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Base class for simple choice dialogs that present a few buttons and return an int result. + * Subclasses define their own layout in {@code initGui()} and call {@code setResult()} from {@code buttonEvent()}. + * The parent reads the result via {@code getResult()} in {@code subGuiClosed()}. + * + *

Result is -1 by default (cancelled / no selection).

+ */ +public abstract class SubGuiSimpleChoice extends SubGuiInterface { + + private int result = -1; + + protected final void setResult(int result) { + this.result = result; + } + + public int getResult() { + return result; + } +} 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 9378e606b..fef120604 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -1,11 +1,14 @@ package noppes.npcs.client.gui.global; -import kamkeel.npcs.client.renderer.TelegraphRenderer; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.ChainedAbilitiesGetPacket; +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; @@ -17,9 +20,9 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; import noppes.npcs.client.NoppesUtil; import kamkeel.npcs.controllers.data.ability.AbilityVariant; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; 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.AbilityPreviewExecutor; import noppes.npcs.client.gui.util.GuiAbilityInterface; @@ -29,6 +32,7 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.GuiTexturedButton; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback; import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IScrollData; @@ -55,7 +59,28 @@ import java.util.Vector; */ public class GuiNpcManageAbilities extends GuiAbilityInterface implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback, - ITextfieldListener, GuiYesNoCallback, IScrollData, IGuiData { + IChainedAbilityConfigCallback, ITextfieldListener, GuiYesNoCallback, + IScrollData, IGuiData { + + // ── Button IDs ──────────────────────────────────────────────────────────── + private static final int BTN_REMOVE = 1; + private static final int BTN_ADD = 2; + private static final int BTN_TOGGLE_VIEW = 10; + private static final int BTN_EDIT = 100; + private static final int BTN_PREVIEW_PLAY = 91; + private static final int BTN_PREVIEW_PAUSE = 92; + private static final int BTN_PREVIEW_STOP = 93; + + // ── TextField / Label IDs ───────────────────────────────────────────────── + private static final int TF_SEARCH = 55; + private static final int LBL_PREVIEW_STATUS = 90; + + // ── Scroll IDs ──────────────────────────────────────────────────────────── + private static final int SCROLL_MAIN = 0; + + // ── Confirm dialog IDs ──────────────────────────────────────────────────── + private static final int CONFIRM_REMOVE_ABILITY = 1; + private static final int CONFIRM_REMOVE_CHAIN = 2; // ==================== DATA ==================== private GuiCustomScroll scroll; @@ -65,10 +90,18 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private String search = ""; private Ability selectedAbility = null; - // ==================== BUILT-IN TOGGLE ==================== + // ==================== VIEW MODE ==================== + // 0 = Custom, 1 = Built-in, 2 = Chained + private int viewMode = 0; private boolean showingBuiltIn = false; + private boolean showingChained = false; private boolean currentIsBuiltIn = false; + // ==================== CHAINED DATA ==================== + private HashMap chainedData = new HashMap<>(); + private ChainedAbility selectedChain = null; + private ChainedAbility pendingSaveChain = null; + // ==================== PENDING SAVE ==================== private Ability pendingSaveAbility = null; private boolean pendingNewCreation = false; @@ -103,42 +136,49 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface public void initGui() { super.initGui(); - // Toggle button — added FIRST to maintain stable buttonList index - String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; - addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 52, 45, 20, toggleLabel)); + // 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)); - if (!showingBuiltIn) { - // Add button — only for custom view - addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - - // Remove button — only for custom view - addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); - getButton(1).setEnabled(selected != null && !selected.isEmpty() && customData.containsKey(selected)); + 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")); + getButton(BTN_REMOVE).setEnabled(selected != null && !selected.isEmpty() && chainedData.containsKey(selected)); + addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 74, 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")); + 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")); + getButton(BTN_EDIT).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); + } } - // Edit button — hidden when viewing built-in - if (!showingBuiltIn && !currentIsBuiltIn) { - addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit")); - getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); - } - - // Scroll list of abilities (right side) + // Scroll list (right side) if (scroll == null) { - scroll = new GuiCustomScroll(this, 0); + scroll = new GuiCustomScroll(this, SCROLL_MAIN); scroll.setSize(143, 185); } scroll.guiLeft = guiLeft + 220; scroll.guiTop = guiTop + 4; addScroll(scroll); - // Update scroll list scroll.setList(getSearchList()); if (selected != null) { scroll.setSelected(selected); } // Search bar - addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); + addTextField(new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); + + // Preview only for ability views (not chained) + if (showingChained) { + return; + } // Show ability info only if one is selected if (selectedAbility == null || selected == null) { @@ -154,22 +194,20 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface boolean isActive = previewExecutor.isActive(); if (!isPlaying || isPaused) { - // Show Play button String statusKey = isPaused ? "animation.paused" : "animation.stopped"; - addLabel(new GuiNpcLabel(90, statusKey, guiLeft + playButtonOffsetX, guiTop + 198)); - addButton(new GuiTexturedButton(91, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71)); + addLabel(new GuiNpcLabel(LBL_PREVIEW_STATUS, statusKey, guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(BTN_PREVIEW_PLAY, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71)); } else { - // Show Pause button - addLabel(new GuiNpcLabel(90, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198)); - addButton(new GuiTexturedButton(92, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71)); + addLabel(new GuiNpcLabel(LBL_PREVIEW_STATUS, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(BTN_PREVIEW_PAUSE, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71)); } if (isActive) { - // Show Stop button - addButton(new GuiTexturedButton(93, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71)); + addButton(new GuiTexturedButton(BTN_PREVIEW_STOP, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71)); } } private HashMap getCurrentData() { + if (showingChained) return chainedData; return showingBuiltIn ? builtInData : customData; } @@ -240,33 +278,64 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface int id = guibutton.id; - // Toggle between custom and built-in views - if (id == 10) { - showingBuiltIn = !showingBuiltIn; + // Toggle: Custom → Built-in → Chained → Custom + if (id == BTN_TOGGLE_VIEW) { + if (!showingBuiltIn && !showingChained) { + // Custom → Built-in + showingBuiltIn = true; + showingChained = false; + } else if (showingBuiltIn) { + // Built-in → Chained + showingBuiltIn = false; + showingChained = true; + } else { + // Chained → Custom + showingChained = false; + showingBuiltIn = false; + } selected = null; selectedAbility = null; + selectedChain = null; currentIsBuiltIn = false; search = ""; previewExecutor.stop(); if (scroll != null) { scroll.clear(); } - PacketClient.sendClient(new CustomAbilitiesGetPacket()); + if (showingChained) { + PacketClient.sendClient(new ChainedAbilitiesGetPacket()); + } else { + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + } initGui(); return; } - if (id == 2 && !showingBuiltIn) { + if (showingChained) { + if (id == BTN_ADD) { + // New chained ability + ChainedAbility newChain = new ChainedAbility("New Chain"); + setSubGui(new SubGuiChainedAbilityConfig(newChain, this)); + } else if (id == BTN_REMOVE && selected != null) { + GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), CONFIRM_REMOVE_CHAIN); + displayGuiScreen(guiyesno); + } else if (id == BTN_EDIT && selectedChain != null) { + setSubGui(new SubGuiChainedAbilityConfig(selectedChain, this)); + } + return; + } + + if (id == BTN_ADD && !showingBuiltIn) { // Add — open type selection setSubGui(new SubGuiAbilityTypeSelect()); - } else if (id == 1 && !showingBuiltIn && selected != null) { - GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); + } else if (id == BTN_REMOVE && !showingBuiltIn && selected != null) { + GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), CONFIRM_REMOVE_ABILITY); displayGuiScreen(guiyesno); - } else if (id == 100 && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) { + } else if (id == BTN_EDIT && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) { // Edit previewExecutor.stop(); setSubGui(selectedAbility.createConfigGui(this)); - } else if (id == 91) { + } else if (id == BTN_PREVIEW_PLAY) { // Play button if (selectedAbility != null) { if (previewExecutor.isPaused()) { @@ -276,11 +345,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface } initGui(); } - } else if (id == 92) { + } else if (id == BTN_PREVIEW_PAUSE) { // Pause button previewExecutor.pause(); initGui(); - } else if (id == 93) { + } else if (id == BTN_PREVIEW_STOP) { // Stop button previewExecutor.stop(); initGui(); @@ -290,9 +359,9 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void keyTyped(char c, int i) { super.keyTyped(c, i); - if (getTextField(55) != null && getTextField(55).isFocused()) { - if (!search.equals(getTextField(55).getText())) { - search = getTextField(55).getText(); + if (getTextField(TF_SEARCH) != null && getTextField(TF_SEARCH).isFocused()) { + if (!search.equals(getTextField(TF_SEARCH).getText())) { + search = getTextField(TF_SEARCH).getText(); scroll.resetScroll(); scroll.setList(getSearchList()); } @@ -301,13 +370,15 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - if (scroll.id == 0) { + if (scroll.id == SCROLL_MAIN) { String newSelection = scroll.getSelected(); if (newSelection != null && !newSelection.equals(selected)) { previewExecutor.stop(); selected = newSelection; - if (showingBuiltIn) { + if (showingChained) { + PacketClient.sendClient(new ChainedAbilityGetPacket(selected)); + } else if (showingBuiltIn) { currentIsBuiltIn = true; PacketClient.sendClient(new BuiltInAbilityGetPacket(selected)); } else { @@ -320,9 +391,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to edit — only for custom, non-built-in abilities - if (!showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) { + if (scroll.id == SCROLL_MAIN && selection != null && !selection.isEmpty()) { + if (showingChained && selectedChain != null) { + setSubGui(new SubGuiChainedAbilityConfig(selectedChain, this)); + } else if (!showingBuiltIn && !showingChained && !currentIsBuiltIn && selectedAbility != null) { previewExecutor.stop(); setSubGui(selectedAbility.createConfigGui(this)); } @@ -334,7 +406,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface if (type == EnumScrollData.CUSTOM_ABILITIES) { String prevSelected = scroll != null ? scroll.getSelected() : null; this.customData = data; - if (!showingBuiltIn && scroll != null) { + if (!showingBuiltIn && !showingChained && scroll != null) { scroll.setList(getSearchList()); if (prevSelected != null && customData.containsKey(prevSelected)) { scroll.setSelected(prevSelected); @@ -350,11 +422,33 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface } } initGui(); + } else if (type == EnumScrollData.CHAINED_ABILITIES) { + String prevSelected = scroll != null ? scroll.getSelected() : null; + this.chainedData = data; + if (showingChained && scroll != null) { + scroll.setList(getSearchList()); + if (prevSelected != null && chainedData.containsKey(prevSelected)) { + scroll.setSelected(prevSelected); + } + } + initGui(); } } @Override public void setGuiData(NBTTagCompound compound) { + // Detect chained ability response (has "Entries" tag) + if (showingChained && compound.hasKey("Entries")) { + selectedChain = new ChainedAbility(); + selectedChain.readNBT(compound); + selected = selectedChain.getName(); + if (scroll != null) { + scroll.setSelected(selected); + } + initGui(); + return; + } + // Check if this is a built-in ability response currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); @@ -380,88 +474,112 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityVariantSelect) { - SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui; - int idx = variantGui.getSelectedIndex(); - if (idx >= 0 && pendingTypeId != null) { - Ability newAbility = AbilityController.Instance.create(pendingTypeId); - if (newAbility != null) { - variantGui.getVariants().get(idx).apply(newAbility); - if (hasDuplicateName(newAbility)) { - pendingSaveAbility = newAbility; - pendingNewCreation = true; - pendingTypeId = null; - setSubGui(new SubGuiDuplicateNameConfirm()); - return; - } + if (subgui instanceof SubGuiChainedAbilityConfig) { handleChainConfigClosed(); initGui(); return; } + if (subgui instanceof SubGuiAbilityVariantSelect) { if (handleVariantSelectClosed((SubGuiAbilityVariantSelect) subgui)) return; } + else if (subgui instanceof SubGuiAbilityTypeSelect) { if (handleTypeSelectClosed((SubGuiAbilityTypeSelect) subgui)) return; } + else if (subgui instanceof SubGuiDuplicateNameConfirm) { if (handleDuplicateNameClosed((SubGuiDuplicateNameConfirm) subgui)) return; } + else if (pendingSaveAbility != null) { if (handlePendingSave()) return; } + initGui(); + } + + private void handleChainConfigClosed() { + if (pendingSaveChain != null) { + PacketClient.sendClient(new ChainedAbilitySavePacket(pendingSaveChain.writeNBT())); + PacketClient.sendClient(new ChainedAbilitiesGetPacket()); + selected = pendingSaveChain.getName(); + pendingSaveChain = null; + } + } + + /** @return true if a SubGui was opened (caller should return early, skipping initGui) */ + private boolean handleVariantSelectClosed(SubGuiAbilityVariantSelect gui) { + int idx = gui.getSelectedIndex(); + if (idx >= 0 && pendingTypeId != null) { + Ability newAbility = AbilityController.Instance.create(pendingTypeId); + if (newAbility != null) { + gui.getVariants().get(idx).apply(newAbility); + if (hasDuplicateName(newAbility)) { + pendingSaveAbility = newAbility; + pendingNewCreation = true; pendingTypeId = null; - openConfig(newAbility); - return; + setSubGui(new SubGuiDuplicateNameConfirm()); + return true; } + pendingTypeId = null; + openConfig(newAbility); + return true; } - pendingTypeId = null; - } else if (subgui instanceof SubGuiAbilityTypeSelect) { - String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId(); - if (typeId != null) { - java.util.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); - } - if (hasDuplicateName(newAbility)) { - pendingSaveAbility = newAbility; - pendingNewCreation = true; - setSubGui(new SubGuiDuplicateNameConfirm()); - return; - } - openConfig(newAbility); - return; - } + } + pendingTypeId = null; + return false; + } + + /** @return true if a SubGui was opened (caller should return early, skipping initGui) */ + private boolean handleTypeSelectClosed(SubGuiAbilityTypeSelect gui) { + String typeId = gui.getSelectedTypeId(); + if (typeId != null) { + java.util.List variants = AbilityController.Instance.getVariantsForType(typeId); + if (variants.size() > 1) { + pendingTypeId = typeId; + setSubGui(new SubGuiAbilityVariantSelect(variants)); + return true; } - } else if (subgui instanceof SubGuiDuplicateNameConfirm) { - SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui; - if (confirm.isConfirmed() && pendingSaveAbility != null) { - if (pendingNewCreation) { - openConfig(pendingSaveAbility); - pendingSaveAbility = null; - pendingNewCreation = false; - return; - } else { - PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); - PacketClient.sendClient(new CustomAbilitiesGetPacket()); - pendingSaveAbility = null; + Ability newAbility = AbilityController.Instance.create(typeId); + if (newAbility != null) { + if (variants.size() == 1) { + variants.get(0).apply(newAbility); } - } else if (pendingSaveAbility != null) { - if (pendingNewCreation) { - // Cancel new creation - discard - pendingSaveAbility = null; - pendingNewCreation = false; - } else { - // Cancel edit save - re-open config GUI to change name - Ability ability = pendingSaveAbility; - pendingSaveAbility = null; - setSubGui(ability.createConfigGui(this)); - return; + if (hasDuplicateName(newAbility)) { + pendingSaveAbility = newAbility; + pendingNewCreation = true; + setSubGui(new SubGuiDuplicateNameConfirm()); + return true; } + openConfig(newAbility); + return true; + } + } + return false; + } + + /** @return true if a SubGui was opened (caller should return early, skipping initGui) */ + private boolean handleDuplicateNameClosed(SubGuiDuplicateNameConfirm gui) { + if (gui.isConfirmed() && pendingSaveAbility != null) { + if (pendingNewCreation) { + openConfig(pendingSaveAbility); + pendingSaveAbility = null; + pendingNewCreation = false; + return true; + } else { + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + pendingSaveAbility = null; } } else if (pendingSaveAbility != null) { - // Config GUI closed with a pending save (set by onAbilitySaved) - if (hasDuplicateName(pendingSaveAbility)) { + if (pendingNewCreation) { + pendingSaveAbility = null; pendingNewCreation = false; - setSubGui(new SubGuiDuplicateNameConfirm()); - return; + } else { + Ability ability = pendingSaveAbility; + pendingSaveAbility = null; + setSubGui(ability.createConfigGui(this)); + return true; } - PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); - PacketClient.sendClient(new CustomAbilitiesGetPacket()); - pendingSaveAbility = null; } - initGui(); + return false; + } + + /** @return true if a SubGui was opened (caller should return early, skipping initGui) */ + private boolean handlePendingSave() { + if (hasDuplicateName(pendingSaveAbility)) { + pendingNewCreation = false; + setSubGui(new SubGuiDuplicateNameConfirm()); + return true; + } + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + pendingSaveAbility = null; + return false; } private void openConfig(Ability ability) { @@ -487,12 +605,18 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface NoppesUtil.openGUI(player, this); if (!result) return; - if (id == 1 && selected != null) { + if (id == CONFIRM_REMOVE_ABILITY && selected != null) { PacketClient.sendClient(new CustomAbilityRemovePacket(selected)); scroll.clear(); selected = null; selectedAbility = null; initGui(); + } else if (id == CONFIRM_REMOVE_CHAIN && selected != null) { + PacketClient.sendClient(new ChainedAbilityRemovePacket(selected)); + scroll.clear(); + selected = null; + selectedChain = null; + initGui(); } } @@ -502,6 +626,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface pendingSaveAbility = ability; } + @Override + public void onChainedAbilitySaved(ChainedAbility chain) { + pendingSaveChain = chain; + } + @Override public void unFocused(GuiNpcTextField textfield) { // Search field handling is done in keyTyped diff --git a/src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java b/src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java new file mode 100644 index 000000000..f714ea2c7 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java @@ -0,0 +1,10 @@ +package noppes.npcs.client.gui.util; + +import kamkeel.npcs.controllers.data.ability.ChainedAbility; + +/** + * Callback for when a chained ability is saved from the config SubGui. + */ +public interface IChainedAbilityConfigCallback { + void onChainedAbilitySaved(ChainedAbility chain); +} diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index cbec6ad34..3f680c188 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -10,5 +10,6 @@ public enum EnumScrollData { CUSTOM_ABILITIES, BUILTIN_ABILITIES, ANIMATIONS, - BUILTIN_ANIMATIONS + BUILTIN_ANIMATIONS, + CHAINED_ABILITIES } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 1a1751a30..d3add842c 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -1,10 +1,9 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.ChainedAbility; -import kamkeel.npcs.controllers.data.ability.ChainedAbilityController; import kamkeel.npcs.controllers.data.ability.IAbilityAction; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -22,9 +21,7 @@ import noppes.npcs.LogWriter; import noppes.npcs.EventHooks; import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.api.entity.IPlayer; -import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.data.Animation; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerAbilityEvent; @@ -322,16 +319,14 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA */ private IAbilityAction resolveActionKey(String key) { if (key.startsWith(CHAIN_PREFIX)) { - if (ChainedAbilityController.Instance == null) return null; - return ChainedAbilityController.Instance.resolve(key.substring(CHAIN_PREFIX.length())); + return AbilityController.Instance.resolveChainedAbility(key.substring(CHAIN_PREFIX.length())); } - if (AbilityController.Instance == null) return null; return AbilityController.Instance.resolveAbility(key); } /** * Activate a specific ability by key. Keys prefixed with "chain:" are - * treated as chained abilities and resolved from {@link ChainedAbilityController}. + * treated as chained abilities and resolved from {@link AbilityController}. * * @param player The player * @param key The ability key (built-in or preset name), or "chain:name" for chains diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 02ec0f4b3..d1d041c8e 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -6,7 +6,7 @@ import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.TelegraphController; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.entity.EntityAbilityBeam; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.entity.EntityAbilityLaser; @@ -480,6 +480,8 @@ public class NpcAPI extends AbstractNpcAPI { data = new ScriptEntityData(new noppes.npcs.scripted.entity.ScriptEnergyDome<>((kamkeel.npcs.entity.EntityEnergyDome) entity)); else if (entity instanceof kamkeel.npcs.entity.EntityEnergyPanel) data = new ScriptEntityData(new noppes.npcs.scripted.entity.ScriptEnergyPanel<>((kamkeel.npcs.entity.EntityEnergyPanel) entity)); + else if (entity instanceof kamkeel.npcs.entity.EntityAbilitySweeper) + data = new ScriptEntityData(new noppes.npcs.scripted.entity.ScriptEnergySweeper<>((kamkeel.npcs.entity.EntityAbilitySweeper) entity)); else data = new ScriptEntityData(new ScriptEntity<>(entity)); entity.registerExtendedProperties("ScriptedObject", data); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java new file mode 100644 index 000000000..cf957c062 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java @@ -0,0 +1,9 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilitySweeper; + +public class ScriptEnergySweeper extends ScriptEnergyAbility { + public ScriptEnergySweeper(T entity) { + super(entity); + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index e08055018..f6b9379c1 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -1,7 +1,7 @@ package noppes.npcs.scripted.wrapper; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityAction; import net.minecraft.entity.EntityLivingBase; import noppes.npcs.DataAbilities; @@ -90,7 +90,7 @@ public class ScriptDataAbilities implements IDataAbilities { if (slot.getReferenceId().equals(abilityId)) return true; } else if (!slot.isReference()) { Ability a = slot.getAbility(); - if (a != null && a.getId().equals(abilityId)) return false; + if (a != null && abilityId.equals(a.getId())) return false; } } return false; @@ -135,10 +135,11 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public void setGlobalCooldown(int ticks) { - // Set the cooldown by manipulating the internal cooldown end time - // Not directly exposed, but we can reset and let it tick down - data.resetCooldown(); - // This is a best-effort implementation + if (ticks <= 0) { + data.resetCooldown(); + } else { + data.setCooldownEndTime(npc.worldObj.getTotalWorldTime() + ticks); + } } @Override diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index 42ed3372e..8d3d068d5 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -1,7 +1,7 @@ package noppes.npcs.scripted.wrapper; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.AbilityController; import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.api.ability.IAbility; import noppes.npcs.api.ability.IPlayerAbilityData; @@ -43,12 +43,14 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { @Override public void lockAbility(String key) { - data.lockAbility(key); + String canonicalKey = resolveCanonicalKey(key); + data.lockAbility(canonicalKey); } @Override public boolean hasUnlockedAbility(String key) { - return data.hasUnlockedAbility(key); + String canonicalKey = resolveCanonicalKey(key); + return data.hasUnlockedAbility(canonicalKey); } @Override @@ -131,4 +133,16 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { if (player == null) return false; return data.activateAbility(player, key); } + + /** + * Resolve a user-provided key to the canonical storage key. + * Uses the same resolution as unlockAbility: registry key for built-in, UUID for custom. + */ + private String resolveCanonicalKey(String key) { + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability != null && ability.getId() != null) { + return ability.getId(); + } + return key; + } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 347997210..51873686d 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -122,6 +122,7 @@ gui.copy=Copy gui.clear=Clear gui.builtin=Built-in gui.custom=Custom +gui.chained=Chained gui.builtinAnimations=Built-in Animations gui.customAnimations=Custom Animations gui.done=Done @@ -1688,6 +1689,13 @@ ability.minRange=Min Range ability.maxRange=Max Range ability.conditions=Conditions +# Chained Ability +ability.select=Select Ability +ability.entries=Entries +ability.delay=Delay +ability.windUpAll=Wind Up All +ability.allowedBy=Allowed By + # Effects Tab ability.windUpSound=Wind Up Sound ability.activeSound=Active Sound @@ -2030,6 +2038,16 @@ ability.variant.large=Large ability.variant.static=Static ability.variant.launched=Launched +ability.addChain=Chain +ability.entrySource=Add Entry Source +ability.fromNpc=From NPC +ability.byReference=By Reference +ability.createNew=Create New +ability.selectSlot=Select NPC Ability +ability.loadType=Load Type +ability.loadAbility=Load Ability +ability.loadChain=Load Chain + gui.both=Both gui.players=Players