From 6514b217b91ebfc1319a7a192eeb9491705b6551 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 17 Feb 2026 23:08:12 -0500 Subject: [PATCH] Abilities are now Stored by Name not UUID. Refactor Built In and Toggleable Abilities --- src/api | 2 +- .../kamkeel/npcs/command/AbilityCommand.java | 26 ++-- .../controllers/data/ability/Ability.java | 134 ++++++++++++++-- .../data/ability/AbilityController.java | 143 +++++++++++++----- .../data/ability/BuiltInAbility.java | 61 -------- .../controllers/data/ability/ToggleEntry.java | 27 ++++ .../noppes/npcs/AbstractDataAbilities.java | 137 ++++++++++++++++- src/main/java/noppes/npcs/DataAbilities.java | 22 +++ .../gui/global/GuiNpcManageAbilities.java | 9 +- .../controllers/data/PlayerAbilityData.java | 25 +++ .../scripted/wrapper/ScriptDataAbilities.java | 4 +- .../wrapper/ScriptPlayerAbilityData.java | 2 +- 12 files changed, 465 insertions(+), 127 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java diff --git a/src/api b/src/api index 22bce4b2a..010849fe8 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 22bce4b2acc9cf9a120a7fdddbb62cc4e567c796 +Subproject commit 010849fe8fb67c0359d7ebdb7bcd5c3c957fe3db diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 10df3cdbe..3f90f9332 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -117,7 +117,8 @@ public class AbilityCommand extends CommandKamkeelBase { return; } - sendResult(sender, "Ability: \u00A7b" + ability.getName()); + sendResult(sender, "Ability: \u00A7b" + ability.getDisplayName()); + sendResult(sender, " Name: \u00A77" + ability.getName()); sendResult(sender, " Type: \u00A7d" + ability.getTypeId()); sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks"); sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks"); @@ -188,12 +189,12 @@ public class AbilityCommand extends CommandKamkeelBase { return; } - // Use the ability's canonical ID for storage (registry key for built-in, name for custom) + // 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 } - String displayName = ability.getName() != null ? ability.getName() : canonicalKey; + String displayName = ability.getDisplayName(); // Check if ability allows players if (!ability.getAllowedBy().allowsPlayer()) { @@ -232,13 +233,13 @@ public class AbilityCommand extends CommandKamkeelBase { return; } - // Resolve ability to get canonical key + // 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; if (ability != null) { canonicalKey = ability.getId() != null ? ability.getId() : abilityKey; - displayName = ability.getName() != null ? ability.getName() : canonicalKey; + displayName = ability.getDisplayName(); } // Get player data and lock @@ -284,7 +285,7 @@ public class AbilityCommand extends CommandKamkeelBase { for (int i = 0; i < abilityKeys.length; i++) { String key = abilityKeys[i]; Ability ability = AbilityController.Instance.resolveAbility(key); - String displayName = ability != null && ability.getName() != null ? ability.getName() : key; + String displayName = ability != null ? ability.getDisplayName() : key; String prefix = (i == selected) ? "\u00A7e> " : " "; // Show display name and key if different if (!displayName.equals(key)) { @@ -306,12 +307,17 @@ public class AbilityCommand extends CommandKamkeelBase { /** * Get list of player-usable ability names (for tab completion). - * Uses registry keys for built-in abilities and display names for custom abilities. + * Resolves UUIDs to names so tab completion shows human-readable names. * Used by CommandKamkeel for usage token. */ public static List getPlayerAbilityNames() { - List keys = new ArrayList<>(AbilityController.Instance.getPlayerAbilityKeys()); - Collections.sort(keys); - return keys; + Set keys = AbilityController.Instance.getPlayerAbilityKeys(); + List names = new ArrayList<>(); + for (String key : keys) { + Ability a = AbilityController.Instance.resolveAbility(key); + names.add(a != null ? a.getName() : key); + } + Collections.sort(names); + return names; } } 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 891558b96..14047b0cf 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -44,9 +44,10 @@ public abstract class Ability implements IAbility { // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ - protected String id; - protected String name; - protected String typeId; // e.g., "ability.cnpc.slam" (also used as lang key) + protected String id = ""; // UUID for custom, registry key for built-in + protected String name = ""; // Unique file key / identifier + protected String displayName = ""; // Cosmetic name (falls back to name when empty) + protected String typeId = ""; // e.g., "ability.cnpc.slam" (also used as lang key) // Selection protected int weight = 10; @@ -102,6 +103,23 @@ public abstract class Ability implements IAbility { // Cooldown override protected boolean ignoreCooldown = false; + // ═══════════════════════════════════════════════════════════════════ + // BUILT-IN (set in constructor via configureAsBuiltIn, NOT persisted) + // ═══════════════════════════════════════════════════════════════════ + + protected transient boolean builtIn = false; + protected transient String registryKey; + + // ═══════════════════════════════════════════════════════════════════ + // TOGGLE CONFIGURATION (saved to NBT for custom abilities) + // ═══════════════════════════════════════════════════════════════════ + + /** Whether this ability is a toggle (ON/OFF) rather than a phased execution */ + protected boolean toggleable = false; + + /** Whether the toggled-ON state ticks each game tick (e.g., Kaioken drains ki per tick) */ + protected boolean hasActiveToggle = false; + // Configurable potion effects protected List effects = new ArrayList<>(); @@ -205,6 +223,31 @@ public abstract class Ability implements IAbility { return false; } + // ═══════════════════════════════════════════════════════════════════ + // TOGGLE CALLBACKS (optional overrides for toggleable abilities) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Called when this toggle ability is turned ON for an entity. + * Override to apply effects (e.g., set a DBC flag, apply a buff). + */ + public void onToggleOn(EntityLivingBase caster) {} + + /** + * Called when this toggle ability is turned OFF for an entity. + * Override to remove effects (e.g., clear a DBC flag, remove a buff). + */ + public void onToggleOff(EntityLivingBase caster) {} + + /** + * Called every tick while this toggle is active (only if hasActiveToggle is true). + * Override for per-tick effects (e.g., ki drain for Kaioken). + * @return true to keep the toggle active, false to auto-deactivate + */ + public boolean onToggleTick(EntityLivingBase caster, int tickCount) { + return true; + } + /** * Whether this ability is ready for auto-completion during burst overlap mode. * Override to delay auto-completion until all staggered projectiles have been fired. @@ -483,6 +526,8 @@ public abstract class Ability implements IAbility { // ── General tab ────────────────────────────────────────────── defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) .tab("General")); + defs.add(FieldDef.stringField("gui.displayName", this::getRawDisplayName, this::setDisplayName) + .tab("General")); defs.add(FieldDef.row( FieldDef.intField("ability.weight", this::getWeight, this::setWeight).range(1, 1000), FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) @@ -1119,6 +1164,7 @@ public abstract class Ability implements IAbility { NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("id", id); nbt.setString("name", name); + nbt.setString("displayName", displayName); nbt.setString("typeId", typeId); nbt.setInteger("weight", weight); nbt.setBoolean("enabled", enabled); @@ -1150,6 +1196,10 @@ public abstract class Ability implements IAbility { nbt.setInteger("allowedBy", allowedBy.ordinal()); nbt.setBoolean("ignoreCooldown", ignoreCooldown); + // Toggle + nbt.setBoolean("toggleable", toggleable); + nbt.setBoolean("hasActiveToggle", hasActiveToggle); + // Burst nbt.setBoolean("burstEnabled", burstEnabled); nbt.setInteger("burstAmount", burstAmount); @@ -1169,10 +1219,12 @@ public abstract class Ability implements IAbility { } nbt.setTag("effects", effectList); - // Type-specific - NBTTagCompound typeNBT = new NBTTagCompound(); - writeTypeNBT(typeNBT); - nbt.setTag("typeData", typeNBT); + // Type-specific (skip for built-in abilities with fixed configs) + if (!builtIn) { + NBTTagCompound typeNBT = new NBTTagCompound(); + writeTypeNBT(typeNBT); + nbt.setTag("typeData", typeNBT); + } return nbt; } @@ -1180,6 +1232,7 @@ public abstract class Ability implements IAbility { public void readNBT(NBTTagCompound nbt) { id = nbt.getString("id"); name = nbt.getString("name"); + displayName = nbt.getString("displayName"); typeId = nbt.getString("typeId"); weight = nbt.getInteger("weight"); enabled = nbt.getBoolean("enabled"); @@ -1215,6 +1268,10 @@ public abstract class Ability implements IAbility { allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy")); ignoreCooldown = nbt.getBoolean("ignoreCooldown"); + // Toggle + toggleable = nbt.getBoolean("toggleable"); + hasActiveToggle = nbt.getBoolean("hasActiveToggle"); + // Burst burstEnabled = nbt.getBoolean("burstEnabled"); burstAmount = nbt.getInteger("burstAmount"); @@ -1242,7 +1299,10 @@ public abstract class Ability implements IAbility { } } - readTypeNBT(nbt.getCompoundTag("typeData")); + // Type-specific (skip for built-in abilities with fixed configs) + if (!builtIn) { + readTypeNBT(nbt.getCompoundTag("typeData")); + } } // ═══════════════════════════════════════════════════════════════════ @@ -1265,12 +1325,52 @@ public abstract class Ability implements IAbility { this.name = name; } + /** + * Get the display name for this ability. + * Returns displayName if set, otherwise falls back to name. + */ + public String getDisplayName() { + return (displayName != null && !displayName.isEmpty()) ? displayName : name; + } + + /** + * Get the raw display name (may be empty). + */ + public String getRawDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName != null ? displayName : ""; + } + public String getTypeId() { return typeId; } public boolean isBuiltIn() { - return false; + return builtIn; + } + + /** + * Get the registry key for built-in abilities. + * Returns null for custom abilities. + */ + public String getRegistryKey() { + return registryKey; + } + + /** + * Configure this ability as a built-in ability with fixed config. + * Sets builtIn flag, assigns registry key, id, name, and derives typeId. + * Call this in the constructor of any ability that should be non-customizable. + */ + protected void configureAsBuiltIn(String registryKey) { + this.builtIn = true; + this.registryKey = registryKey; + this.id = registryKey; + this.name = registryKey; + this.typeId = "ability." + registryKey.replace(':', '.'); } public Ability deepCopy() { @@ -1727,6 +1827,22 @@ public abstract class Ability implements IAbility { this.ignoreCooldown = ignoreCooldown; } + public boolean isToggleable() { + return toggleable; + } + + public void setToggleable(boolean toggleable) { + this.toggleable = toggleable; + } + + public boolean hasActiveToggle() { + return hasActiveToggle; + } + + public void setHasActiveToggle(boolean hasActiveToggle) { + this.hasActiveToggle = hasActiveToggle; + } + public List getEffects() { return effects; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 406d66c12..d26f9e0cd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -33,7 +33,8 @@ public class AbilityController implements IAbilityHandler { // ── Core Registries ────────────────────────────────────────────────────── private final Map> abilityTypes = new LinkedHashMap<>(); private final Map builtAbilities = new LinkedHashMap<>(); - private final Map customAbilities = new LinkedHashMap<>(); + private final Map customAbilities = new LinkedHashMap<>(); // name → Ability + private final Map customAbilitiesById = new LinkedHashMap<>(); // UUID → Ability // ── Extension Points ───────────────────────────────────────────────────── private final Map> externalVariants = new LinkedHashMap<>(); @@ -162,6 +163,7 @@ public class AbilityController implements IAbilityHandler { public void load() { customAbilities.clear(); + customAbilitiesById.clear(); int migrated = 0; File dir = getDir(); @@ -177,6 +179,13 @@ public class AbilityController implements IAbilityHandler { Ability ability = fromNBT(nbt); if (ability == null) continue; + // Skip built-in abilities that were erroneously saved as custom files + if (ability.isBuiltIn()) { + file.delete(); + LogWriter.info("Removed stale built-in ability file: " + filename); + continue; + } + // Legacy migration: UUID-named files get renamed to name-based if (UUID_PATTERN.matcher(key).matches()) { String name = ability.getName(); @@ -202,8 +211,24 @@ public class AbilityController implements IAbilityHandler { migrated++; } - ability.setId(key); + // Ensure ability has a UUID; generate one for legacy abilities + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty() || uuid.equals(key)) { + // Legacy: id was the name or empty — generate a real UUID + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); + // Re-save file with the new UUID + try { + NBTJsonUtil.SaveFile(file, ability.writeNBT()); + } catch (Exception ex) { + LogWriter.error("Failed to save UUID for ability: " + key, ex); + } + migrated++; + } + + ability.setName(key); customAbilities.put(key, ability); + customAbilitiesById.put(uuid, ability); } catch (Exception e) { LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); } @@ -212,7 +237,7 @@ public class AbilityController implements IAbilityHandler { customAbilityRevision++; if (migrated > 0) { - LogWriter.info("Migrated " + migrated + " UUID-named abilities to name-based files"); + LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)"); } LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); } @@ -223,42 +248,50 @@ public class AbilityController implements IAbilityHandler { public boolean saveCustomAbility(Ability ability) { if (ability == null) return false; + if (ability.isBuiltIn()) return false; String name = ability.getName(); if (name == null || name.isEmpty()) return false; - // Ensure unique name for new abilities (no existing id) - String oldName = ability.getId(); - boolean isNew = oldName == null || oldName.isEmpty(); + String uuid = ability.getId(); + boolean isNew = (uuid == null || uuid.isEmpty()); + if (isNew) { + // Generate UUID for new ability + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); + + // Ensure unique name while (hasCustomAbilityName(name)) { name = name + "_"; } ability.setName(name); + } else { + // Existing ability — check for rename via UUID lookup + Ability existing = customAbilitiesById.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 (customAbilities.containsKey(testName) && !testName.equals(oldName)) { + testName = testName + "_"; + } + if (!testName.equals(name)) { + name = testName; + ability.setName(name); + } + + // Delete old file and remove old map entry + File oldFile = new File(getDir(), oldName + ".json"); + if (oldFile.exists()) { + oldFile.delete(); + } + customAbilities.remove(oldName); + } + } } - // Handle rename: old id (previous name) differs from current name - if (!isNew && !oldName.equals(name)) { - // Check uniqueness of new name (excluding the entry being renamed) - String testName = name; - while (customAbilities.containsKey(testName) && !testName.equals(oldName)) { - testName = testName + "_"; - } - if (!testName.equals(name)) { - name = testName; - ability.setName(name); - } - - // Delete old file and remove old map entry - File oldFile = new File(getDir(), oldName + ".json"); - if (oldFile.exists()) { - oldFile.delete(); - } - customAbilities.remove(oldName); - } - - ability.setId(name); - File dir = getDir(); File fileNew = new File(dir, name + ".json_new"); File fileCurrent = new File(dir, name + ".json"); @@ -276,8 +309,9 @@ public class AbilityController implements IAbilityHandler { } customAbilities.put(name, ability); + customAbilitiesById.put(uuid, ability); customAbilityRevision++; - LogWriter.info("Saved custom ability: " + name); + LogWriter.info("Saved custom ability: " + name + " [" + uuid + "]"); SyncController.syncAllCustomAbilities(); return true; } catch (Exception e) { @@ -292,6 +326,12 @@ public class AbilityController implements IAbilityHandler { Ability removed = customAbilities.remove(name); if (removed == null) return false; + // Also remove from UUID index + String uuid = removed.getId(); + if (uuid != null && !uuid.isEmpty()) { + customAbilitiesById.remove(uuid); + } + File dir = getDir(); File file = new File(dir, name + ".json"); if (file.exists()) { @@ -318,7 +358,14 @@ public class AbilityController implements IAbilityHandler { public void setCustomAbilities(Map synced) { customAbilities.clear(); + customAbilitiesById.clear(); customAbilities.putAll(synced); + for (Ability ability : synced.values()) { + String uuid = ability.getId(); + if (uuid != null && !uuid.isEmpty()) { + customAbilitiesById.put(uuid, ability); + } + } customAbilityRevision++; } @@ -341,6 +388,10 @@ public class AbilityController implements IAbilityHandler { public Ability resolveAbility(String key) { if (key == null || key.isEmpty()) return null; + // Custom: UUID lookup (most common for persistent references) + Ability byUuid = customAbilitiesById.get(key); + if (byUuid != null) return byUuid.deepCopy(); + // Built-in: exact name Ability builtIn = builtAbilities.get(key); if (builtIn != null) return builtIn.deepCopy(); @@ -359,7 +410,7 @@ public class AbilityController implements IAbilityHandler { } } - // Custom: exact name + // Custom: exact name (fallback for legacy references) Ability custom = customAbilities.get(key); if (custom != null) return custom.deepCopy(); @@ -381,11 +432,11 @@ public class AbilityController implements IAbilityHandler { keys.add(id); } } - // Custom abilities keyed by name + // Custom abilities keyed by UUID (stable references) for (Ability ability : customAbilities.values()) { - String name = ability.getName(); - if (name != null && !name.isEmpty()) { - keys.add(name); + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); } } return keys; @@ -401,12 +452,12 @@ public class AbilityController implements IAbilityHandler { } } } - // Custom abilities keyed by name + // Custom abilities keyed by UUID (stable references) for (Ability ability : customAbilities.values()) { if (ability.getAllowedBy().allowsPlayer()) { - String name = ability.getName(); - if (name != null && !name.isEmpty()) { - keys.add(name); + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); } } } @@ -414,7 +465,16 @@ public class AbilityController implements IAbilityHandler { } public boolean hasAbility(String key) { - return builtAbilities.containsKey(key) || customAbilities.containsKey(key); + return builtAbilities.containsKey(key) || customAbilities.containsKey(key) + || customAbilitiesById.containsKey(key); + } + + public Ability getCustomAbilityByUUID(String uuid) { + return customAbilitiesById.get(uuid); + } + + public Ability getCustomAbilityByName(String name) { + return customAbilities.get(name); } /** @@ -424,6 +484,9 @@ public class AbilityController implements IAbilityHandler { public boolean canResolveAbility(String key) { if (key == null || key.isEmpty()) return false; + // Custom: UUID lookup (most common for persistent references) + if (customAbilitiesById.containsKey(key)) return true; + // Built-in: exact name if (builtAbilities.containsKey(key)) return true; @@ -437,7 +500,7 @@ public class AbilityController implements IAbilityHandler { if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true; } - // Custom: exact name + // Custom: exact name (fallback for legacy references) if (customAbilities.containsKey(key)) return true; // Custom: case-insensitive name diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java deleted file mode 100644 index 36344e9e5..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java +++ /dev/null @@ -1,61 +0,0 @@ -package kamkeel.npcs.controllers.data.ability; - -import net.minecraft.nbt.NBTTagCompound; - -/** - * Abstract base class for built-in (non-customizable) abilities. - * Built-in abilities are registered by key, have fixed configurations, - * and cannot be modified through the preset GUI system. - *

