diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 13101f599..4356ac271 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -7,6 +7,7 @@ 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.IChainedAbilityFieldProvider; import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.ability.type.*; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam; @@ -46,6 +47,7 @@ public class AbilityController implements IAbilityHandler { // ── Extension Points ───────────────────────────────────────────────────── private final Map> externalVariants = new LinkedHashMap<>(); private final List fieldProviders = new ArrayList<>(); + private final List chainedFieldProviders = new ArrayList<>(); private final List extenders = new ArrayList<>(); private final List> flightCheckers = new ArrayList<>(); @@ -807,6 +809,14 @@ public class AbilityController implements IAbilityHandler { return fieldProviders; } + public void registerChainedFieldProvider(IChainedAbilityFieldProvider provider) { + chainedFieldProviders.add(provider); + } + + public List getChainedFieldProviders() { + return chainedFieldProviders; + } + public void registerExtender(IAbilityExtender extender) { extenders.add(extender); } 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 17b4f89d9..eff65e8a9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java @@ -44,15 +44,15 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { /** Maximum range for NPC eligibility check. */ private float maxRange = 20; - /** Which entity types can use this chained ability. */ - private UserType allowedBy = UserType.BOTH; - /** Chain-level conditions (individual ability conditions are ignored). */ private List conditions = new ArrayList<>(); /** Ordered list of ability entries to execute. */ private List entries = new ArrayList<>(); + /** Extension data for external mods (e.g., icons, DBC stats). */ + private NBTTagCompound customData = new NBTTagCompound(); + public ChainedAbility() {} public ChainedAbility(String name) { @@ -145,12 +145,27 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { this.maxRange = Math.max(0, maxRange); } + /** + * Compute the allowed user type from child abilities. + * A chain allows players if ALL resolved entries allow players. + * A chain allows NPCs if ALL resolved entries allow NPCs. + * Unresolvable entries are skipped (don't restrict). + */ public UserType getAllowedBy() { - return allowedBy; - } - - public void setAllowedBy(UserType allowedBy) { - this.allowedBy = allowedBy != null ? allowedBy : UserType.BOTH; + if (entries.isEmpty()) return UserType.BOTH; + boolean allAllowPlayer = true; + boolean allAllowNpc = true; + for (ChainedAbilityEntry entry : entries) { + Ability a = entry.resolve(); + if (a == null) continue; + UserType ut = a.getAllowedBy(); + if (!ut.allowsPlayer()) allAllowPlayer = false; + if (!ut.allowsNpc()) allAllowNpc = false; + } + if (allAllowPlayer && allAllowNpc) return UserType.BOTH; + if (allAllowPlayer) return UserType.PLAYER_ONLY; + if (allAllowNpc) return UserType.NPC_ONLY; + return UserType.NPC_ONLY; } public List getConditions() { @@ -169,6 +184,10 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { this.entries = entries != null ? entries : new ArrayList<>(); } + public NBTTagCompound getCustomData() { + return customData; + } + // ═══════════════════════════════════════════════════════════════════ // IChainedAbility INTERFACE // ═══════════════════════════════════════════════════════════════════ @@ -343,7 +362,6 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { nbt.setInteger("CooldownTicks", cooldownTicks); nbt.setFloat("MinRange", minRange); nbt.setFloat("MaxRange", maxRange); - nbt.setInteger("AllowedBy", allowedBy.ordinal()); // Conditions NBTTagList condList = new NBTTagList(); @@ -359,6 +377,9 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { } nbt.setTag("Entries", entryList); + // Custom data (for external mods) + nbt.setTag("customData", (NBTTagCompound) customData.copy()); + return nbt; } @@ -372,7 +393,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { cooldownTicks = nbt.hasKey("CooldownTicks") ? nbt.getInteger("CooldownTicks") : 100; minRange = nbt.getFloat("MinRange"); maxRange = nbt.hasKey("MaxRange") ? nbt.getFloat("MaxRange") : 20; - allowedBy = UserType.fromOrdinal(nbt.getInteger("AllowedBy")); + // AllowedBy is computed from child abilities — old NBT data silently ignored // Conditions conditions.clear(); @@ -393,5 +414,8 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { entries.add(entry); } } + + // Custom data (for external mods) + customData = nbt.getCompoundTag("customData"); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IChainedAbilityFieldProvider.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IChainedAbilityFieldProvider.java new file mode 100644 index 000000000..95e8b70e0 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IChainedAbilityFieldProvider.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.client.gui.builder.FieldDef; + +import java.util.List; + +/** + * External field definition provider for chained abilities. + * Called during GUI initialization to allow mods to inject custom tabs/fields + * into the chained ability configuration GUI. + *

+ * Note: Although this interface references FieldDef (client-only), it is safe to + * register on both sides because the generic type is erased at bytecode level. + * The addFieldDefinitions method is only ever called from client-side GUI code. + */ +public interface IChainedAbilityFieldProvider { + /** + * Add field definitions to the chained ability's GUI. + * Fields added here should use .tab() to assign them to a custom tab. + * + * @param chain The chained ability being configured + * @param defs The mutable list of field definitions to add to + */ + void addFieldDefinitions(ChainedAbility chain, List defs); +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java index 83a091a87..ccef30b0d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -6,6 +6,7 @@ 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.IChainedAbilityFieldProvider; import kamkeel.npcs.controllers.data.ability.UserType; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; @@ -25,11 +26,14 @@ import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * SubGui for editing a {@link ChainedAbility}. - * Three tabs: General, Entries, Conditions. + * Core tabs: General, Entries, Conditions. + * Additional tabs are injected dynamically by {@link IChainedAbilityFieldProvider}s. */ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener, IAbilityConfigCallback { @@ -38,15 +42,17 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText private static final int CLEAR_ID_START = 2000; private static final int LABEL_ID_START = 3000; - // ── Tab constants ───────────────────────────────────────────────────────── + // ── Tab constants (core tabs) ──────────────────────────────────────────── private static final int TAB_GENERAL = 0; private static final int TAB_ENTRIES = 1; private static final int TAB_CONDITIONS = 2; + private static final int CORE_TAB_COUNT = 3; // ── 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_TAB_EXTRA_BASE = 93; private static final int BTN_CLOSE = -1000; // ── Entries tab: per-entry ID encoding ───────────────────────────────────── @@ -80,8 +86,11 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText private List entries; private List conditions; + // Dynamic tabs injected by field providers (e.g., "Icon") + private List extraTabs = new ArrayList<>(); + private GuiFieldBuilder builder; - private float[] tabScrollY = new float[3]; + private float[] tabScrollY; // Entry editing private int editingEntryIndex = -1; @@ -128,7 +137,57 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText 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")); + + // Computed compatibility label (derived from child abilities) + fieldDefs.add(FieldDef.labelField("ability.compatible", () -> { + UserType ut = computeAllowedBy(); + return StatCollector.translateToLocal("ability.compatible") + ": \u00A7e" + ut.name(); + }).tab("General")); + + // External field providers (e.g., DBC Addon injecting an "Icon" tab) + if (AbilityController.Instance != null) { + for (IChainedAbilityFieldProvider provider : AbilityController.Instance.getChainedFieldProviders()) { + provider.addFieldDefinitions(chain, fieldDefs); + } + } + + // Collect extra tab names from provider-injected fields + Set tabNames = new LinkedHashSet<>(); + for (FieldDef def : fieldDefs) { + String tab = def.getTab(); + if (tab != null && !tab.isEmpty() && !"General".equals(tab)) { + tabNames.add(tab); + } + } + extraTabs = new ArrayList<>(tabNames); + + // Size scroll array for all tabs + tabScrollY = new float[CORE_TAB_COUNT + extraTabs.size()]; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // COMPUTED USERTYPE + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Compute the allowed UserType from the current (possibly modified) entries list. + * Mirrors ChainedAbility.getAllowedBy() but uses the local entries. + */ + private UserType computeAllowedBy() { + if (entries.isEmpty()) return UserType.BOTH; + boolean allAllowPlayer = true; + boolean allAllowNpc = true; + for (ChainedAbilityEntry entry : entries) { + Ability a = entry.resolve(); + if (a == null) continue; + UserType ut = a.getAllowedBy(); + if (!ut.allowsPlayer()) allAllowPlayer = false; + if (!ut.allowsNpc()) allAllowNpc = false; + } + if (allAllowPlayer && allAllowNpc) return UserType.BOTH; + if (allAllowPlayer) return UserType.PLAYER_ONLY; + if (allAllowNpc) return UserType.NPC_ONLY; + return UserType.NPC_ONLY; } // ═══════════════════════════════════════════════════════════════════════════ @@ -145,7 +204,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText super.initGui(); - // Tab buttons + // Tab buttons — core tabs GuiMenuTopButton generalTab = new GuiMenuTopButton(BTN_TAB_GENERAL, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); @@ -158,6 +217,16 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText conditionsTab.active = (activeTab == TAB_CONDITIONS); addTopButton(conditionsTab); + // Dynamic extra tabs from field providers + GuiMenuTopButton prevTab = conditionsTab; + for (int i = 0; i < extraTabs.size(); i++) { + int tabIndex = CORE_TAB_COUNT + i; + GuiMenuTopButton extraTab = new GuiMenuTopButton(BTN_TAB_EXTRA_BASE + i, prevTab, extraTabs.get(i)); + extraTab.active = (activeTab == tabIndex); + addTopButton(extraTab); + prevTab = extraTab; + } + GuiMenuTopButton closeBtn = new GuiMenuTopButton(BTN_CLOSE, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); @@ -168,22 +237,27 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText int swH = ySize - 10; if (activeTab == TAB_GENERAL) { - buildGeneralTab(swX, swY, swW, swH); + buildFieldDefTab("General", swX, swY, swW, swH); } else if (activeTab == TAB_ENTRIES) { buildEntriesTab(swX, swY, swW, swH); } else if (activeTab == TAB_CONDITIONS) { buildConditionsTab(swX, swY, swW, swH); + } else if (activeTab >= CORE_TAB_COUNT) { + int extraIndex = activeTab - CORE_TAB_COUNT; + if (extraIndex < extraTabs.size()) { + buildFieldDefTab(extraTabs.get(extraIndex), swX, swY, swW, swH); + } } } // ═══════════════════════════════════════════════════════════════════════════ - // GENERAL TAB + // FIELDDEF-BASED TAB (General + dynamic provider tabs) // ═══════════════════════════════════════════════════════════════════════════ - private void buildGeneralTab(int swX, int swY, int swW, int swH) { + private void buildFieldDefTab(String tabName, int swX, int swY, int swW, int swH) { List tabFields = new ArrayList<>(); for (FieldDef def : fieldDefs) { - if ("General".equals(def.getTab()) && def.isVisible()) { + if (tabName.equals(def.getTab()) && def.isVisible()) { tabFields.add(def); } } @@ -342,12 +416,19 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - // Tab switching + // Tab switching — core tabs 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; } + // Tab switching — extra tabs + if (id >= BTN_TAB_EXTRA_BASE && id < BTN_TAB_EXTRA_BASE + extraTabs.size()) { + activeTab = CORE_TAB_COUNT + (id - BTN_TAB_EXTRA_BASE); + initGui(); + return; + } + // Entries tab if (activeTab == TAB_ENTRIES) { if (handleEntryButton(id)) return; @@ -358,8 +439,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText if (handleConditionButton(id)) return; } - // General tab — declarative field handling - if (activeTab == TAB_GENERAL && builder != null) { + // FieldDef-based tabs (General + extra tabs) — declarative field handling + if (isFieldDefTab() && builder != null) { if (builder.handleButtonEvent(id, guibutton)) { if (!hasSubGui()) { initGui(); @@ -369,6 +450,13 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText } } + /** + * Whether the current active tab uses the declarative FieldDef system. + */ + private boolean isFieldDefTab() { + return activeTab == TAB_GENERAL || activeTab >= CORE_TAB_COUNT; + } + private boolean handleEntryButton(int id) { // Add entry if (id == BTN_ADD_ENTRY) { @@ -456,8 +544,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText public void unFocused(GuiNpcTextField textField) { int id = textField.id; - // General tab — declarative fields - if (activeTab == TAB_GENERAL && builder != null && id >= DECLARATIVE_ID_START) { + // FieldDef-based tabs — declarative fields + if (isFieldDefTab() && builder != null && id >= DECLARATIVE_ID_START) { if (builder.handleTextFieldEvent(id, textField)) { initGui(); } @@ -479,8 +567,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText @Override public void subGuiClosed(SubGuiInterface subgui) { - // General tab — declarative sub-gui handling - if (activeTab == TAB_GENERAL && builder != null) { + // FieldDef-based tabs — declarative sub-gui handling + if (isFieldDefTab() && builder != null) { if (builder.handleSubGuiClosed(subgui)) { initGui(); return; diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index ae24a9ae9..8d7ef3ed4 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -78,7 +78,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe int totalBtnWidth = btnWidth * 2 + btnSpacing; int btnX = centerX - totalBtnWidth / 2; - addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.placeBid")); + addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.place")); addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 51873686d..2778a1ec7 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -234,7 +234,7 @@ party.partying=Partying! party.messageNoParty=You're not in a party! Either wait for an invite or create one. party.partyRequirements=Required by party.setQuestConfirm=Setting a Quest to a party will clear its progress! -party.dropQuestConfirm=Are you sure you want to drop the party quest?` +party.dropQuestConfirm=Are you sure you want to drop the party quest? settings.chatBubbles=Chat Bubbles settings.trackAlignment=Tracking Alignment @@ -483,9 +483,9 @@ stock.reset.never=Never stock.reset.mcdaily=MC Daily stock.reset.mcweekly=MC Weekly stock.reset.mccustom=MC Custom -stock.reset.rldaily=Real Daily -stock.reset.rlweekly=Real Weekly -stock.reset.rlcustom=Real Custom +stock.reset.rldaily=RL Daily +stock.reset.rlweekly=RL Weekly +stock.reset.rlcustom=RL Custom stock.options.hover=Configure stock limits and reset timers stock.enable.hover=Enable limited stock for trade slots @@ -534,7 +534,7 @@ stats.respawn=Respawn stats.deadbody=HideDeadBody stats.fireimmune=ImmuneToFire stats.candrown=Drowns -stats.firesound:=SoundFX +stats.firesound=SoundFX stats.creaturetype=CreatureType stats.normal=Normal stats.reverse=Reverse @@ -1518,17 +1518,13 @@ ability.globalCooldown=Global Cooldown ability.minCooldown=Min CD ability.maxCooldown=Max CD ability.edit=Edit Ability -ability.weight=Weight ability.targeting=Targeting -ability.minRange=Min Range -ability.maxRange=Max Range ability.timing=Timing (ticks) ability.cooldown=Cooldown ability.windup=Wind Up ability.syncOn=Sync: On ability.syncOff=Sync: Off ability.dazed=Dazed Ticks -ability.interruptible=Interruptible ability.threshold=Threshold ability.lockMove=Lock Move ability.lockMove.windup=Windup @@ -1637,15 +1633,10 @@ ability.tab.timing=Timing ability.tab.effects=Effects ability.tab.telegraph=Telegraph ability.tab.visual=Visual -ability.showTelegraph=Show Telegraph ability.telegraphColor=Color ability.telegraphSizeNote=Size is based on ability parameters # Ability Effects Tab -ability.windUpSound=Wind Up Sound -ability.activeSound=Active Sound -ability.windUpAnim=Wind Up Anim -ability.activeAnim=Active Anim ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit @@ -1750,7 +1741,6 @@ gui.radius=Radius gui.thickness=Thickness gui.rotationSpeed=Rotation Speed gui.strength=Strength -gui.range=Range gui.delay=Delay gui.density=Density gui.length=Length @@ -2134,7 +2124,6 @@ auction.filter.search=Search auction.bid.minimum=Minimum Bid: %s auction.bid.yourBid=Your Bid: auction.bid.place=Place Bid -auction.bid.placeBid=Place Bid auction.bid.buyout=Buyout Now # Errors @@ -2206,7 +2195,6 @@ auction.trades.leftClickRebid=Left-click to rebid auction.trades.rightClickRefund=Right-click to claim refund auction.claim.soldItem=Item: %s auction.claim.buyer=Buyer: %s -auction.claim.outbidItem=Item: %s auction.claim.outbidBy=Outbid by: %s # Sell Page - Buyout Toggle @@ -2233,9 +2221,3 @@ auction.bid.yourBalance=Your Balance: %s # Auction Buyout SubGui auction.buyout.title=Buy Now auction.buyout.confirm=Purchase this item for: - -# Auction Status -auction.ended=Ended - -# Role -role.auctioneer=Auctioneer