From 095709b9d2c5ae7beed84de5f0f3d173823a7e86 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 5 Feb 2026 22:49:40 -0500 Subject: [PATCH] Add ability wheel and toggle abilities for enhanced gameplay --- .../kamkeel/npcs/command/AbilityCommand.java | 192 ++++++++++++++++++ .../kamkeel/npcs/command/CommandKamkeel.java | 4 + .../data/ability/AbilityController.java | 77 ++++++- .../packets/player/SpecialKeyStatePacket.java | 10 + src/main/java/noppes/npcs/DataAbilities.java | 10 + .../java/noppes/npcs/NoppesUtilServer.java | 4 + .../client/gui/advanced/GuiNPCAbilities.java | 6 +- .../scripted/wrapper/ScriptDataAbilities.java | 23 ++- .../wrapper/ScriptPlayerAbilityData.java | 14 +- 9 files changed, 330 insertions(+), 10 deletions(-) diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index d5b9420cc..004274fc3 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -2,7 +2,12 @@ package kamkeel.npcs.command; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +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.PlayerData; import java.util.ArrayList; import java.util.Collections; @@ -122,5 +127,192 @@ public class AbilityCommand extends CommandKamkeelBase { } sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange()); sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No")); + sendResult(sender, " Allowed By: \u00A7e" + ability.getAllowedBy().name()); + } + + @SubCommand( + desc = "List all built-in abilities available for players" + ) + public void prebuilts(ICommandSender sender, String[] args) { + Set names = AbilityController.Instance.getAbilityNames(); + if (names.isEmpty()) { + sendResult(sender, "No built-in abilities found."); + return; + } + + List playerAbilities = new ArrayList<>(); + for (String name : names) { + Ability ability = AbilityController.Instance.getAbility(name); + if (ability != null && ability.getAllowedBy().allowsPlayer()) { + playerAbilities.add(name); + } + } + + if (playerAbilities.isEmpty()) { + sendResult(sender, "No player-usable built-in abilities found."); + return; + } + + Collections.sort(playerAbilities); + sendResult(sender, "Built-in Player Abilities (" + playerAbilities.size() + "):"); + for (String name : playerAbilities) { + Ability ability = AbilityController.Instance.getAbility(name); + String userType = ability.getAllowedBy() == UserType.PLAYER_ONLY ? "\u00A7d[PLAYER]" : "\u00A7a[BOTH]"; + sendResult(sender, " - \u00A7b" + name + " " + userType); + } + } + + @SubCommand( + desc = "Unlock an ability for a player", + usage = " " + ) + public void unlock(ICommandSender sender, String[] args) { + if (args.length < 2) { + sendError(sender, "Usage: /kam ability unlock "); + 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); + return; + } + + // 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; + + // 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); + return; + } + + data.abilityData.unlockAbility(canonicalKey); + sendResult(sender, "Unlocked ability '\u00A7b" + displayName + "\u00A77' for player \u00A7a" + playerName); + } + + @SubCommand( + desc = "Lock (revoke) an ability from a player", + usage = " " + ) + public void lock(ICommandSender sender, String[] args) { + if (args.length < 2) { + sendError(sender, "Usage: /kam ability lock "); + 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 + 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; + } + + // Get player data and lock + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (!data.abilityData.hasUnlockedAbility(canonicalKey)) { + sendError(sender, "Player doesn't have ability: " + displayName); + return; + } + + data.abilityData.lockAbility(canonicalKey); + sendResult(sender, "Locked ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName); + } + + @SubCommand( + desc = "List abilities unlocked for a player", + usage = "" + ) + public void player(ICommandSender sender, String[] args) { + if (args.length < 1) { + sendError(sender, "Usage: /kam ability player "); + return; + } + + String playerName = args[0]; + + // Find player + EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); + if (player == null) { + sendError(sender, "Player not found: " + playerName); + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + String[] abilityKeys = data.abilityData.getUnlockedAbilities(); + + if (abilityKeys.length == 0) { + sendResult(sender, "Player \u00A7a" + playerName + "\u00A77 has no unlocked abilities."); + return; + } + + sendResult(sender, "Abilities for \u00A7a" + playerName + "\u00A77 (" + abilityKeys.length + "):"); + 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.getName() != null ? ability.getName() : key; + String prefix = (i == selected) ? "\u00A7e> " : " "; + // Show display name and key if different + if (!displayName.equals(key)) { + sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]"); + } else { + sendResult(sender, prefix + "\u00A7b" + displayName); + } + } + } + + private String joinArgs(String[] args, int startIndex) { + StringBuilder sb = new StringBuilder(); + for (int i = startIndex; i < args.length; i++) { + if (i > startIndex) sb.append(" "); + sb.append(args[i]); + } + return sb.toString(); + } + + /** + * Get list of player-usable ability keys (for tab completion). + * Uses registry keys for built-in abilities (no spaces) and UUIDs for custom. + * Used by CommandKamkeel for usage token. + */ + public static List getPlayerAbilityNames() { + List keys = new ArrayList<>(AbilityController.Instance.getPlayerAbilityKeys()); + Collections.sort(keys); + return keys; } } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index d0341f211..91356c825 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static kamkeel.npcs.command.AbilityCommand.getPlayerAbilityNames; import static kamkeel.npcs.command.EffectCommand.getSortedEffectNames; public class CommandKamkeel extends CommandBase { @@ -135,6 +136,9 @@ public class CommandKamkeel extends CommandBase { } else if (usage.equals("")) { List keys = getSortedEffectNames(); return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()])); + } else if (usage.equals("")) { + List keys = getPlayerAbilityNames(); + 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/data/ability/AbilityController.java index 213c1a214..05110f553 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -180,30 +180,93 @@ public class AbilityController implements IAbilityHandler { // ═══════════════════════════════════════════════════════════════════════════ /** - * Resolve an ability by key. Checks built-in abilities first (by name), - * then custom abilities (by UUID). Returns a copy safe to modify. + * Resolve an ability by key. Checks built-in abilities first (by name or registry key), + * then custom abilities (by UUID or name). Returns a copy safe to modify for + * custom abilities, or the singleton instance for built-in abilities. * - * @param key The ability name (built-in) or UUID (custom) - * @return A copy of the resolved ability, or null if not found + * @param key The ability name, registry key (built-in) or UUID (custom) + * @return The resolved ability, or null if not found */ public Ability resolveAbility(String key) { if (key == null || key.isEmpty()) return null; - // Check built-in abilities first + // Check built-in abilities first (exact match by name) Ability builtIn = abilities.get(key); if (builtIn != null) { - return fromNBT(builtIn.writeNBT()); + return builtIn; } - // Check custom abilities + // Check built-in abilities by name (case-insensitive) + for (Map.Entry entry : abilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + + // Check built-in abilities by registry key / ID + for (Ability ability : abilities.values()) { + if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) { + return ability; + } + } + + // Check custom abilities (exact UUID match) Ability custom = customAbilities.get(key); if (custom != null) { return fromNBT(custom.writeNBT()); } + // Check custom abilities by name (case-insensitive) + for (Ability ability : customAbilities.values()) { + if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) { + return fromNBT(ability.writeNBT()); + } + } + return null; } + /** + * Get all keys that can be used to look up abilities (for tab completion). + * Returns registry keys for built-in abilities (no spaces) and UUIDs for custom. + */ + public Set getAbilityKeys() { + Set keys = new LinkedHashSet<>(); + // Built-in abilities - use the ID (registry key for BuiltInAbility) + for (Ability ability : abilities.values()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + // Custom abilities - use UUID + keys.addAll(customAbilities.keySet()); + return keys; + } + + /** + * Get keys for player-usable abilities only (for tab completion). + */ + public Set getPlayerAbilityKeys() { + Set keys = new LinkedHashSet<>(); + // Built-in abilities - use the ID (registry key for BuiltInAbility) + for (Ability ability : abilities.values()) { + if (ability.getAllowedBy().allowsPlayer()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + } + // Custom abilities - use UUID (if player-usable) + for (Map.Entry entry : customAbilities.entrySet()) { + if (entry.getValue().getAllowedBy().allowsPlayer()) { + keys.add(entry.getKey()); + } + } + return keys; + } + /** * Check if an ability key exists (either built-in or custom). */ diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java index 48ac3391b..a58b896f4 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -59,6 +59,16 @@ public final class SpecialKeyStatePacket extends AbstractPacket { // Trigger ability activation on key press (rising edge) if (keyDown && !wasDown) { + // Don't activate if player has a GUI open (not the default inventory) + if (player.openContainer != null && !(player.openContainer instanceof net.minecraft.inventory.ContainerPlayer)) { + return; + } + + // Don't activate if player is riding an entity (mount) + if (player.ridingEntity != null) { + return; + } + data.abilityData.activateAbility(player); } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index cb83caae2..36dc6f34a 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -368,6 +368,11 @@ public class DataAbilities { * Check if a specific ability is eligible for use. */ private boolean isAbilityEligible(Ability ability, EntityLivingBase target) { + // Check UserType allows NPCs + if (!ability.getAllowedBy().allowsNpc()) { + return false; + } + // Check enabled if (!ability.isEnabled()) { return false; @@ -695,6 +700,11 @@ public class DataAbilities { return false; } + // Check UserType allows NPCs + if (!ability.getAllowedBy().allowsNpc()) { + return false; + } + // Stop any currently executing ability stopCurrentAbility(); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 4b93ad2b5..7faa7b2b5 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -2,6 +2,7 @@ package noppes.npcs; import io.netty.buffer.ByteBuf; 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; @@ -792,6 +793,9 @@ public class NoppesUtilServer { if (controller != null) { int index = 0; for (String uuid : controller.getCustomAbilityIds()) { + Ability ability = controller.getCustomAbility(uuid); + if (ability == null || !ability.getAllowedBy().allowsNpc()) continue; + String name = controller.getCustomAbilityName(uuid); if (name == null || name.isEmpty()) name = uuid; map.put(name + "\t" + uuid, index++); 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 998473623..fd8461245 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -334,8 +334,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC Map dummyMap = new HashMap<>(allAbilityTypes); filteredAbilityTypes.clear(); filteredAbilityTypes.putAll(getFilteredData(dummyMap)); - } + // Reset scroll position when changing category + if (availableTypesScroll != null) { + availableTypesScroll.resetScroll(); + } + } initGui(); } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index d0b5259ea..e27f6e075 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -42,12 +42,25 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public void addAbility(IAbility ability) { if (ability instanceof Ability) { - data.addAbility((Ability) ability); + Ability a = (Ability) ability; + // Validate ability allows NPCs + if (!a.getAllowedBy().allowsNpc()) { + return; // Ability is PLAYER_ONLY + } + data.addAbility(a); } } @Override public void addAbilityReference(String key) { + // Validate ability exists and allows NPCs + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) { + return; // Ability doesn't exist + } + if (!ability.getAllowedBy().allowsNpc()) { + return; // Ability is PLAYER_ONLY + } data.addAbilityReference(key); } @@ -158,6 +171,14 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public boolean executeAbility(String key, Object target) { + // Validate ability exists and allows NPCs + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) { + return false; // Ability doesn't exist + } + if (!ability.getAllowedBy().allowsNpc()) { + return false; // Ability is PLAYER_ONLY + } EntityLivingBase targetEntity = resolveTarget(target); return data.executeAbility(key, targetEntity); } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index a36c65d51..42ed3372e 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -1,5 +1,7 @@ package noppes.npcs.scripted.wrapper; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.api.ability.IAbility; import noppes.npcs.api.ability.IPlayerAbilityData; @@ -26,7 +28,17 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { @Override public void unlockAbility(String key) { - data.unlockAbility(key); + // Validate ability exists and allows players + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) { + return; // Ability doesn't exist + } + if (!ability.getAllowedBy().allowsPlayer()) { + return; // Ability is NPC-only + } + // Use canonical key (registry key for built-in, UUID for custom) + String canonicalKey = ability.getId() != null ? ability.getId() : key; + data.unlockAbility(canonicalKey); } @Override