- * Examples: simple utility abilities like Jump, Sprint, etc. - * Third-party mods can extend this class to create their own built-in abilities. - */ -public abstract class BuiltInAbility extends Ability { - - /** - * The registry key used to look up this ability (e.g., "cnpc:jump", "dbc:ki_blast"). - */ - private final String registryKey; - - public BuiltInAbility(String registryKey) { - this.registryKey = registryKey; - this.id = registryKey; - this.name = registryKey; - // Convert "namespace:name" to "ability.namespace.name" for GUI filter compatibility - this.typeId = "ability." + registryKey.replace(':', '.'); - } - - @Override - public boolean isBuiltIn() { - return true; - } - - /** - * Get the registry key for this built-in ability. - */ - public String getRegistryKey() { - return registryKey; - } - - /** - * Built-in abilities do not write type-specific NBT (they have fixed configs). - */ - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - // No-op: built-in abilities have fixed configurations - } - - /** - * Built-in abilities do not read type-specific NBT. - */ - @Override - public void readTypeNBT(NBTTagCompound nbt) { - // No-op: built-in abilities have fixed configurations - } - - /** - * Get the duration of the active phase in ticks. - * Built-in abilities must define their own active duration. - */ - public abstract int getActiveDurationTicks(); -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java new file mode 100644 index 000000000..e08350601 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java @@ -0,0 +1,27 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Tracks the state of a single active toggle for an entity. + * Stored in AbstractDataAbilities.activeToggles map. + */ +public class ToggleEntry { + private final Ability ability; + private int tickCount; + + public ToggleEntry(Ability ability) { + this.ability = ability; + this.tickCount = 0; + } + + public Ability getAbility() { + return ability; + } + + public int getTickCount() { + return tickCount; + } + + public void incrementTick() { + tickCount++; + } +} diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 774e9661e..9084c7a75 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -3,13 +3,14 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.ToggleEntry; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import net.minecraft.entity.EntityLivingBase; import net.minecraft.util.DamageSource; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; -import java.util.List; +import java.util.*; /** * Shared base class for NPC and Player ability data management. @@ -168,6 +169,140 @@ public abstract class AbstractDataAbilities { return currentAbility; } + // ═══════════════════════════════════════════════════════════════════ + // TOGGLE STATE (active toggles, independent of currentAbility) + // ═══════════════════════════════════════════════════════════════════ + + /** Currently active toggles. Key = ability key, Value = toggle entry with tick counter. */ + protected Map activeToggles = new LinkedHashMap<>(); + + /** + * Toggle an ability ON or OFF. If currently on, turns off; if off, turns on. + * @param key The ability key (e.g., "npcdbc:ki_fist") + * @return true if the toggle is now ON, false if OFF + */ + public boolean toggleAbility(String key) { + if (activeToggles.containsKey(key)) { + deactivateToggle(key); + return false; + } else { + return activateToggle(key); + } + } + + /** + * Set a toggle to a specific state. + * @param key The ability key + * @param on true to activate, false to deactivate + */ + public void setAbilityToggled(String key, boolean on) { + if (on && !activeToggles.containsKey(key)) { + activateToggle(key); + } else if (!on && activeToggles.containsKey(key)) { + deactivateToggle(key); + } + } + + /** + * Check if a toggle ability is currently active. + */ + public boolean isAbilityToggled(String key) { + return activeToggles.containsKey(key); + } + + /** + * Get all active toggle keys. + */ + public Set getActiveToggleKeys() { + return new LinkedHashSet<>(activeToggles.keySet()); + } + + private boolean activateToggle(String key) { + Ability ability = AbilityController.Instance != null + ? AbilityController.Instance.resolveAbility(key) : null; + if (ability == null || !ability.isToggleable()) return false; + + ToggleEntry entry = new ToggleEntry(ability); + activeToggles.put(key, entry); + ability.onToggleOn(getEntity()); + onToggleStateChanged(key, true); + return true; + } + + private void deactivateToggle(String key) { + ToggleEntry entry = activeToggles.remove(key); + if (entry != null) { + entry.getAbility().onToggleOff(getEntity()); + onToggleStateChanged(key, false); + } + } + + /** + * Tick all active toggles. Call from tick() each game tick. + * Toggles with hasActiveToggle=true get their onToggleTick() called. + * If onToggleTick returns false, the toggle is auto-deactivated. + */ + protected void tickActiveToggles() { + if (activeToggles.isEmpty()) return; + + EntityLivingBase entity = getEntity(); + List toRemove = null; + + for (Map.Entry mapEntry : activeToggles.entrySet()) { + ToggleEntry entry = mapEntry.getValue(); + entry.incrementTick(); + + if (entry.getAbility().hasActiveToggle()) { + if (!entry.getAbility().onToggleTick(entity, entry.getTickCount())) { + if (toRemove == null) toRemove = new ArrayList<>(); + toRemove.add(mapEntry.getKey()); + } + } + } + + if (toRemove != null) { + for (String key : toRemove) { + ToggleEntry entry = activeToggles.remove(key); + if (entry != null) { + entry.getAbility().onToggleOff(entity); + onToggleStateChanged(key, false); + } + } + } + } + + /** Hook for subclass to react to toggle state changes (sync packets, script events). */ + protected void onToggleStateChanged(String key, boolean active) {} + + /** + * Clear all active toggles (e.g., on death/reset). + * Calls onToggleOff for each active toggle. + */ + protected void clearActiveToggles() { + if (activeToggles.isEmpty()) return; + EntityLivingBase entity = getEntity(); + for (ToggleEntry entry : activeToggles.values()) { + entry.getAbility().onToggleOff(entity); + } + activeToggles.clear(); + } + + /** + * Add a toggle entry directly without calling onToggleOn. + * Used for client-side sync and NBT restoration. + */ + public void setToggleEntryDirect(String key, boolean active) { + if (active) { + Ability ability = AbilityController.Instance != null + ? AbilityController.Instance.resolveAbility(key) : null; + if (ability != null && ability.isToggleable()) { + activeToggles.put(key, new ToggleEntry(ability)); + } + } else { + activeToggles.remove(key); + } + } + // ═══════════════════════════════════════════════════════════════════ // COOLDOWN MANAGEMENT // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index cb1e5104d..b303a8c52 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -11,6 +11,7 @@ import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; import net.minecraft.util.DamageSource; import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; @@ -297,6 +298,9 @@ public class DataAbilities extends AbstractDataAbilities { if (positionLocked) releaseLockedPosition(); } + // Tick active toggles (independent of currentAbility) + tickActiveToggles(); + // Tick current ability if executing (shared logic from base class) if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); @@ -664,6 +668,7 @@ public class DataAbilities extends AbstractDataAbilities { */ public void reset() { stopCurrentAbility(); + clearActiveToggles(); // Roll cooldown so NPC doesn't immediately attack after reset rollCooldownOnReset(); @@ -983,6 +988,13 @@ public class DataAbilities extends AbstractDataAbilities { } compound.setTag("Abilities", abilityList); + // Active toggles + NBTTagList toggleList = new NBTTagList(); + for (String key : activeToggles.keySet()) { + toggleList.appendTag(new NBTTagString(key)); + } + compound.setTag("ActiveToggles", toggleList); + return compound; } @@ -1000,5 +1012,15 @@ public class DataAbilities extends AbstractDataAbilities { abilitySlots.add(slot); } } + + // Active toggles - restore state directly (no onToggleOn callback during load) + activeToggles.clear(); + if (compound.hasKey("ActiveToggles")) { + NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 8); // 8 = TAG_STRING + for (int i = 0; i < toggleNbt.tagCount(); i++) { + String key = toggleNbt.getStringTagAt(i); + setToggleEntryDirect(key, true); + } + } } } 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 d016e7c77..9378e606b 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -474,9 +474,12 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private boolean hasDuplicateName(Ability ability) { String name = ability.getName(); if (name == null || name.isEmpty()) return false; - String oldName = ability.getId(); - // A duplicate exists if the name is already in use and it's not the same ability being edited - return customData.containsKey(name) && !name.equals(oldName); + if (!customData.containsKey(name)) return false; + // Name exists — check if it belongs to a different ability (by UUID) + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) return true; // New ability with conflicting name + Ability existing = AbilityController.Instance.getCustomAbilityByName(name); + return existing == null || !uuid.equals(existing.getId()); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index f5ffb7028..93215daae 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -263,6 +263,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA return; } + // Tick active toggles (independent of currentAbility) + tickActiveToggles(); + if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); @@ -385,6 +388,11 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } } + @Override + protected void onToggleStateChanged(String key, boolean active) { + syncToClient(); + } + // ═══════════════════════════════════════════════════════════════════ // SELECTION // ═══════════════════════════════════════════════════════════════════ @@ -750,6 +758,13 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA compound.setTag("PlayerAbilities", list); compound.setInteger("PlayerAbilitySelected", selectedIndex); compound.setBoolean("AbilityAnimating", playingAbilityAnimation); + + // Active toggles + NBTTagList toggleList = new NBTTagList(); + for (String key : activeToggles.keySet()) { + toggleList.appendTag(new NBTTagString(key)); + } + compound.setTag("ActiveToggles", toggleList); } public void readFromNBT(NBTTagCompound compound) { @@ -772,6 +787,16 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } playingAbilityAnimation = compound.getBoolean("AbilityAnimating"); + + // Active toggles - restore state directly (no onToggleOn callback during load) + activeToggles.clear(); + if (compound.hasKey("ActiveToggles")) { + NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 8); // 8 = TAG_STRING + for (int i = 0; i < toggleNbt.tagCount(); i++) { + String key = toggleNbt.getStringTagAt(i); + setToggleEntryDirect(key, true); + } + } } /** diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index e27f6e075..b356b55c6 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -61,7 +61,9 @@ public class ScriptDataAbilities implements IDataAbilities { if (!ability.getAllowedBy().allowsNpc()) { return; // Ability is PLAYER_ONLY } - data.addAbilityReference(key); + // Store by UUID/registry key for stable reference + String canonicalKey = ability.getId() != null ? ability.getId() : key; + data.addAbilityReference(canonicalKey); } @Override diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index c1de83878..42ed3372e 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -36,7 +36,7 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { if (!ability.getAllowedBy().allowsPlayer()) { return; // Ability is NPC-only } - // Use canonical key (registry key for built-in, name for custom) + // Use canonical key (registry key for built-in, UUID for custom) String canonicalKey = ability.getId() != null ? ability.getId() : key; data.unlockAbility(canonicalKey); }