From 0e6ece56c621f9d87caf7be13269067cbb71acd8 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 18 Feb 2026 22:19:18 -0500 Subject: [PATCH] Fix DBC Toggle Abilities and Wheel System --- src/api | 2 +- .../controllers/data/ability/Ability.java | 82 ++++++++-- .../controllers/data/ability/ToggleEntry.java | 15 +- .../noppes/npcs/AbstractDataAbilities.java | 147 ++++++++++++++---- src/main/java/noppes/npcs/DataAbilities.java | 34 +++- src/main/java/noppes/npcs/EventHooks.java | 10 ++ .../noppes/npcs/constants/EnumScriptType.java | 2 + .../controllers/data/PlayerAbilityData.java | 44 +++++- .../npcs/scripted/event/AbilityEvent.java | 77 +++++++++ .../event/player/PlayerAbilityEvent.java | 77 +++++++++ .../wrapper/ScriptPlayerAbilityData.java | 22 +++ 11 files changed, 453 insertions(+), 59 deletions(-) diff --git a/src/api b/src/api index ca2980909..2c2f0c0d8 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit ca2980909ee597fa5b062ca945a2cce533277afc +Subproject commit 2c2f0c0d8f21f0ab3a3bc6bea168b3d3b85aa806 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 f93a5c772..9f332e12d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -12,6 +12,7 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.util.StatCollector; @@ -115,15 +116,23 @@ public abstract class Ability implements IAbility, IAbilityAction { // ═══════════════════════════════════════════════════════════════════ /** - * Whether this ability is a toggle (ON/OFF) rather than a phased execution + * Number of toggle states. 0 = not toggleable, 1 = binary on/off, 2+ = multi-state cycling. + * Cycling order: Off(0) -> State 1 -> State 2 -> ... -> State N -> Off(0) */ - protected boolean toggleable = false; + protected int toggleStates = 0; /** * Whether the toggled-ON state ticks each game tick (e.g., Kaioken drains ki per tick) */ protected boolean hasActiveToggle = false; + /** + * Optional display labels for each toggle state (1-indexed). + * Used in ability wheel/hotbar to show current state name. + * Null means no labels. Array length should match toggleStates. + */ + protected String[] toggleStateLabels; + // Configurable potion effects protected List effects = new ArrayList<>(); @@ -232,26 +241,38 @@ public abstract class Ability implements IAbility, IAbilityAction { // ═══════════════════════════════════════════════════════════════════ /** - * Called when this toggle ability is turned ON for an entity. + * Called when this toggle ability is first activated (state goes from 0 to any active state). * 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. + * Called when this toggle ability is fully deactivated (state goes to 0). * Override to remove effects (e.g., clear a DBC flag, remove a buff). */ public void onToggleOff(EntityLivingBase caster) { } + /** + * Called whenever the toggle state changes (including on/off transitions and mid-cycling). + * For multi-state toggles, this fires on every state transition. + * + * @param caster The entity + * @param oldState Previous state (0 = was off) + * @param newState New state (0 = now off, 1+ = active state) + */ + public void onToggleStateChanged(EntityLivingBase caster, int oldState, int newState) { + } + /** * Called every tick while this toggle is active (only if hasActiveToggle is true). * Override for per-tick effects (e.g., ki drain for Kaioken). * + * @param state Current toggle state (1-based) * @return true to keep the toggle active, false to auto-deactivate */ - public boolean onToggleTick(EntityLivingBase caster, int tickCount) { + public boolean onToggleTick(EntityLivingBase caster, int tickCount, int state) { return true; } @@ -1231,8 +1252,15 @@ public abstract class Ability implements IAbility, IAbilityAction { nbt.setBoolean("ignoreCooldown", ignoreCooldown); // Toggle - nbt.setBoolean("toggleable", toggleable); + nbt.setInteger("toggleStates", toggleStates); nbt.setBoolean("hasActiveToggle", hasActiveToggle); + if (toggleStateLabels != null && toggleStateLabels.length > 0) { + NBTTagList labelList = new NBTTagList(); + for (String label : toggleStateLabels) { + labelList.appendTag(new NBTTagString(label != null ? label : "")); + } + nbt.setTag("toggleStateLabels", labelList); + } // Burst nbt.setBoolean("burstEnabled", burstEnabled); @@ -1305,8 +1333,18 @@ public abstract class Ability implements IAbility, IAbilityAction { ignoreCooldown = nbt.getBoolean("ignoreCooldown"); // Toggle - toggleable = nbt.getBoolean("toggleable"); + toggleStates = nbt.getInteger("toggleStates"); hasActiveToggle = nbt.getBoolean("hasActiveToggle"); + if (nbt.hasKey("toggleStateLabels")) { + NBTTagList labelList = nbt.getTagList("toggleStateLabels", 8); + toggleStateLabels = new String[labelList.tagCount()]; + for (int i = 0; i < labelList.tagCount(); i++) { + String label = labelList.getStringTagAt(i); + toggleStateLabels[i] = (label != null && !label.isEmpty()) ? label : null; + } + } else { + toggleStateLabels = null; + } // Burst burstEnabled = nbt.getBoolean("burstEnabled"); @@ -1882,11 +1920,15 @@ public abstract class Ability implements IAbility, IAbilityAction { } public boolean isToggleable() { - return toggleable; + return toggleStates > 0; } - public void setToggleable(boolean toggleable) { - this.toggleable = toggleable; + public int getToggleStates() { + return toggleStates; + } + + public void setToggleStates(int toggleStates) { + this.toggleStates = Math.max(0, toggleStates); } public boolean hasActiveToggle() { @@ -1897,6 +1939,26 @@ public abstract class Ability implements IAbility, IAbilityAction { this.hasActiveToggle = hasActiveToggle; } + /** + * Get the display label for a specific toggle state. + * @param state 1-based state index + * @return the label, or null if none set + */ + public String getToggleStateLabel(int state) { + if (toggleStateLabels != null && state >= 1 && state <= toggleStateLabels.length) { + return toggleStateLabels[state - 1]; + } + return null; + } + + /** + * Set display labels for toggle states (1-indexed). + * @param labels one label per state, null entries allowed + */ + public void setToggleStateLabels(String... labels) { + this.toggleStateLabels = (labels != null && labels.length > 0) ? labels : null; + } + public List getEffects() { return effects; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java index e08350601..4a758bead 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java @@ -3,14 +3,19 @@ package kamkeel.npcs.controllers.data.ability; /** * Tracks the state of a single active toggle for an entity. * Stored in AbstractDataAbilities.activeToggles map. + *

+ * State is 1-based: state 1 = first ON state, state 2 = second, etc. + * State 0 means OFF (entry should not exist in the map). */ public class ToggleEntry { private final Ability ability; private int tickCount; + private int state; - public ToggleEntry(Ability ability) { + public ToggleEntry(Ability ability, int state) { this.ability = ability; this.tickCount = 0; + this.state = Math.max(1, state); } public Ability getAbility() { @@ -24,4 +29,12 @@ public class ToggleEntry { public void incrementTick() { tickCount++; } + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = Math.max(1, state); + } } diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 13d3095c9..717c6c0c9 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -269,41 +269,65 @@ public abstract class AbstractDataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Currently active toggles. Key = ability key, Value = toggle entry with tick counter. + * Currently active toggles. Key = ability key, Value = toggle entry with state and tick counter. */ protected Map activeToggles = new LinkedHashMap<>(); /** - * Toggle an ability ON or OFF. If currently on, turns off; if off, turns on. + * Cycle a toggle ability to its next state. + * Off(0) -> State 1 -> State 2 -> ... -> State N -> Off(0) * * @param key The ability key (e.g., "npcdbc:ki_fist") - * @return true if the toggle is now ON, false if OFF + * @return The new state (0 = off, 1+ = active state number) */ - public boolean toggleAbility(String key) { - if (activeToggles.containsKey(key)) { - deactivateToggle(key); - return false; + public int toggleAbility(String key) { + ToggleEntry existing = activeToggles.get(key); + if (existing != null) { + int currentState = existing.getState(); + int maxStates = existing.getAbility().getToggleStates(); + if (currentState < maxStates) { + return cycleToggleState(key, existing, currentState, currentState + 1); + } else { + deactivateToggle(key); + return 0; + } } else { - return activateToggle(key); + return activateToggle(key, 1) ? 1 : 0; } } /** - * Set a toggle to a specific state. - * - * @param key The ability key - * @param on true to activate, false to deactivate + * Get the current toggle state for an ability. + * @return 0 if not active, 1+ for active state */ - public void setAbilityToggled(String key, boolean on) { - if (on && !activeToggles.containsKey(key)) { - activateToggle(key); - } else if (!on && activeToggles.containsKey(key)) { - deactivateToggle(key); + public int getToggleState(String key) { + ToggleEntry entry = activeToggles.get(key); + return entry != null ? entry.getState() : 0; + } + + /** + * Set a toggle to a specific state. 0 = deactivate, 1+ = specific state. + */ + public void setToggleState(String key, int state) { + if (state <= 0) { + if (activeToggles.containsKey(key)) { + deactivateToggle(key); + } + return; + } + ToggleEntry existing = activeToggles.get(key); + if (existing != null) { + int currentState = existing.getState(); + if (currentState != state) { + cycleToggleState(key, existing, currentState, state); + } + } else { + activateToggle(key, state); } } /** - * Check if a toggle ability is currently active. + * Check if a toggle ability is currently active (any state > 0). */ public boolean isAbilityToggled(String key) { return activeToggles.containsKey(key); @@ -316,30 +340,52 @@ public abstract class AbstractDataAbilities { return new LinkedHashSet<>(activeToggles.keySet()); } - private boolean activateToggle(String key) { + private boolean activateToggle(String key, int state) { Ability ability = AbilityController.Instance != null ? AbilityController.Instance.resolveAbility(key) : null; if (ability == null || !ability.isToggleable()) return false; + if (state < 1 || state > ability.getToggleStates()) state = 1; - ToggleEntry entry = new ToggleEntry(ability); + if (fireToggleEvent(ability, 0, state)) return false; + + ToggleEntry entry = new ToggleEntry(ability, state); activeToggles.put(key, entry); ability.onToggleOn(getEntity()); - onToggleStateChanged(key, true); + ability.onToggleStateChanged(getEntity(), 0, state); + onToggleStateChanged(key, true, state); return true; } + private int cycleToggleState(String key, ToggleEntry entry, int oldState, int newState) { + Ability ability = entry.getAbility(); + if (newState < 1 || newState > ability.getToggleStates()) return oldState; + + if (fireToggleEvent(ability, oldState, newState)) return oldState; + + entry.setState(newState); + ability.onToggleStateChanged(getEntity(), oldState, newState); + onToggleStateChanged(key, true, newState); + return newState; + } + private void deactivateToggle(String key) { - ToggleEntry entry = activeToggles.remove(key); - if (entry != null) { - entry.getAbility().onToggleOff(getEntity()); - onToggleStateChanged(key, false); - } + ToggleEntry entry = activeToggles.get(key); + if (entry == null) return; + int oldState = entry.getState(); + + if (fireToggleEvent(entry.getAbility(), oldState, 0)) return; + + activeToggles.remove(key); + entry.getAbility().onToggleOff(getEntity()); + entry.getAbility().onToggleStateChanged(getEntity(), oldState, 0); + onToggleStateChanged(key, false, 0); } /** * 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. + * Every 10 ticks, fires a toggle update event for ALL active toggles. */ protected void tickActiveToggles() { if (activeToggles.isEmpty()) return; @@ -352,7 +398,16 @@ public abstract class AbstractDataAbilities { entry.incrementTick(); if (entry.getAbility().hasActiveToggle()) { - if (!entry.getAbility().onToggleTick(entity, entry.getTickCount())) { + if (!entry.getAbility().onToggleTick(entity, entry.getTickCount(), entry.getState())) { + if (toRemove == null) toRemove = new ArrayList<>(); + toRemove.add(mapEntry.getKey()); + continue; + } + } + + if (entry.getTickCount() % 10 == 0) { + boolean enabled = fireToggleUpdateEvent(entry.getAbility(), entry.getTickCount(), entry.getState()); + if (!enabled) { if (toRemove == null) toRemove = new ArrayList<>(); toRemove.add(mapEntry.getKey()); } @@ -363,8 +418,10 @@ public abstract class AbstractDataAbilities { for (String key : toRemove) { ToggleEntry entry = activeToggles.remove(key); if (entry != null) { + int oldState = entry.getState(); entry.getAbility().onToggleOff(entity); - onToggleStateChanged(key, false); + entry.getAbility().onToggleStateChanged(entity, oldState, 0); + onToggleStateChanged(key, false, 0); } } } @@ -373,18 +430,38 @@ public abstract class AbstractDataAbilities { /** * Hook for subclass to react to toggle state changes (sync packets, script events). */ - protected void onToggleStateChanged(String key, boolean active) { + protected void onToggleStateChanged(String key, boolean active, int state) { + } + + /** + * Fire a toggle event before state change. + * @param oldState Previous state (0 = off) + * @param newState Target state (0 = off) + * @return true if the event was canceled + */ + protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { + return false; + } + + /** + * Fire a toggle update event every 10 ticks. + * @return true if the toggle should remain active, false to force-deactivate + */ + protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { + return true; } /** * Clear all active toggles (e.g., on death/reset). - * Calls onToggleOff for each active toggle. + * Calls onToggleOff and onToggleStateChanged for each active toggle. */ protected void clearActiveToggles() { if (activeToggles.isEmpty()) return; EntityLivingBase entity = getEntity(); - for (ToggleEntry entry : activeToggles.values()) { + for (Map.Entry mapEntry : activeToggles.entrySet()) { + ToggleEntry entry = mapEntry.getValue(); entry.getAbility().onToggleOff(entity); + entry.getAbility().onToggleStateChanged(entity, entry.getState(), 0); } activeToggles.clear(); } @@ -392,13 +469,15 @@ public abstract class AbstractDataAbilities { /** * Add a toggle entry directly without calling onToggleOn. * Used for client-side sync and NBT restoration. + * @param key The ability key + * @param state The toggle state (0 = remove, 1+ = set at state) */ - public void setToggleEntryDirect(String key, boolean active) { - if (active) { + public void setToggleEntryDirect(String key, int state) { + if (state > 0) { Ability ability = AbilityController.Instance != null ? AbilityController.Instance.resolveAbility(key) : null; if (ability != null && ability.isToggleable()) { - activeToggles.put(key, new ToggleEntry(ability)); + activeToggles.put(key, new ToggleEntry(ability, state)); } } else { activeToggles.remove(key); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 1caca740a..988a2f073 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityAction; import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.ChainedAbility; import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import kamkeel.npcs.controllers.data.ability.ToggleEntry; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; @@ -22,6 +23,7 @@ import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Random; /** @@ -153,6 +155,21 @@ public class DataAbilities extends AbstractDataAbilities { NpcAPI.EVENT_BUS.post(interruptEvent); } + @Override + protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { + AbilityEvent.ToggleEvent event = new AbilityEvent.ToggleEvent( + npc.wrappedNPC, ability, oldState, newState); + return NpcAPI.EVENT_BUS.post(event); + } + + @Override + protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { + AbilityEvent.ToggleUpdateEvent event = new AbilityEvent.ToggleUpdateEvent( + npc.wrappedNPC, ability, tick, state); + NpcAPI.EVENT_BUS.post(event); + return event.isEnabled(); + } + @Override protected void spawnTelegraph(Ability ability, EntityLivingBase target) { List telegraphs = ability.createTelegraphs(npc, target); @@ -1060,10 +1077,13 @@ public class DataAbilities extends AbstractDataAbilities { } compound.setTag("AbilityActions", actionList); - // Active toggles + // Active toggles (compound format with state) NBTTagList toggleList = new NBTTagList(); - for (String key : activeToggles.keySet()) { - toggleList.appendTag(new NBTTagString(key)); + for (Map.Entry entry : activeToggles.entrySet()) { + NBTTagCompound toggleNbt = new NBTTagCompound(); + toggleNbt.setString("Key", entry.getKey()); + toggleNbt.setInteger("State", entry.getValue().getState()); + toggleList.appendTag(toggleNbt); } compound.setTag("ActiveToggles", toggleList); @@ -1111,10 +1131,12 @@ public class DataAbilities extends AbstractDataAbilities { // 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 + NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND for (int i = 0; i < toggleNbt.tagCount(); i++) { - String key = toggleNbt.getStringTagAt(i); - setToggleEntryDirect(key, true); + NBTTagCompound entry = toggleNbt.getCompoundTagAt(i); + String key = entry.getString("Key"); + int state = entry.hasKey("State") ? entry.getInteger("State") : 1; + setToggleEntryDirect(key, state); } } } diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 2b9ebaabe..76029adab 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -881,6 +881,16 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } + public static boolean onPlayerAbilityToggle(PlayerDataScript handler, PlayerAbilityEvent.ToggleEvent event) { + handler.callScript(EnumScriptType.ABILITY_TOGGLE, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static void onPlayerAbilityToggleUpdate(PlayerDataScript handler, PlayerAbilityEvent.ToggleUpdateEvent event) { + handler.callScript(EnumScriptType.ABILITY_TOGGLE_UPDATE, event); + NpcAPI.EVENT_BUS.post(event); + } + public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) { PlayerEvent.ChangedDimension event = new PlayerEvent.ChangedDimension(player, fromDim, toDim); handler.callScript(EnumScriptType.CHANGED_DIM, event); diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index 1088ef83a..5b5a318b3 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -126,6 +126,8 @@ public enum EnumScriptType { ABILITY_COMPLETE("abilityComplete"), ABILITY_HIT("abilityHit"), ABILITY_TICK("abilityTick"), + ABILITY_TOGGLE("abilityToggle"), + ABILITY_TOGGLE_UPDATE("abilityToggleUpdate"), ENERGY_PROJECTILE_FIRED("energyProjectileFired"), ENERGY_PROJECTILE_TICK("energyProjectileTick"), diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 8562053c2..f06c21686 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -5,6 +5,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.ChainedAbility; import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import kamkeel.npcs.controllers.data.ability.ToggleEntry; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; @@ -30,6 +31,7 @@ import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; /** * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. @@ -422,10 +424,33 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } @Override - protected void onToggleStateChanged(String key, boolean active) { + protected void onToggleStateChanged(String key, boolean active, int state) { syncToClient(); } + @Override + protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { + EntityPlayer player = playerData.player; + if (ScriptController.Instance == null || player == null) return false; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler == null) return false; + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.ToggleEvent event = new PlayerAbilityEvent.ToggleEvent(iPlayer, ability, oldState, newState); + return EventHooks.onPlayerAbilityToggle(handler, event); + } + + @Override + protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { + EntityPlayer player = playerData.player; + if (ScriptController.Instance == null || player == null) return true; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler == null) return true; + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.ToggleUpdateEvent event = new PlayerAbilityEvent.ToggleUpdateEvent(iPlayer, ability, tick, state); + EventHooks.onPlayerAbilityToggleUpdate(handler, event); + return event.isEnabled(); + } + // ═══════════════════════════════════════════════════════════════════ // SELECTION // ═══════════════════════════════════════════════════════════════════ @@ -837,10 +862,13 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA compound.setInteger("PlayerAbilitySelected", selectedIndex); compound.setBoolean("AbilityAnimating", playingAbilityAnimation); - // Active toggles + // Active toggles (compound format with state) NBTTagList toggleList = new NBTTagList(); - for (String key : activeToggles.keySet()) { - toggleList.appendTag(new NBTTagString(key)); + for (Map.Entry entry : activeToggles.entrySet()) { + NBTTagCompound toggleNbt = new NBTTagCompound(); + toggleNbt.setString("Key", entry.getKey()); + toggleNbt.setInteger("State", entry.getValue().getState()); + toggleList.appendTag(toggleNbt); } compound.setTag("ActiveToggles", toggleList); } @@ -869,10 +897,12 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA // 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 + NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND for (int i = 0; i < toggleNbt.tagCount(); i++) { - String key = toggleNbt.getStringTagAt(i); - setToggleEntryDirect(key, true); + NBTTagCompound entry = toggleNbt.getCompoundTagAt(i); + String key = entry.getString("Key"); + int state = entry.hasKey("State") ? entry.getInteger("State") : 1; + setToggleEntryDirect(key, state); } } } diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java index 9bf7719d5..c7a43cfe7 100644 --- a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -203,4 +203,81 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { return EnumScriptType.ABILITY_TICK.function; } } + + /** + * Fired when a toggle ability changes state. + * Canceling prevents the toggle from changing state. + */ + @Cancelable + public static class ToggleEvent extends AbilityEvent implements IAbilityEvent.ToggleEvent { + private final int oldState; + private final int newState; + + public ToggleEvent(ICustomNpc npc, Ability ability, int oldState, int newState) { + super(npc, ability, null); + this.oldState = oldState; + this.newState = newState; + } + + @Override + public boolean isTogglingOn() { + return newState > 0; + } + + @Override + public int getOldState() { + return oldState; + } + + @Override + public int getNewState() { + return newState; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_TOGGLE.function; + } + } + + /** + * Fired every 10 ticks for each active toggle ability. + * Scripts can set enabled to false to force-deactivate. + */ + public static class ToggleUpdateEvent extends AbilityEvent implements IAbilityEvent.ToggleUpdateEvent { + private final int tick; + private final int state; + private boolean enabled = true; + + public ToggleUpdateEvent(ICustomNpc npc, Ability ability, int tick, int state) { + super(npc, ability, null); + this.tick = tick; + this.state = state; + } + + @Override + public int getTick() { + return tick; + } + + @Override + public int getState() { + return state; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_TOGGLE_UPDATE.function; + } + } } diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java index b99aac1a1..e5d46fd15 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -202,4 +202,81 @@ public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEve return EnumScriptType.ABILITY_HIT.function; } } + + /** + * Fired when a toggle ability changes state. + * Canceling prevents the toggle from changing state. + */ + @Cancelable + public static class ToggleEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ToggleEvent { + private final int oldState; + private final int newState; + + public ToggleEvent(IPlayer player, Ability ability, int oldState, int newState) { + super(player, ability, null); + this.oldState = oldState; + this.newState = newState; + } + + @Override + public boolean isTogglingOn() { + return newState > 0; + } + + @Override + public int getOldState() { + return oldState; + } + + @Override + public int getNewState() { + return newState; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_TOGGLE.function; + } + } + + /** + * Fired every 10 ticks for each active toggle ability. + * Scripts can set enabled to false to force-deactivate. + */ + public static class ToggleUpdateEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ToggleUpdateEvent { + private final int tick; + private final int state; + private boolean enabled = true; + + public ToggleUpdateEvent(IPlayer player, Ability ability, int tick, int state) { + super(player, ability, null); + this.tick = tick; + this.state = state; + } + + @Override + public int getTick() { + return tick; + } + + @Override + public int getState() { + return state; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_TOGGLE_UPDATE.function; + } + } } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index c66a12a48..70d915537 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -134,6 +134,28 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { return data.activateAbility(player, key); } + // Toggle system + + @Override + public int toggleAbility(String key) { + return data.toggleAbility(key); + } + + @Override + public int getToggleState(String key) { + return data.getToggleState(key); + } + + @Override + public void setToggleState(String key, int state) { + data.setToggleState(key, state); + } + + @Override + public boolean isAbilityToggled(String key) { + return data.isAbilityToggled(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.