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 89e059967..ee4bb5090 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -734,6 +734,25 @@ public abstract class Ability implements IAbility, IAbilityAction { } } + // ── Icon tab (native) ───────────────────────────────────── + if (!isNpcInlineEdit()) { + AbilityIconData icon = AbilityIconData.fromAbility(this); + defs.add(FieldDef.stringField("gui.texture", icon::getTexture, icon::setTexture) + .tab("Icon")); + defs.add(FieldDef.section("ability.icon.section.uv").tab("Icon")); + defs.add(FieldDef.intField("ability.icon.x", icon::getIconX, icon::setIconX) + .tab("Icon").range(0, 4096)); + defs.add(FieldDef.intField("ability.icon.y", icon::getIconY, icon::setIconY) + .tab("Icon").range(0, 4096)); + defs.add(FieldDef.section("gui.size").tab("Icon")); + defs.add(FieldDef.intField("gui.width", icon::getWidth, icon::setWidth) + .tab("Icon").range(1, 256)); + defs.add(FieldDef.intField("gui.height", icon::getHeight, icon::setHeight) + .tab("Icon").range(1, 256)); + defs.add(FieldDef.floatField("gui.scale", icon::getScale, icon::setScale) + .tab("Icon").range(0.1f, 10.0f)); + } + // External field providers (e.g., DBC Addon injecting a "DBC" tab) for (IAbilityFieldProvider provider : AbilityController.Instance.getFieldProviders()) { provider.addFieldDefinitions(this, defs); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityIconData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityIconData.java new file mode 100644 index 000000000..2120e9ca6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityIconData.java @@ -0,0 +1,202 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; + +/** + * Icon data for abilities, stored in the ability's customData NBT under "AbilityIcon". + * Used to render ability icons in the Ability Hotbar, Wheel, and Abilities Tab. + */ +public class AbilityIconData { + public static final String NBT_KEY = "AbilityIcon"; + + private final NBTTagCompound customData; + + // Icon properties + public String texture = ""; + public int iconX = 0; + public int iconY = 0; + public int width = 32; + public int height = 32; + public float scale = 1.0f; + + /** Per-state icon UV overrides (0-indexed, maps to toggle states 1, 2, ...). */ + private int[][] stateIcons = null; + + private AbilityIconData(NBTTagCompound customData) { + this.customData = customData; + } + + /** + * Create an AbilityIconData instance from a raw customData compound. + * Reads existing values if present, otherwise uses defaults. + */ + public static AbilityIconData fromCustomData(NBTTagCompound customData) { + AbilityIconData icon = new AbilityIconData(customData); + if (customData.hasKey(NBT_KEY)) { + icon.readFromNBT(customData.getCompoundTag(NBT_KEY)); + } + return icon; + } + + /** + * Create an AbilityIconData instance from an ability's customData. + */ + public static AbilityIconData fromAbility(Ability ability) { + return fromCustomData(ability.getCustomData()); + } + + /** + * Create an AbilityIconData instance from a chained ability's customData. + */ + public static AbilityIconData fromChainedAbility(ChainedAbility chain) { + return fromCustomData(chain.getCustomData()); + } + + /** + * Write current state back to the customData NBT. + */ + public void save() { + NBTTagCompound tag = new NBTTagCompound(); + writeToNBT(tag); + customData.setTag(NBT_KEY, tag); + } + + public void writeToNBT(NBTTagCompound nbt) { + nbt.setString("Texture", texture); + nbt.setInteger("IconX", iconX); + nbt.setInteger("IconY", iconY); + nbt.setInteger("Width", width); + nbt.setInteger("Height", height); + nbt.setFloat("Scale", scale); + if (stateIcons != null && stateIcons.length > 0) { + NBTTagList list = new NBTTagList(); + for (int[] pair : stateIcons) { + NBTTagCompound comp = new NBTTagCompound(); + comp.setInteger("IconX", pair[0]); + comp.setInteger("IconY", pair[1]); + list.appendTag(comp); + } + nbt.setTag("StateIcons", list); + } + } + + public void readFromNBT(NBTTagCompound nbt) { + texture = nbt.getString("Texture"); + iconX = nbt.getInteger("IconX"); + iconY = nbt.getInteger("IconY"); + width = nbt.getInteger("Width"); + if (width <= 0) width = 32; + height = nbt.getInteger("Height"); + if (height <= 0) height = 32; + scale = nbt.getFloat("Scale"); + if (scale <= 0) scale = 1.0f; + if (nbt.hasKey("StateIcons")) { + NBTTagList list = nbt.getTagList("StateIcons", 10); + stateIcons = new int[list.tagCount()][2]; + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound comp = list.getCompoundTagAt(i); + stateIcons[i][0] = comp.getInteger("IconX"); + stateIcons[i][1] = comp.getInteger("IconY"); + } + } + } + + /** + * Check if this icon has a valid texture configured. + */ + public boolean hasTexture() { + return texture != null && !texture.isEmpty(); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS AND SETTERS (with auto-save) + // ═══════════════════════════════════════════════════════════════════ + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture != null ? texture : ""; + save(); + } + + public int getIconX() { + return iconX; + } + + public void setIconX(int iconX) { + this.iconX = Math.max(0, iconX); + save(); + } + + public int getIconY() { + return iconY; + } + + public void setIconY(int iconY) { + this.iconY = Math.max(0, iconY); + save(); + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = Math.max(1, width); + save(); + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = Math.max(1, height); + save(); + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = Math.max(0.1f, scale); + save(); + } + + // ═══════════════════════════════════════════════════════════════════ + // PER-STATE ICON OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + public void setStateIcons(int[][] stateIcons) { + this.stateIcons = stateIcons; + save(); + } + + /** + * Get iconX for a specific toggle state. + * Falls back to default iconX if no state override exists. + * @param state 1-indexed toggle state (0 = off/default) + */ + public int getIconXForState(int state) { + if (state > 0 && stateIcons != null && state - 1 < stateIcons.length) { + return stateIcons[state - 1][0]; + } + return iconX; + } + + /** + * Get iconY for a specific toggle state. + * Falls back to default iconY if no state override exists. + * @param state 1-indexed toggle state (0 = off/default) + */ + public int getIconYForState(int state) { + if (state > 0 && stateIcons != null && state - 1 < stateIcons.length) { + return stateIcons[state - 1][1]; + } + return iconY; + } +} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index c3c555d48..ca172edc3 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,7 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; @@ -54,6 +55,9 @@ import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSavePacket; +import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSelectPacket; +import kamkeel.npcs.network.packets.player.ability.AbilityTogglePacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -598,6 +602,7 @@ public class PacketHandler { DATA_PACKET.registerPacket(new TelegraphRemovePacket()); DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); DATA_PACKET.registerPacket(new PlayerAbilityStatePacket()); + DATA_PACKET.registerPacket(new AbilityHotbarSyncPacket()); } public void registerPlayerPackets() { @@ -663,6 +668,11 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new PartyAcceptInvitePacket()); PLAYER_PACKET.registerPacket(new PartyIgnoreInvitePacket()); PLAYER_PACKET.registerPacket(new PartyLogToServerPacket()); + + // Ability Hotbar Packets + PLAYER_PACKET.registerPacket(new AbilityHotbarSavePacket()); + PLAYER_PACKET.registerPacket(new AbilityHotbarSelectPacket()); + PLAYER_PACKET.registerPacket(new AbilityTogglePacket()); } public void registerChannels() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 8362cb3c5..e72cd2810 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -64,6 +64,7 @@ public enum EnumDataPacket { TELEGRAPH_REMOVE, PLAYER_ABILITY_SYNC, PLAYER_ABILITY_STATE, + ABILITY_HOTBAR_SYNC, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 48f400fb2..e1587467b 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -37,4 +37,9 @@ public enum EnumPlayerPacket { // Auction AuctionAction, + + // Ability Hotbar + AbilityHotbarSave, + AbilityHotbarSelect, + AbilityToggle, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java new file mode 100644 index 000000000..a30682ecc --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java @@ -0,0 +1,101 @@ +package kamkeel.npcs.network.packets.data.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.AbilityHotbarData; +import noppes.npcs.controllers.data.PlayerAbilityHotbarData; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +public final class AbilityHotbarSyncPacket extends AbstractPacket { + public static final String packetName = "Data|AbilityHotbarSync"; + + private NBTTagCompound hotbarNBT; + + public AbilityHotbarSyncPacket() { + } + + public AbilityHotbarSyncPacket(NBTTagCompound hotbarNBT) { + this.hotbarNBT = hotbarNBT; + } + + @Override + public Enum getType() { + return EnumDataPacket.ABILITY_HOTBAR_SYNC; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, hotbarNBT); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + if (nbt == null) return; + + PlayerData data = ClientCacheHandler.playerData; + if (data != null) { + data.hotbarData.readFromNBT(nbt); + } + } + + public static void sendToPlayer(EntityPlayerMP player) { + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null) return; + + PlayerAbilityHotbarData hotbarData = data.hotbarData; + boolean anyCleared = false; + + // Validate all slots before sending + if (AbilityController.Instance != null) { + for (int i = 0; i < hotbarData.slots.length; i++) { + AbilityHotbarData slot = hotbarData.slots[i]; + if (slot.isEmpty()) continue; + + boolean valid; + if (slot.isChainKey()) { + valid = AbilityController.Instance.canResolveChainedAbility(slot.getResolveKey()); + } else { + valid = AbilityController.Instance.canResolveAbility(slot.abilityKey); + } + + if (valid && data.abilityData != null) { + valid = data.abilityData.hasUnlockedAbility(slot.abilityKey); + } + + if (!valid) { + slot.reset(); + anyCleared = true; + } + } + } + + NBTTagCompound nbt = new NBTTagCompound(); + hotbarData.writeToNBT(nbt); + PacketHandler.Instance.sendToPlayer(new AbilityHotbarSyncPacket(nbt), player); + + if (anyCleared) { + data.save(); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java new file mode 100644 index 000000000..1522763fc --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java @@ -0,0 +1,86 @@ +package kamkeel.npcs.network.packets.player.ability; + +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.AbilityHotbarData; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +public final class AbilityHotbarSavePacket extends AbstractPacket { + public static final String packetName = "Player|AbilityHotbarSave"; + + private int slotIndex; + private NBTTagCompound slotData; + + public AbilityHotbarSavePacket() { + } + + public AbilityHotbarSavePacket(int slotIndex, NBTTagCompound slotData) { + this.slotIndex = slotIndex; + this.slotData = slotData; + } + + @Override + public Enum getType() { + return EnumPlayerPacket.AbilityHotbarSave; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(slotIndex); + ByteBufUtils.writeNBT(out, slotData); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + int slot = in.readInt(); + NBTTagCompound compound = ByteBufUtils.readNBT(in); + + if (slot < 0 || slot >= AbilityHotbarData.TOTAL_SLOTS) return; + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null) return; + + AbilityHotbarData slotData = data.hotbarData.getSlot(slot); + if (slotData == null) return; + + slotData.readFromNBT(compound.getCompoundTag("AbilityHotbar" + slot)); + + // Validate the ability key + boolean valid = true; + if (!slotData.isEmpty() && AbilityController.Instance != null) { + if (slotData.isChainKey()) { + valid = AbilityController.Instance.canResolveChainedAbility(slotData.getResolveKey()); + } else { + valid = AbilityController.Instance.canResolveAbility(slotData.abilityKey); + } + + if (valid && data.abilityData != null) { + valid = data.abilityData.hasUnlockedAbility(slotData.abilityKey); + } + } + + if (!valid) { + slotData.reset(); + } + + data.save(); + AbilityHotbarSyncPacket.sendToPlayer((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java new file mode 100644 index 000000000..7a4e183a2 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSelectPacket.java @@ -0,0 +1,63 @@ +package kamkeel.npcs.network.packets.player.ability; + +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.AbilityHotbarData; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +public final class AbilityHotbarSelectPacket extends AbstractPacket { + public static final String packetName = "Player|AbilityHotbarSelect"; + + private String abilityKey; + + public AbilityHotbarSelectPacket() { + } + + public AbilityHotbarSelectPacket(String abilityKey) { + this.abilityKey = abilityKey; + } + + @Override + public Enum getType() { + return EnumPlayerPacket.AbilityHotbarSelect; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityKey != null ? abilityKey : ""); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + String key = ByteBufUtils.readString(in); + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null || data.abilityData == null) return; + + if (key == null || key.isEmpty()) { + // Deselect + data.abilityData.setSelectedIndex(-1); + } else { + int index = data.abilityData.getUnlockedAbilityList().indexOf(key); + if (index >= 0) { + data.abilityData.setSelectedIndex(index); + } + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityTogglePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityTogglePacket.java new file mode 100644 index 000000000..f9f402c5e --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityTogglePacket.java @@ -0,0 +1,72 @@ +package kamkeel.npcs.network.packets.player.ability; + +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.StatCollector; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +public final class AbilityTogglePacket extends AbstractPacket { + public static final String packetName = "Player|AbilityToggle"; + private String abilityKey; + + public AbilityTogglePacket() { + } + + public AbilityTogglePacket(String abilityKey) { + this.abilityKey = abilityKey; + } + + @Override + public Enum getType() { + return EnumPlayerPacket.AbilityToggle; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityKey != null ? abilityKey : ""); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + String key = ByteBufUtils.readString(in); + if (key == null || key.isEmpty()) return; + if (AbilityController.Instance == null) return; + + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null || !ability.isToggleable() || !ability.getAllowedBy().allowsPlayer()) return; + + PlayerData playerData = PlayerData.get(player); + if (playerData == null || playerData.abilityData == null) return; + + int newState = playerData.abilityData.toggleAbility(key); + + String displayName = ability.getDisplayName(); + if (newState > 0) { + String stateLabel = ability.getToggleStateLabel(newState); + if (stateLabel != null) { + player.addChatMessage(new ChatComponentText("\u00A7a" + displayName + " " + stateLabel)); + } else { + String enabled = StatCollector.translateToLocal("gui.enabled"); + player.addChatMessage(new ChatComponentText("\u00A7a" + displayName + " " + enabled)); + } + } else { + String disabled = StatCollector.translateToLocal("gui.disabled"); + player.addChatMessage(new ChatComponentText("\u00A7c" + displayName + " " + disabled)); + } + } +} diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index a73ccf707..6eebfd5d5 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -25,6 +25,7 @@ import noppes.npcs.client.gui.hud.ClientHudManager; import noppes.npcs.client.gui.hud.CompassHudComponent; import noppes.npcs.client.gui.hud.EnumHudComponent; import noppes.npcs.client.gui.hud.QuestTrackingComponent; +import noppes.npcs.client.gui.hud.ability.AbilityHotbarComponent; import noppes.npcs.client.gui.player.AuctionTooltipHandler; import noppes.npcs.client.renderer.MarkRenderer; import noppes.npcs.client.renderer.RenderCNPCPlayer; @@ -98,6 +99,24 @@ public class ClientEventHandler { return; } + // Ability Hotbar: Hold HUD key + scroll wheel to cycle + if (event.dwheel != 0 && ClientProxy.AbilityHudKey != null) { + int hudKeyCode = ClientProxy.AbilityHudKey.getKeyCode(); + if (hudKeyCode != 0 && org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode)) { + AbilityHotbarComponent comp = (AbilityHotbarComponent) ClientHudManager.getInstance() + .getHudComponents().get(EnumHudComponent.AbilityHotbar); + if (comp != null && comp.hasAnyAbilities()) { + if (event.dwheel > 0) { + comp.onCyclePrev(); + } else { + comp.onCycleNext(); + } + event.setCanceled(true); + return; + } + } + } + ArrayList removeList = new ArrayList<>(); for (Map.Entry entry : disabledButtonTimes.entrySet()) { if (entry.getValue() > 0) { @@ -125,6 +144,7 @@ public class ClientEventHandler { if (ClientHudManager.getInstance().getHudComponents().isEmpty()) { ClientHudManager.getInstance().registerHud(EnumHudComponent.QuestTracker, new QuestTrackingComponent(Minecraft.getMinecraft())); ClientHudManager.getInstance().registerHud(EnumHudComponent.QuestCompass, new CompassHudComponent(Minecraft.getMinecraft())); + ClientHudManager.getInstance().registerHud(EnumHudComponent.AbilityHotbar, new AbilityHotbarComponent(Minecraft.getMinecraft())); } ClientHudManager.getInstance().renderAllHUDs(event.partialTicks); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index a0a5ce060..c0a113a96 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -257,6 +257,7 @@ import java.util.Random; public class ClientProxy extends CommonProxy { public static KeyBinding NPCButton; public static KeyBinding SpecialKey; + public static KeyBinding AbilityHudKey; public static final Random RAND = new Random(); public static FontContainer Font; @@ -332,11 +333,14 @@ public class ClientProxy extends CommonProxy { Minecraft mc = Minecraft.getMinecraft(); NPCButton = new KeyBinding("NPC Inventory", Keyboard.KEY_N, "key.categories.customnpc"); - SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_B, "key.categories.customnpc"); + SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_GRAVE, "key.categories.customnpc"); ClientRegistry.registerKeyBinding(NPCButton); ClientRegistry.registerKeyBinding(SpecialKey); + AbilityHudKey = new KeyBinding("key.customnpcs.abilityHudKey", Keyboard.KEY_LMENU, "key.categories.customnpc"); + ClientRegistry.registerKeyBinding(AbilityHudKey); + new PresetController(CustomNpcs.Dir); if (ConfigMain.EnableUpdateChecker) { 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 da71dec70..565443c3d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityVariant; 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.AbilityIconData; import kamkeel.npcs.controllers.data.ability.IChainedAbilityFieldProvider; import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.network.PacketClient; @@ -157,7 +158,18 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText FieldDef.floatField("ability.maxRange", chain::getMaxRange, chain::setMaxRange).range(0, 64) ).tab("Target")); - // External field providers (e.g., DBC Addon injecting an "Icon" tab) + // ── Icon tab ────────────────────────────────────────────── + AbilityIconData chainIcon = AbilityIconData.fromChainedAbility(chain); + fieldDefs.add(FieldDef.stringField("gui.texture", chainIcon::getTexture, chainIcon::setTexture).tab("Icon")); + fieldDefs.add(FieldDef.section("ability.icon.section.uv").tab("Icon")); + fieldDefs.add(FieldDef.intField("ability.icon.x", chainIcon::getIconX, chainIcon::setIconX).tab("Icon").range(0, 4096)); + fieldDefs.add(FieldDef.intField("ability.icon.y", chainIcon::getIconY, chainIcon::setIconY).tab("Icon").range(0, 4096)); + fieldDefs.add(FieldDef.section("gui.size").tab("Icon")); + fieldDefs.add(FieldDef.intField("gui.width", chainIcon::getWidth, chainIcon::setWidth).tab("Icon").range(1, 256)); + fieldDefs.add(FieldDef.intField("gui.height", chainIcon::getHeight, chainIcon::setHeight).tab("Icon").range(1, 256)); + fieldDefs.add(FieldDef.floatField("gui.scale", chainIcon::getScale, chainIcon::setScale).tab("Icon").range(0.1f, 10.0f)); + + // External field providers (e.g., DBC Addon injecting a "DBC" tab) if (AbilityController.Instance != null) { for (IChainedAbilityFieldProvider provider : AbilityController.Instance.getChainedFieldProviders()) { provider.addFieldDefinitions(chain, fieldDefs); diff --git a/src/main/java/noppes/npcs/client/gui/hud/CompassHudComponent.java b/src/main/java/noppes/npcs/client/gui/hud/CompassHudComponent.java index aa6d9c21a..cd5ddb2dd 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/CompassHudComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/CompassHudComponent.java @@ -81,10 +81,10 @@ public class CompassHudComponent extends HudComponent { ConfigClient.CompassEnabledProperty.set(ConfigClient.CompassEnabled); ConfigClient.CompassOverlayX = posX; - ConfigClient.CompassOverlayXProperty.set(ConfigClient.CompassOverlayX); + ConfigClient.CompassOverlayXProperty.set((double) ConfigClient.CompassOverlayX); ConfigClient.CompassOverlayY = posY; - ConfigClient.CompassOverlayYProperty.set(ConfigClient.CompassOverlayY); + ConfigClient.CompassOverlayYProperty.set((double) ConfigClient.CompassOverlayY); ConfigClient.CompassOverlayScale = scale; ConfigClient.CompassOverlayScaleProperty.set(ConfigClient.CompassOverlayScale); diff --git a/src/main/java/noppes/npcs/client/gui/hud/EnumHudComponent.java b/src/main/java/noppes/npcs/client/gui/hud/EnumHudComponent.java index fe6eb07b6..7eda9a335 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/EnumHudComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/EnumHudComponent.java @@ -2,5 +2,6 @@ package noppes.npcs.client.gui.hud; public enum EnumHudComponent { QuestTracker, - QuestCompass + QuestCompass, + AbilityHotbar } diff --git a/src/main/java/noppes/npcs/client/gui/hud/GuiHudEditor.java b/src/main/java/noppes/npcs/client/gui/hud/GuiHudEditor.java index 5908cf9b8..d36727aad 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/GuiHudEditor.java +++ b/src/main/java/noppes/npcs/client/gui/hud/GuiHudEditor.java @@ -173,7 +173,8 @@ public class GuiHudEditor extends GuiScreen { int absHeight = (int) (hud.overlayHeight * effectiveScale); if (mouseX >= actualX && mouseX <= actualX + absWidth && mouseY >= actualY && mouseY <= actualY + absHeight) { - if (mouseX >= actualX + absWidth - HANDLE_SIZE && mouseY >= actualY + absHeight - HANDLE_SIZE) { + int scaledHandle = Math.max(8, (int) (HANDLE_SIZE * effectiveScale)); + if (mouseX >= actualX + absWidth - scaledHandle && mouseY >= actualY + absHeight - scaledHandle) { resizing = true; selectedComponent = hud; initialScale = hud.scale; @@ -214,12 +215,14 @@ public class GuiHudEditor extends GuiScreen { int newY = scaledHeight - (Mouse.getY() * scaledHeight / mc.displayHeight) - dragOffsetY; if (newX < 0) newX = 0; if (newY < 0) newY = 0; - if (newX + (int) (selectedComponent.overlayWidth * effectiveScale) > scaledWidth) - newX = scaledWidth - (int) (selectedComponent.overlayWidth * effectiveScale); - if (newY + (int) (selectedComponent.overlayHeight * effectiveScale) > scaledHeight) - newY = scaledHeight - (int) (selectedComponent.overlayHeight * effectiveScale); - selectedComponent.posX = (int) (100F * newX / scaledWidth); - selectedComponent.posY = (int) (100F * newY / scaledHeight); + int compW = (int) Math.ceil(selectedComponent.overlayWidth * effectiveScale); + int compH = (int) Math.ceil(selectedComponent.overlayHeight * effectiveScale); + if (newX + compW > scaledWidth) + newX = scaledWidth - compW; + if (newY + compH > scaledHeight) + newY = scaledHeight - compH; + selectedComponent.posX = 100F * newX / scaledWidth; + selectedComponent.posY = 100F * newY / scaledHeight; } if (resizing) { int currentMouseX = Mouse.getX() * width / mc.displayWidth; @@ -260,8 +263,8 @@ public class GuiHudEditor extends GuiScreen { } int centerX = (res.getScaledWidth() - compWidth) / 2; int centerY = (res.getScaledHeight() - compHeight) / 2; - selectedComponent.posX = (int) (100F * centerX / res.getScaledWidth()); - selectedComponent.posY = (int) (100F * centerY / res.getScaledHeight()); + selectedComponent.posX = 100F * centerX / res.getScaledWidth(); + selectedComponent.posY = 100F * centerY / res.getScaledHeight(); updateCustomButtons(); } } else if (button.id == CYCLE_LEFT_ID) { diff --git a/src/main/java/noppes/npcs/client/gui/hud/HudComponent.java b/src/main/java/noppes/npcs/client/gui/hud/HudComponent.java index 53a7a2170..bc153f890 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/HudComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/HudComponent.java @@ -8,7 +8,8 @@ import java.util.List; public abstract class HudComponent { // Stored as percentages (0-100) of the current screen resolution. - protected int posX = 50, posY = 50, scale = 100, textAlign = 0; + public float posX = 50, posY = 50; + protected int scale = 100, textAlign = 0; public boolean enabled = true; public boolean hasData = false; diff --git a/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java b/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java index 8ed5ece3a..ee6a6fe51 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/QuestTrackingComponent.java @@ -113,10 +113,10 @@ public class QuestTrackingComponent extends HudComponent { @Override public void save() { ConfigClient.QuestOverlayX = posX; - ConfigClient.QuestOverlayXProperty.set(ConfigClient.QuestOverlayX); + ConfigClient.QuestOverlayXProperty.set((double) ConfigClient.QuestOverlayX); ConfigClient.QuestOverlayY = posY; - ConfigClient.QuestOverlayYProperty.set(ConfigClient.QuestOverlayY); + ConfigClient.QuestOverlayYProperty.set((double) ConfigClient.QuestOverlayY); ConfigClient.QuestOverlayScale = scale; ConfigClient.QuestOverlayScaleProperty.set(ConfigClient.QuestOverlayScale); @@ -218,8 +218,8 @@ public class QuestTrackingComponent extends HudComponent { int compHeight = (int) (overlayHeight * effectiveScale); int centerX = (res.getScaledWidth() - compWidth) / 2; int centerY = (res.getScaledHeight() - compHeight) / 2; - posX = (int) (100F * centerX / res.getScaledWidth()); - posY = (int) (100F * centerY / res.getScaledHeight()); + posX = 100F * centerX / res.getScaledWidth(); + posY = 100F * centerY / res.getScaledHeight(); } else { super.onEditorButtonPressed(button); } diff --git a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java new file mode 100644 index 000000000..56189a698 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java @@ -0,0 +1,637 @@ +package noppes.npcs.client.gui.hud.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSelectPacket; +import net.minecraft.client.Minecraft; +import noppes.npcs.client.ClientProxy; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.gui.hud.HudComponent; +import noppes.npcs.config.ConfigClient; +import noppes.npcs.controllers.data.AbilityHotbarData; +import noppes.npcs.controllers.data.PlayerAbilityHotbarData; +import noppes.npcs.controllers.data.PlayerData; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +@SideOnly(Side.CLIENT) +public class AbilityHotbarComponent extends HudComponent { + public static final int TOTAL_SLOTS = AbilityHotbarData.TOTAL_SLOTS; + + private static final int BASE_SLOT_SIZE = 24; + private static final int BASE_SPACING = 28; + + private AbilityHotbarSlotRenderer[] slots = new AbilityHotbarSlotRenderer[TOTAL_SLOTS]; + + // Filtered carousel: only filled slots + one deselect entry + // Each entry is a raw slot index (0-11), or -1 for the deselect slot + private int[] activeIndices = new int[0]; + private int activeCenter = 0; + + private float scrollOffset = 0f; + private long scrollStartTime = 0; + private static final long SCROLL_DURATION = 150; + + private long lastUpdateTime = 0; + private static final long UPDATE_INTERVAL = 500; + + // Debounce: delay sending selection packet + private String pendingSelectKey = null; + private long pendingSelectTime = 0; + private static final long SELECT_DEBOUNCE = 300; + + // Grace period: don't let server state override local state after user interaction + private long lastUserCycleTime = 0; + private static final long USER_CYCLE_GRACE = 1000; + + // Fade: when ShowAlways=false, fade in/out when HUD key is held + private float fadeAlpha = 0f; + private static final float FADE_SPEED = 8f; // per second — ~125ms full transition + + private Minecraft mc; + + public AbilityHotbarComponent(Minecraft mc) { + this.mc = mc; + updateOverlayDimensions(); + for (int i = 0; i < TOTAL_SLOTS; i++) { + slots[i] = new AbilityHotbarSlotRenderer(i); + } + load(); + } + + private int getEffectiveVisibleSlots() { + int configMax = ConfigClient.AbilityHotbarVisibleSlots; + if (configMax != 3 && configMax != 5 && configMax != 7) configMax = 5; + int filledCount = activeIndices.length - 1; // subtract deselect slot + if (filledCount < 0) filledCount = 0; + if (filledCount < 5) return Math.min(configMax, 3); + if (filledCount < 7) return Math.min(configMax, 5); + return configMax; + } + + private void updateOverlayDimensions() { + boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal; + int visibleSlots = getEffectiveVisibleSlots(); + int span = BASE_SPACING * (visibleSlots - 1) + BASE_SLOT_SIZE; + int thick = BASE_SLOT_SIZE + 4; + if (isHorizontal) { + overlayWidth = span; + overlayHeight = thick; + } else { + overlayWidth = thick; + overlayHeight = span; + } + } + + @Override + public void loadData(NBTTagCompound compound) { + } + + @Override + public void load() { + posX = ConfigClient.AbilityHotbarX; + posY = ConfigClient.AbilityHotbarY; + scale = ConfigClient.AbilityHotbarScale; + enabled = ConfigClient.AbilityHotbarEnabled; + } + + @Override + public void save() { + ConfigClient.AbilityHotbarX = posX; + ConfigClient.AbilityHotbarXProperty.set((double) posX); + + ConfigClient.AbilityHotbarY = posY; + ConfigClient.AbilityHotbarYProperty.set((double) posY); + + ConfigClient.AbilityHotbarScale = scale; + ConfigClient.AbilityHotbarScaleProperty.set(scale); + + ConfigClient.AbilityHotbarEnabled = enabled; + ConfigClient.AbilityHotbarEnabledProperty.set(enabled); + + ConfigClient.AbilityHotbarHorizontalProperty.set(ConfigClient.AbilityHotbarHorizontal); + ConfigClient.AbilityHotbarAltTextureProperty.set(ConfigClient.AbilityHotbarAltTexture); + ConfigClient.AbilityHotbarTextPositionProperty.set(ConfigClient.AbilityHotbarTextPosition); + ConfigClient.AbilityHotbarVisibleSlotsProperty.set(ConfigClient.AbilityHotbarVisibleSlots); + ConfigClient.AbilityHotbarShowAlwaysProperty.set(ConfigClient.AbilityHotbarShowAlways); + + if (ConfigClient.config.hasChanged()) { + ConfigClient.config.save(); + } + } + + @Override + public void renderOnScreen(float partialTicks) { + if (!enabled || isEditting) return; + if (mc.thePlayer == null || mc.currentScreen != null) return; + if (!hasAnyAbilities()) return; + + // Fade logic: when ShowAlways=false, only show while HUD key is held + boolean showAlways = ConfigClient.AbilityHotbarShowAlways; + if (!showAlways) { + boolean hudKeyHeld = isHudKeyHeld(); + float dt = partialTicks > 0 ? 1f / 20f : 0.05f; // approximate frame time + if (hudKeyHeld || pendingSelectKey != null || scrollOffset != 0f) { + fadeAlpha = Math.min(1f, fadeAlpha + FADE_SPEED * dt); + } else { + fadeAlpha = Math.max(0f, fadeAlpha - FADE_SPEED * dt); + } + if (fadeAlpha <= 0f) return; + } else { + fadeAlpha = 1f; + } + + long currentTime = Minecraft.getSystemTime(); + if (currentTime - lastUpdateTime > UPDATE_INTERVAL) { + updateAbilities(); + lastUpdateTime = currentTime; + } + + // Flush debounced selection packet + if (pendingSelectKey != null && currentTime - pendingSelectTime >= SELECT_DEBOUNCE) { + PacketHandler.Instance.sendToServer(new AbilityHotbarSelectPacket(pendingSelectKey)); + pendingSelectKey = null; + } + + updateScrollAnimation(); + updateOverlayDimensions(); + + if (activeIndices.length == 0) return; + + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal; + float effectiveScale = getEffectiveScale(sr); + int visibleSlots = getEffectiveVisibleSlots(); + int half = visibleSlots / 2; + + int actualX = (int) (posX / 100.0f * sr.getScaledWidth()); + int actualY = (int) (posY / 100.0f * sr.getScaledHeight()); + + GL11.glPushMatrix(); + GL11.glTranslatef(actualX, actualY, 0); + GL11.glScalef(effectiveScale, effectiveScale, 1); + + int centerX = overlayWidth / 2; + int centerY = overlayHeight / 2; + + float cooldownProgress = 0; + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData != null && playerData.abilityData != null) { + if (playerData.abilityData.isOnCooldown()) { + cooldownProgress = 0.5f; + } + } + + for (int i = 0; i < TOTAL_SLOTS; i++) { + slots[i].updateNameFade(); + } + + for (int offset = -half; offset <= half; offset++) { + int activeIdx = ((activeCenter + offset) % activeIndices.length + activeIndices.length) % activeIndices.length; + int rawSlotIndex = activeIndices[activeIdx]; + + float visualPos = offset + scrollOffset; + float absDist = Math.abs(visualPos); + + float slotScale; + if (absDist <= 1f) { + slotScale = 1.0f - 0.25f * absDist; + } else { + slotScale = 0.75f - 0.15f * (absDist - 1f); + } + slotScale = Math.max(0.1f, slotScale); + + int scaledSize = Math.max(1, (int) (BASE_SLOT_SIZE * slotScale)); + + int cx, cy; + if (isHorizontal) { + cx = centerX + (int) (visualPos * BASE_SPACING); + cy = centerY; + } else { + cx = centerX; + cy = centerY + (int) (visualPos * BASE_SPACING); + } + + float maxDist = half + 0.5f; + if (absDist > maxDist) continue; + + float edgeAlpha = absDist > half - 0.5f ? 1f - (absDist - (half - 0.5f)) : 1f; + float slotAlpha = edgeAlpha * fadeAlpha; + boolean isCenter = (offset == 0 && scrollOffset == 0f); + float slotCooldown = isCenter ? cooldownProgress : 0; + + if (rawSlotIndex == -1) { + // Deselect slot — draw empty marker + drawDeselectSlot(cx, cy, scaledSize, isCenter, slotAlpha); + } else { + slots[rawSlotIndex].drawCarousel(mc, sr, slotCooldown, cx, cy, scaledSize, isCenter, slotAlpha); + } + } + + GL11.glPopMatrix(); + } + + private void drawDeselectSlot(int cx, int cy, int size, boolean isCenter, float alpha) { + float radius = size / 2f; + float baseAlpha = isCenter ? 0.9f : 0.6f; + float a = baseAlpha * alpha; + boolean altTexture = ConfigClient.AbilityHotbarAltTexture; + + GL11.glPushMatrix(); + GL11.glTranslatef(cx, cy, 0); + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + if (isCenter) { + GL11.glColor4f(0.5f, 0.2f, 0.2f, a); + } else { + GL11.glColor4f(0.3f, 0.3f, 0.3f, a); + } + + if (altTexture) { + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex2f(-radius, -radius); + GL11.glVertex2f(radius, -radius); + GL11.glVertex2f(radius, radius); + GL11.glVertex2f(-radius, radius); + GL11.glEnd(); + } else { + GL11.glBegin(GL11.GL_TRIANGLE_FAN); + GL11.glVertex2f(0, 0); + for (int i = 0; i <= 32; i++) { + double angle = 2 * Math.PI * i / 32; + GL11.glVertex2f((float) (Math.cos(angle) * radius), (float) (Math.sin(angle) * radius)); + } + GL11.glEnd(); + } + + // Draw X mark + if (isCenter) { + GL11.glColor4f(0.8f, 0.4f, 0.4f, alpha); + } else { + GL11.glColor4f(0.5f, 0.5f, 0.5f, alpha * 0.8f); + } + float xSize = radius * 0.4f; + GL11.glLineWidth(2.0f); + GL11.glBegin(GL11.GL_LINES); + GL11.glVertex2f(-xSize, -xSize); + GL11.glVertex2f(xSize, xSize); + GL11.glVertex2f(-xSize, xSize); + GL11.glVertex2f(xSize, -xSize); + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + GL11.glPopMatrix(); + } + + @Override + public void renderEditing() { + isEditting = true; + updateOverlayDimensions(); + + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + float effectiveScale = getEffectiveScale(sr); + boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal; + FontRenderer fr = mc.fontRenderer; + int visibleSlots = getEffectiveVisibleSlots(); + int half = visibleSlots / 2; + + int actualX = (int) (posX / 100.0f * sr.getScaledWidth()); + int actualY = (int) (posY / 100.0f * sr.getScaledHeight()); + + GL11.glPushMatrix(); + GL11.glTranslatef(actualX, actualY, 0); + GL11.glScalef(effectiveScale, effectiveScale, 1); + + int centerX = overlayWidth / 2; + int centerY = overlayHeight / 2; + + int[] slotCx = new int[visibleSlots]; + int[] slotCy = new int[visibleSlots]; + int[] slotSizes = new int[visibleSlots]; + + for (int offset = -half; offset <= half; offset++) { + float absDist = Math.abs(offset); + float slotScale; + if (absDist <= 1f) { + slotScale = 1.0f - 0.25f * absDist; + } else { + slotScale = 0.75f - 0.15f * (absDist - 1f); + } + slotScale = Math.max(0.1f, slotScale); + int scaledSize = Math.max(1, (int) (BASE_SLOT_SIZE * slotScale)); + + int cx, cy; + if (isHorizontal) { + cx = centerX + offset * BASE_SPACING; + cy = centerY; + } else { + cx = centerX; + cy = centerY + offset * BASE_SPACING; + } + + int idx = offset + half; + slotCx[idx] = cx; + slotCy[idx] = cy; + slotSizes[idx] = scaledSize; + + float radius = scaledSize / 2f; + boolean altTexture = ConfigClient.AbilityHotbarAltTexture; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + float alpha = offset == 0 ? 0.9f : 0.6f; + if (offset == 0) { + GL11.glColor4f(0.8f, 0.8f, 0.2f, alpha); + } else { + GL11.glColor4f(0.4f, 0.4f, 0.4f, alpha); + } + + if (altTexture) { + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex2f(cx - radius, cy - radius); + GL11.glVertex2f(cx + radius, cy - radius); + GL11.glVertex2f(cx + radius, cy + radius); + GL11.glVertex2f(cx - radius, cy + radius); + GL11.glEnd(); + } else { + GL11.glBegin(GL11.GL_TRIANGLE_FAN); + GL11.glVertex2f(cx, cy); + for (int i = 0; i <= 32; i++) { + double angle = 2 * Math.PI * i / 32; + GL11.glVertex2f(cx + (float) (Math.cos(angle) * radius), cy + (float) (Math.sin(angle) * radius)); + } + GL11.glEnd(); + } + } + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + for (int i = 0; i < visibleSlots; i++) { + String label = String.valueOf(i + 1); + float labelScale = slotSizes[i] / 24.0f; + if (labelScale < 0.4f) continue; + + int textColor = (i == half) ? 0xFFFFFFFF : 0xAABBBBBB; + int textW = fr.getStringWidth(label); + + GL11.glPushMatrix(); + GL11.glTranslatef(slotCx[i], slotCy[i], 0); + GL11.glScalef(labelScale, labelScale, 1); + fr.drawString(label, -textW / 2, -fr.FONT_HEIGHT / 2, textColor); + GL11.glPopMatrix(); + } + + boolean altTexture = ConfigClient.AbilityHotbarAltTexture; + String modeLabel = altTexture ? "Square" : "Circle"; + int modeLabelW = fr.getStringWidth(modeLabel); + fr.drawStringWithShadow(modeLabel, overlayWidth / 2 - modeLabelW / 2, overlayHeight + 2, 0xAA888888); + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(0.2f, 0.8f, 0.2f, 0.8f); + GL11.glLineWidth(1.0f); + GL11.glBegin(GL11.GL_LINE_LOOP); + GL11.glVertex2f(0, 0); + GL11.glVertex2f(overlayWidth, 0); + GL11.glVertex2f(overlayWidth, overlayHeight); + GL11.glVertex2f(0, overlayHeight); + GL11.glEnd(); + + GL11.glColor4f(0.8f, 0.8f, 0.8f, 0.9f); + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex2f(overlayWidth - 10, overlayHeight - 10); + GL11.glVertex2f(overlayWidth, overlayHeight - 10); + GL11.glVertex2f(overlayWidth, overlayHeight); + GL11.glVertex2f(overlayWidth - 10, overlayHeight); + GL11.glEnd(); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + + GL11.glPopMatrix(); + } + + @Override + public void addEditorButtons(List buttonList) { + super.addEditorButtons(buttonList); + String orientLabel = ConfigClient.AbilityHotbarHorizontal ? "Horizontal" : "Vertical"; + buttonList.add(new GuiButton(1, 0, 0, 120, 20, orientLabel)); + + String texLabel = ConfigClient.AbilityHotbarAltTexture ? "Square" : "Circle"; + buttonList.add(new GuiButton(2, 0, 0, 120, 20, texLabel)); + + buttonList.add(new GuiButton(3, 0, 0, 120, 20, getTextPositionLabel())); + buttonList.add(new GuiButton(4, 0, 0, 120, 20, "Slots: " + ConfigClient.AbilityHotbarVisibleSlots)); + buttonList.add(new GuiButton(5, 0, 0, 120, 20, ConfigClient.AbilityHotbarShowAlways ? "Show: Always" : "Show: Hold Key")); + } + + @Override + public void onEditorButtonPressed(GuiButton button) { + if (button.id == 1) { + ConfigClient.AbilityHotbarHorizontal = !ConfigClient.AbilityHotbarHorizontal; + button.displayString = ConfigClient.AbilityHotbarHorizontal ? "Horizontal" : "Vertical"; + updateOverlayDimensions(); + } else if (button.id == 2) { + ConfigClient.AbilityHotbarAltTexture = !ConfigClient.AbilityHotbarAltTexture; + button.displayString = ConfigClient.AbilityHotbarAltTexture ? "Square" : "Circle"; + } else if (button.id == 3) { + ConfigClient.AbilityHotbarTextPosition = (ConfigClient.AbilityHotbarTextPosition + 1) % 3; + button.displayString = getTextPositionLabel(); + } else if (button.id == 4) { + int current = ConfigClient.AbilityHotbarVisibleSlots; + if (current <= 3) ConfigClient.AbilityHotbarVisibleSlots = 5; + else if (current <= 5) ConfigClient.AbilityHotbarVisibleSlots = 7; + else ConfigClient.AbilityHotbarVisibleSlots = 3; + button.displayString = "Slots: " + ConfigClient.AbilityHotbarVisibleSlots; + updateOverlayDimensions(); + } else if (button.id == 5) { + ConfigClient.AbilityHotbarShowAlways = !ConfigClient.AbilityHotbarShowAlways; + button.displayString = ConfigClient.AbilityHotbarShowAlways ? "Show: Always" : "Show: Hold Key"; + } else { + super.onEditorButtonPressed(button); + } + } + + private String getTextPositionLabel() { + boolean h = ConfigClient.AbilityHotbarHorizontal; + switch (ConfigClient.AbilityHotbarTextPosition) { + case 0: return "Text: Hide"; + case 1: return h ? "Text: Above" : "Text: Left"; + case 2: return h ? "Text: Below" : "Text: Right"; + default: return "Text: Hide"; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // Ability Update & Scroll + // ═══════════════════════════════════════════════════════════════════ + + private void updateAbilities() { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData == null) return; + + PlayerAbilityHotbarData hotbarData = playerData.hotbarData; + if (hotbarData == null) return; + + for (int i = 0; i < TOTAL_SLOTS; i++) { + AbilityHotbarData slotData = hotbarData.slots[i]; + String key = slotData.isEmpty() ? null : slotData.abilityKey; + Ability ability = null; + IAbilityAction action = null; + if (key != null && AbilityController.Instance != null) { + if (slotData.isChainKey()) { + action = AbilityController.Instance.resolveChainedAbility(slotData.getResolveKey()); + } else { + ability = AbilityController.Instance.resolveAbility(key); + action = ability; + } + if (ability == null && action == null) key = null; + } + slots[i].setAbility(key, ability, action); + } + + // Rebuild active indices: filled slots + one deselect slot (-1) + ArrayList active = new ArrayList<>(); + for (int i = 0; i < TOTAL_SLOTS; i++) { + if (slots[i].abilityKey != null) { + active.add(i); + } + } + active.add(-1); // Deselect slot at the end + activeIndices = new int[active.size()]; + for (int i = 0; i < active.size(); i++) { + activeIndices[i] = active.get(i); + } + + // Clamp activeCenter + if (activeCenter >= activeIndices.length) { + activeCenter = 0; + } + + // Don't override local selection state while user is scrolling, debounce is pending, + // or within the grace period after the last user interaction. + // This prevents the server state from fighting the local carousel position. + long now = Minecraft.getSystemTime(); + if (pendingSelectKey != null || scrollOffset != 0f || (now - lastUserCycleTime < USER_CYCLE_GRACE)) { + return; + } + + // Sync selected state and find which active index is selected + if (playerData.abilityData != null) { + String selectedKey = playerData.abilityData.getSelectedAbilityKey(); + boolean foundSelected = false; + for (int i = 0; i < TOTAL_SLOTS; i++) { + AbilityHotbarData slotData = hotbarData.slots[i]; + boolean sel = selectedKey != null && !slotData.isEmpty() + && selectedKey.equals(slotData.abilityKey); + slots[i].setSelectedState(sel); + if (sel) { + // Find this slot's position in activeIndices + for (int j = 0; j < activeIndices.length; j++) { + if (activeIndices[j] == i) { + activeCenter = j; + foundSelected = true; + break; + } + } + } + } + if (!foundSelected) { + // No ability selected on server — center on deselect slot + for (int j = 0; j < activeIndices.length; j++) { + if (activeIndices[j] == -1) { + activeCenter = j; + break; + } + } + } + } + } + + private boolean isHudKeyHeld() { + if (ClientProxy.AbilityHudKey == null) return false; + int keyCode = ClientProxy.AbilityHudKey.getKeyCode(); + return keyCode != 0 && org.lwjgl.input.Keyboard.isKeyDown(keyCode); + } + + public boolean hasAnyAbilities() { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData == null || playerData.hotbarData == null) return false; + return playerData.hotbarData.hasAnyAbilities(); + } + + public void onCycleNext() { + cycle(1); + } + + public void onCyclePrev() { + cycle(-1); + } + + private void cycle(int direction) { + if (!enabled) return; + if (mc.thePlayer == null || mc.currentScreen != null) return; + if (activeIndices.length == 0) return; + + activeCenter = ((activeCenter + direction) % activeIndices.length + activeIndices.length) % activeIndices.length; + scrollOffset = direction; + long now = Minecraft.getSystemTime(); + scrollStartTime = now; + lastUserCycleTime = now; + + int rawSlotIndex = activeIndices[activeCenter]; + + // Update selection states locally + for (int i = 0; i < TOTAL_SLOTS; i++) { + slots[i].setSelectedState(i == rawSlotIndex); + } + + // Debounce: queue the selection, don't send immediately + String keyToSend = rawSlotIndex >= 0 && slots[rawSlotIndex].abilityKey != null + ? slots[rawSlotIndex].abilityKey : ""; + pendingSelectKey = keyToSend; + pendingSelectTime = Minecraft.getSystemTime(); + } + + private void updateScrollAnimation() { + if (scrollOffset == 0f) return; + + long now = Minecraft.getSystemTime(); + float t = (float) (now - scrollStartTime) / SCROLL_DURATION; + t = Math.min(t, 1f); + + float eased = easeOutCubic(t); + float initialOffset = scrollOffset < 0 ? -1f : 1f; + scrollOffset = initialOffset * (1f - eased); + + if (t >= 1f) scrollOffset = 0f; + } + + private float easeOutCubic(float x) { + float v = 1 - x; + return 1 - v * v * v; + } + + public void refresh() { + updateAbilities(); + lastUpdateTime = Minecraft.getSystemTime(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarSlotRenderer.java b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarSlotRenderer.java new file mode 100644 index 000000000..f23ac583a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarSlotRenderer.java @@ -0,0 +1,262 @@ +package noppes.npcs.client.gui.hud.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.ScaledResolution; +import noppes.npcs.config.ConfigClient; +import noppes.npcs.controllers.data.PlayerData; +import org.lwjgl.opengl.GL11; + +@SideOnly(Side.CLIENT) +public class AbilityHotbarSlotRenderer extends Gui { + private static final int CIRCLE_SEGMENTS = 32; + private static final long NAME_FADE_DURATION = 180; + + public int index; + public String abilityKey = null; + public Ability ability = null; + public IAbilityAction action = null; + private AbilityIcon icon = null; + + public boolean isSelected = false; + public float nameAlpha = 0f; + private boolean nameFadingIn = false; + private long nameFadeStartTime = 0; + + public AbilityHotbarSlotRenderer(int index) { + this.index = index; + } + + public void setAbility(String key, Ability ability, IAbilityAction action) { + this.abilityKey = key; + this.ability = ability; + this.action = action; + if (ability != null) { + this.icon = AbilityIcon.fromAbility(ability); + } else if (action instanceof ChainedAbility) { + this.icon = AbilityIcon.fromChainedAbility((ChainedAbility) action); + } else { + this.icon = null; + } + } + + public void drawCarousel(Minecraft mc, ScaledResolution sr, float cooldownProgress, + int cx, int cy, int size, boolean isCenter, float slotAlpha) { + float nameAlpha = this.nameAlpha; + boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal; + if (size <= 0 || slotAlpha <= 0) return; + float radius = size / 2f; + float baseAlpha = isCenter ? 0.9f : 0.6f; + float alpha = baseAlpha * slotAlpha; + + GL11.glPushMatrix(); + GL11.glTranslatef(cx, cy, 0); + + boolean altTexture = ConfigClient.AbilityHotbarAltTexture; + + if (altTexture) { + drawRoundedRect(-radius, -radius, radius, radius, 0.6f, 0.6f, 0.6f, 0.75f * alpha, false); + if (isCenter) { + drawRoundedRect(-radius, -radius, radius, radius, 0.8f, 0.8f, 0.2f, alpha, true); + } else { + drawRoundedRect(-radius, -radius, radius, radius, 0.4f, 0.4f, 0.4f, alpha * 0.8f, true); + } + } else { + if (isCenter) { + drawCircle(radius, 0.8f, 0.8f, 0.2f, 0.66f * alpha, false); + drawCircle(radius, 0.8f, 0.8f, 0.2f, alpha, true); + } else { + drawCircle(radius, 1f, 1f, 1f, 0.66f * alpha, false); + drawCircle(radius, 0.4f, 0.4f, 0.4f, alpha * 0.8f, true); + } + } + + // Ability icon + if (icon != null) { + GL11.glPushMatrix(); + float targetSize = size * 0.55f; + float iconNaturalSize = Math.max(icon.width, icon.height); + if (iconNaturalSize <= 0) iconNaturalSize = 16f; + float iconScale = targetSize / iconNaturalSize; + GL11.glScalef(iconScale, iconScale, 1); + GL11.glColor4f(1, 1, 1, slotAlpha); + icon.draw(getToggleState()); + GL11.glColor4f(1, 1, 1, 1); + GL11.glPopMatrix(); + } + + // Cooldown arc + if (cooldownProgress > 0 && abilityKey != null) { + drawCooldownArc(radius, cooldownProgress); + } + + GL11.glPopMatrix(); + + // Ability name on center slot (respects text position config) + int textPos = ConfigClient.AbilityHotbarTextPosition; + if (textPos != 0 && isCenter && action != null && nameAlpha > 0) { + FontRenderer fr = mc.fontRenderer; + String name = getAbilityName(); + if (name != null && !name.isEmpty()) { + int a = (int) (nameAlpha * 255) & 0xFF; + int nameColor = (a << 24) | (getNameColor() & 0x00FFFFFF); + int nameX, nameY; + if (isHorizontal) { + nameX = cx - fr.getStringWidth(name) / 2; + if (textPos == 1) { + nameY = cy - (int) radius - fr.FONT_HEIGHT - 2; // Above + } else { + nameY = cy + (int) radius + 2; // Below + } + } else { + nameY = cy - fr.FONT_HEIGHT / 2; + if (textPos == 1) { + nameX = cx - (int) radius - fr.getStringWidth(name) - 4; // Left + } else { + nameX = cx + (int) radius + 4; // Right + } + } + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(1, 1, 1, nameAlpha); + fr.drawStringWithShadow(name, nameX, nameY, nameColor); + GL11.glDisable(GL11.GL_BLEND); + GL11.glColor4f(1, 1, 1, 1); + } + } + } + + private void drawRoundedRect(float x1, float y1, float x2, float y2, + float r, float g, float b, float a, boolean outline) { + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + + if (outline) { + GL11.glLineWidth(2.0f); + GL11.glBegin(GL11.GL_LINE_LOOP); + } else { + GL11.glBegin(GL11.GL_QUADS); + } + + GL11.glVertex2f(x1, y1); + GL11.glVertex2f(x2, y1); + GL11.glVertex2f(x2, y2); + GL11.glVertex2f(x1, y2); + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glColor4f(1, 1, 1, 1); + } + + private void drawCircle(float radius, float r, float g, float b, float a, boolean outline) { + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + + if (outline) { + GL11.glLineWidth(1.5f); + GL11.glBegin(GL11.GL_LINE_LOOP); + } else { + GL11.glBegin(GL11.GL_TRIANGLE_FAN); + GL11.glVertex2f(0, 0); + } + + for (int i = 0; i <= CIRCLE_SEGMENTS; i++) { + double angle = 2 * Math.PI * i / CIRCLE_SEGMENTS; + GL11.glVertex2f((float) (Math.cos(angle) * radius), (float) (Math.sin(angle) * radius)); + } + GL11.glEnd(); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glColor4f(1, 1, 1, 1); + } + + private void drawCooldownArc(float radius, float progress) { + progress = Math.min(1, Math.max(0, progress)); + if (progress <= 0) return; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(0.1f, 0.1f, 0.1f, 0.7f); + + GL11.glBegin(GL11.GL_TRIANGLE_FAN); + GL11.glVertex2f(0, 0); + int steps = (int) (CIRCLE_SEGMENTS * progress) + 1; + for (int i = 0; i <= steps; i++) { + double angle = -Math.PI / 2 + 2 * Math.PI * i * progress / steps; + GL11.glVertex2f((float) (Math.cos(angle) * radius), (float) (Math.sin(angle) * radius)); + } + GL11.glEnd(); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + } + + private String getAbilityName() { + if (ability != null) { + int state = getToggleState(); + if (state > 0) { + String label = ability.getToggleStateLabel(state); + if (label != null) return label; + } + return ability.getDisplayName(); + } + if (action instanceof ChainedAbility) return ((ChainedAbility) action).getDisplayName(); + return action != null ? action.getName() : ""; + } + + private int getToggleState() { + if (ability == null || !ability.isToggleable()) return 0; + Minecraft mc = Minecraft.getMinecraft(); + if (mc.thePlayer == null) return 0; + PlayerData playerData = PlayerData.get(mc.thePlayer); + if (playerData == null || playerData.abilityData == null) return 0; + return playerData.abilityData.getToggleState(abilityKey); + } + + private int getNameColor() { + if (ability != null && ability.isToggleable()) { + return getToggleState() > 0 ? 0xFF55FF55 : 0xFFFF5555; + } + return 0xFFFFFFFF; + } + + public void setSelectedState(boolean newSelectState) { + if (!isSelected && newSelectState) { + nameFadingIn = true; + nameFadeStartTime = Minecraft.getSystemTime(); + } + if (isSelected && !newSelectState) { + nameFadingIn = false; + nameFadeStartTime = Minecraft.getSystemTime(); + } + isSelected = newSelectState; + } + + public void updateNameFade() { + long now = Minecraft.getSystemTime(); + float t = (float) (now - nameFadeStartTime) / NAME_FADE_DURATION; + t = Math.min(1f, Math.max(0f, t)); + if (nameFadingIn) { + nameAlpha = t; + } else { + nameAlpha = 1f - t; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java new file mode 100644 index 000000000..1371bcc2a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java @@ -0,0 +1,140 @@ +package noppes.npcs.client.gui.hud.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityIconData; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.renderer.ImageData; +import org.lwjgl.opengl.GL11; + +@SideOnly(Side.CLIENT) +public class AbilityIcon extends Gui { + private static final ResourceLocation FALLBACK_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/ability_fallback.png"); + + // Backed by AbilityIconData + private final AbilityIconData data; + public int width; + public int height; + + private AbilityIcon(AbilityIconData data) { + this.data = data; + this.width = data.width; + this.height = data.height; + } + + public static AbilityIcon fromAbility(Ability ability) { + if (ability != null) { + return new AbilityIcon(AbilityIconData.fromAbility(ability)); + } + return fromDefaults(); + } + + public static AbilityIcon fromChainedAbility(ChainedAbility chain) { + if (chain != null) { + return new AbilityIcon(AbilityIconData.fromChainedAbility(chain)); + } + return fromDefaults(); + } + + public static AbilityIcon fromAction(IAbilityAction action) { + if (action instanceof Ability) return fromAbility((Ability) action); + if (action instanceof ChainedAbility) return fromChainedAbility((ChainedAbility) action); + return fromDefaults(); + } + + private static AbilityIcon fromDefaults() { + return new AbilityIcon(AbilityIconData.fromCustomData(new NBTTagCompound())); + } + + public void draw() { + draw(0); + } + + public void draw(int state) { + TextureManager renderEngine = Minecraft.getMinecraft().renderEngine; + + ImageData imageData = null; + if (data.hasTexture()) { + imageData = ClientCacheHandler.getImageData(data.texture); + } + + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + + Tessellator t; + + if (imageData == null || !imageData.imageLoaded()) { + GL11.glScalef(1.0f, 1.0f, 1); + renderEngine.bindTexture(FALLBACK_TEXTURE); + t = getFallbackTessellator(); + } else { + GL11.glScalef(data.scale, data.scale, 1); + renderEngine.bindTexture(imageData.getLocation()); + t = getTessellator(imageData, state); + } + + t.draw(); + GL11.glPopMatrix(); + } + + private Tessellator getTessellator(ImageData imageData, int state) { + float hw = data.width / 2f; + float hh = data.height / 2f; + + float texW = imageData.getTotalWidth(); + float texH = imageData.getTotalHeight(); + + int ix = data.getIconXForState(state); + int iy = data.getIconYForState(state); + + float u1 = ix / texW; + float v1 = iy / texH; + float u2 = (ix + data.width) / texW; + float v2 = (iy + data.height) / texH; + + Tessellator t = Tessellator.instance; + t.startDrawingQuads(); + t.addVertexWithUV(-hw, hh, zLevel, u1, v2); + t.addVertexWithUV(hw, hh, zLevel, u2, v2); + t.addVertexWithUV(hw, -hh, zLevel, u2, v1); + t.addVertexWithUV(-hw, -hh, zLevel, u1, v1); + return t; + } + + private Tessellator getFallbackTessellator() { + float hw = 16f; + float hh = 16f; + + Tessellator t = Tessellator.instance; + t.startDrawingQuads(); + t.addVertexWithUV(-hw, hh, zLevel, 0, 1); + t.addVertexWithUV(hw, hh, zLevel, 1, 1); + t.addVertexWithUV(hw, -hh, zLevel, 1, 0); + t.addVertexWithUV(-hw, -hh, zLevel, 0, 0); + return t; + } + + public boolean hasTexture() { + return data.hasTexture(); + } + + /** + * Returns the approximate visual size (max dimension) that draw() produces. + * Textured: max(width,height) * scale. Fallback: 32. + */ + public float getDrawSize() { + if (data.hasTexture()) { + return Math.max(width, height) * data.scale; + } + return 32f; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java new file mode 100644 index 000000000..0f66036ae --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java @@ -0,0 +1,984 @@ +package noppes.npcs.client.gui.player.inventory; + +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.IAbilityAction; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSavePacket; +import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSelectPacket; +import kamkeel.npcs.network.packets.player.ability.AbilityTogglePacket; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.gui.hud.ability.AbilityIcon; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.data.AbilityHotbarData; +import noppes.npcs.controllers.data.PlayerData; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; +import tconstruct.client.tabs.AbstractTab; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); + + // Sub-tab: 0 = Abilities, 1 = Toggles + private int subTab = 0; + private static final int BTN_TAB_ABILITIES = -200; + private static final int BTN_TAB_TOGGLES = -201; + + // ═══ Panel Bounds (relative to guiLeft/guiTop) ═══ + // Selection panel (RIGHT side - icon grid) + private int selLeft = 140; + private int selTop = 4; + private int selRight = 316; + private int selBottom = 150; + + // Information panel (LEFT side - ability details) + private int infoLeft = 5; + private int infoTop = 4; + private int infoRight = 137; + private int infoBottom = 150; + + // Hotbar panel (BOTTOM) + private int hbLeft = 5; + private int hbTop = 153; + private int hbRight = 316; + private int hbBottom = 190; + + // ═══ Grid Settings ═══ + private int gridPad = 3; + private int gridCellSize = 22; + private int gridCols = 7; + + // ═══ Hotbar Settings ═══ + private int hbSlotSize = 18; + private int hbSlotGap = 2; + private int hbSlots = AbilityHotbarData.TOTAL_SLOTS; + + // ═══ Info Panel Settings ═══ + private int infoPad = 5; + private int infoIconSize = 40; + + // ═══ Drag Settings ═══ + private int dragThreshold = 3; + + // ═══ Filtered ability data ═══ + private List filteredKeys = new ArrayList<>(); + private List filteredDisplayNames = new ArrayList<>(); + private List filteredIcons = new ArrayList<>(); + private List filteredAbilities = new ArrayList<>(); + private List filteredActions = new ArrayList<>(); + + // ═══ Grid state ═══ + private int scrollRow = 0; + private int hoveredGridIndex = -1; + private float[] gridHoverScale; + private int selectedIndex = -1; + + // ═══ Detail panel cache ═══ + private AbilityIcon cachedDetailIcon = null; + private int cachedDetailIndex = -1; + + // ═══ Hotbar state ═══ + private int hoveredSlotIndex = -1; + private AbilityIcon[] hotbarIcons = new AbilityIcon[AbilityHotbarData.TOTAL_SLOTS]; + + // ═══ Drag state ═══ + private boolean isDragging = false; + private boolean dragPending = false; + private String draggedKey = null; + private AbilityIcon draggedIcon = null; + private int dragSourceSlot = -1; + private int dragStartX, dragStartY; + private int dragMouseX, dragMouseY; + + // ═══ Close sync ═══ + private String initialSelectedKey = null; + + // ═══ Toggle double-tap ═══ + private long lastToggleClickTime = 0; + private int lastToggleClickIndex = -1; + + public GuiAbilities() { + super(); + xSize = 280; + ySize = 180; + + hbSlotSize = 23; + gridCellSize = 24; + gridCols = 7; + } + + @Override + public void initGui() { + super.initGui(); + + GuiMenuTopButton tabAbilities = new GuiMenuTopButton(BTN_TAB_ABILITIES, guiLeft + xSize - 10, guiTop - 17, "gui.abilities"); + tabAbilities.active = (subTab == 0); + addTopButton(tabAbilities); + + GuiMenuTopButton tabToggles = new GuiMenuTopButton(BTN_TAB_TOGGLES, "gui.toggles", tabAbilities); + tabToggles.active = (subTab == 1); + addTopButton(tabToggles); + + buildFilteredList(); + updateHotbarIcons(); + + // Capture initial selected key for close sync + if (initialSelectedKey == null) { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData != null && playerData.abilityData != null) { + initialSelectedKey = playerData.abilityData.getSelectedAbilityKey(); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // Data Building + // ═══════════════════════════════════════════════════════════════════ + + private void buildFilteredList() { + filteredKeys.clear(); + filteredDisplayNames.clear(); + filteredIcons.clear(); + filteredAbilities.clear(); + filteredActions.clear(); + cachedDetailIcon = null; + cachedDetailIndex = -1; + + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData == null || playerData.abilityData == null) return; + + List abilities = playerData.abilityData.getUnlockedAbilityList(); + + // Build temporary lists before sorting + List tempKeys = new ArrayList<>(); + List tempNames = new ArrayList<>(); + List tempIcons = new ArrayList<>(); + List tempAbilities = new ArrayList<>(); + List tempActions = new ArrayList<>(); + + for (String key : abilities) { + Ability ability = null; + IAbilityAction action = null; + + if (key.startsWith(AbilityHotbarData.CHAIN_PREFIX)) { + String chainKey = key.substring(AbilityHotbarData.CHAIN_PREFIX.length()); + if (AbilityController.Instance != null) { + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainKey); + action = chain; + } + } else if (AbilityController.Instance != null) { + ability = AbilityController.Instance.resolveAbility(key); + action = ability; + } + + boolean isToggle = ability != null && ability.isToggleable(); + if (subTab == 0 && isToggle) continue; + if (subTab == 1 && !isToggle) continue; + + String displayName; + if (action instanceof ChainedAbility) { + displayName = "\u00A76\u2726 " + ((ChainedAbility) action).getDisplayName(); + } else if (ability != null) { + displayName = ability.getDisplayName(); + } else { + displayName = key; + } + + tempKeys.add(key); + tempNames.add(displayName); + tempIcons.add(AbilityIcon.fromAction(action)); + tempAbilities.add(ability); + tempActions.add(action); + } + + // Sort: toggles tab sorts enabled first, then alphabetically + List indices = new ArrayList<>(); + for (int i = 0; i < tempNames.size(); i++) indices.add(i); + final boolean sortTogglesFirst = (subTab == 1); + Collections.sort(indices, new Comparator() { + @Override + public int compare(Integer a, Integer b) { + if (sortTogglesFirst && playerData != null && playerData.abilityData != null) { + boolean aToggled = playerData.abilityData.isAbilityToggled(tempKeys.get(a)); + boolean bToggled = playerData.abilityData.isAbilityToggled(tempKeys.get(b)); + if (aToggled != bToggled) return aToggled ? -1 : 1; + } + return stripFormatting(tempNames.get(a)).compareToIgnoreCase(stripFormatting(tempNames.get(b))); + } + }); + + for (int idx : indices) { + filteredKeys.add(tempKeys.get(idx)); + filteredDisplayNames.add(tempNames.get(idx)); + filteredIcons.add(tempIcons.get(idx)); + filteredAbilities.add(tempAbilities.get(idx)); + filteredActions.add(tempActions.get(idx)); + } + + gridHoverScale = new float[filteredKeys.size()]; + Arrays.fill(gridHoverScale, 1.0f); + scrollRow = 0; + selectedIndex = -1; + } + + private String stripFormatting(String s) { + return s.replaceAll("\u00A7.", "").trim(); + } + + private void updateHotbarIcons() { + PlayerData playerData = ClientCacheHandler.playerData; + for (int i = 0; i < hbSlots; i++) { + hotbarIcons[i] = null; + if (playerData == null || playerData.hotbarData == null) continue; + AbilityHotbarData slotData = playerData.hotbarData.getSlot(i); + if (slotData == null || slotData.isEmpty()) continue; + + if (AbilityController.Instance != null) { + if (slotData.isChainKey()) { + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(slotData.getResolveKey()); + if (chain != null) hotbarIcons[i] = AbilityIcon.fromChainedAbility(chain); + } else { + Ability ability = AbilityController.Instance.resolveAbility(slotData.abilityKey); + if (ability != null) hotbarIcons[i] = AbilityIcon.fromAbility(ability); + } + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // Grid Positioning Helpers + // ═══════════════════════════════════════════════════════════════════ + + private int getGridVisibleRows() { + int usableHeight = (selBottom - selTop) - gridPad * 2; + return usableHeight / gridCellSize; + } + + private int getGridStartX() { + int panelWidth = selRight - selLeft; + int usableWidth = panelWidth - gridPad * 2; + int gridWidth = gridCols * gridCellSize; + int offsetX = (usableWidth - gridWidth) / 2; + return guiLeft + selLeft + gridPad + offsetX; + } + + private int getGridStartY() { + int panelHeight = selBottom - selTop; + int usableHeight = panelHeight - gridPad * 2; + int visibleRows = getGridVisibleRows(); + int gridHeight = visibleRows * gridCellSize; + int offsetY = (usableHeight - gridHeight) / 2; + return guiTop + selTop + gridPad + offsetY; + } + + private int getMaxScrollRow() { + int totalRows = (filteredKeys.size() + gridCols - 1) / gridCols; + return Math.max(0, totalRows - getGridVisibleRows()); + } + + // ═══════════════════════════════════════════════════════════════════ + // Drawing + // ═══════════════════════════════════════════════════════════════════ + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + drawDefaultBackground(); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(resource); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, 252, 195); + drawTexturedModalRect(guiLeft + 252, guiTop, 188, 0, 67, 195); + super.drawScreen(mouseX, mouseY, partialTicks); + + updateHoverState(mouseX, mouseY); + updateHoverScales(); + + // Panel backgrounds + drawGradientRect(guiLeft + selLeft, guiTop + selTop, guiLeft + selRight, guiTop + selBottom, 0xC0101010, 0xC0101010); + drawGradientRect(guiLeft + infoLeft, guiTop + infoTop, guiLeft + infoRight, guiTop + infoBottom, 0xC0101010, 0xC0101010); + drawGradientRect(guiLeft + hbLeft, guiTop + hbTop, guiLeft + hbRight, guiTop + hbBottom, 0xC0101010, 0xC0101010); + + // RIGHT panel: Selection grid (with scissor clipping) + drawIconGrid(mouseX, mouseY); + + // LEFT panel: Ability information + drawInfoPanel(); + + // BOTTOM panel: Hotbar + drawHotbarSection(mouseX, mouseY); + + // Drag ghost + if (isDragging && draggedIcon != null) { + drawDragGhost(mouseX, mouseY); + } + + // Tooltip (last - renders on top) + drawTooltip(mouseX, mouseY); + } + + private void drawIconGrid(int mouseX, int mouseY) { + int visibleRows = getGridVisibleRows(); + int gridStartX = getGridStartX(); + int gridStartY = getGridStartY(); + int startIndex = scrollRow * gridCols; + + // GL Scissor to clip grid content within the selection panel + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + int scissorX = (guiLeft + selLeft) * scaleFactor; + int scissorY = mc.displayHeight - (guiTop + selBottom) * scaleFactor; + int scissorW = (selRight - selLeft) * scaleFactor; + int scissorH = (selBottom - selTop) * scaleFactor; + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + + int maxVisibleCount = gridCols * visibleRows; + for (int vi = 0; vi < maxVisibleCount; vi++) { + int absIndex = startIndex + vi; + if (absIndex >= filteredKeys.size()) break; + + int col = vi % gridCols; + int row = vi / gridCols; + int cellX = gridStartX + col * gridCellSize; + int cellY = gridStartY + row * gridCellSize; + + boolean isHovered = (hoveredGridIndex == absIndex) && !isDragging; + boolean isSelected = (selectedIndex == absIndex); + boolean isDragSource = isDragging && dragSourceSlot == -1 && draggedKey != null + && draggedKey.equals(filteredKeys.get(absIndex)); + + // Cell background + int bgColor; + if (isDragSource) bgColor = 0x40333333; + else if (isSelected) bgColor = 0x60505070; + else if (isHovered) bgColor = 0x60404060; + else bgColor = 0x40202030; + drawRect(cellX, cellY, cellX + gridCellSize - 1, cellY + gridCellSize - 1, bgColor); + + // Cell border + int borderColor; + if (isSelected) borderColor = 0xAA9999DD; + else if (isHovered) borderColor = 0xAA8888CC; + else borderColor = 0x40404060; + drawHorizontalLine(cellX, cellX + gridCellSize - 2, cellY, borderColor); + drawHorizontalLine(cellX, cellX + gridCellSize - 2, cellY + gridCellSize - 2, borderColor); + drawVerticalLine(cellX, cellY, cellY + gridCellSize - 2, borderColor); + drawVerticalLine(cellX + gridCellSize - 2, cellY, cellY + gridCellSize - 2, borderColor); + + GL11.glColor4f(1, 1, 1, 1); + + // Icon (state-aware for multi-state toggles) + AbilityIcon icon = filteredIcons.get(absIndex); + if (icon != null) { + float hoverScale = gridHoverScale[absIndex]; + float targetPixels = (gridCellSize - 4) * hoverScale; + float drawSize = icon.getDrawSize(); + float iconScale = targetPixels / drawSize; + + int iconCenterX = cellX + (gridCellSize - 1) / 2; + int iconCenterY = cellY + (gridCellSize - 1) / 2; + + int cellToggleState = 0; + Ability cellAbility = filteredAbilities.get(absIndex); + PlayerData cellPlayerData = ClientCacheHandler.playerData; + if (cellAbility != null && cellAbility.isToggleable() && cellPlayerData != null && cellPlayerData.abilityData != null) { + cellToggleState = cellPlayerData.abilityData.getToggleState(filteredKeys.get(absIndex)); + } + + GL11.glPushMatrix(); + GL11.glTranslatef(iconCenterX, iconCenterY, 0); + GL11.glScalef(iconScale, iconScale, 1); + + float alpha = isDragSource ? 0.3f : 1.0f; + GL11.glColor4f(1, 1, 1, alpha); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + icon.draw(cellToggleState); + GL11.glColor4f(1, 1, 1, 1); + GL11.glPopMatrix(); + } + } + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Restore GL state + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + + // Scroll indicator (drawn outside scissor) + int totalRows = (filteredKeys.size() + gridCols - 1) / gridCols; + if (totalRows > visibleRows) { + FontRenderer fr = mc.fontRenderer; + String scrollText = (scrollRow + 1) + "/" + (totalRows - visibleRows + 1); + int textX = guiLeft + selRight - fr.getStringWidth(scrollText) - 4; + int textY = guiTop + selBottom + 1; + fr.drawStringWithShadow(scrollText, textX, textY, 0x888888); + } + } + + private void drawInfoPanel() { + int infoIndex = hoveredGridIndex >= 0 ? hoveredGridIndex : selectedIndex; + if (infoIndex < 0 || infoIndex >= filteredKeys.size()) return; + + FontRenderer fr = mc.fontRenderer; + int panelInnerLeft = guiLeft + infoLeft + infoPad; + int panelInnerTop = guiTop + infoTop + infoPad; + int panelInnerWidth = (infoRight - infoLeft) - infoPad * 2; + int y = panelInnerTop; + + // Cached icon for detail preview + if (cachedDetailIndex != infoIndex) { + cachedDetailIcon = filteredIcons.get(infoIndex); + cachedDetailIndex = infoIndex; + } + + // Get toggle state for icon rendering and state display + int toggleState = 0; + PlayerData togglePlayerData = ClientCacheHandler.playerData; + Ability infoAbility = filteredAbilities.get(infoIndex); + if (infoAbility != null && infoAbility.isToggleable() && togglePlayerData != null && togglePlayerData.abilityData != null) { + toggleState = togglePlayerData.abilityData.getToggleState(filteredKeys.get(infoIndex)); + } + + // Preview icon (centered, state-aware for multi-state toggles) + if (cachedDetailIcon != null) { + float drawSize = cachedDetailIcon.getDrawSize(); + float iconScale = (float) infoIconSize / drawSize; + + int iconCX = panelInnerLeft + panelInnerWidth / 2; + int iconCY = y + infoIconSize / 2 + 4; + + GL11.glPushMatrix(); + GL11.glTranslatef(iconCX, iconCY, 0); + GL11.glScalef(iconScale, iconScale, 1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(1, 1, 1, 1); + cachedDetailIcon.draw(toggleState); + GL11.glPopMatrix(); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + + y += infoIconSize + 12; + } + + // Ability name (centered, wrapped) + String name = filteredDisplayNames.get(infoIndex); + List nameLines = fr.listFormattedStringToWidth(name, panelInnerWidth); + for (String line : nameLines) { + int lineX = panelInnerLeft + (panelInnerWidth - fr.getStringWidth(line)) / 2; + fr.drawStringWithShadow(line, lineX, y, 0xFFFFFF); + y += fr.FONT_HEIGHT + 1; + } + y += 4; + + // Type info + Ability ability = filteredAbilities.get(infoIndex); + if (ability != null) { + String typeId = ability.getTypeId(); + if (typeId != null && !typeId.isEmpty()) { + String translated = StatCollector.translateToLocal(typeId); + if (!translated.equals(typeId)) { + int textW = fr.getStringWidth(translated); + fr.drawStringWithShadow("\u00A77" + translated, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0xAAAAAA); + y += fr.FONT_HEIGHT + 3; + } + } + + if (ability.isToggleable()) { + String toggleText = "[" + StatCollector.translateToLocal("gui.toggle") + "]"; + int textW = fr.getStringWidth(toggleText); + fr.drawStringWithShadow("\u00A7a" + toggleText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0x55FF55); + y += fr.FONT_HEIGHT + 3; + } + + // Toggle state (uses pre-computed toggleState) + if (ability.isToggleable()) { + String stateText; + String prefix; + if (toggleState > 0) { + // Active — check for state label (multi-state toggles like Ki Weapon) + String stateLabel = ability.getToggleStateLabel(toggleState); + stateText = stateLabel != null ? stateLabel : StatCollector.translateToLocal("gui.toggle.active"); + prefix = "\u00A72"; + } else { + stateText = StatCollector.translateToLocal("gui.toggle.inactive"); + prefix = "\u00A78"; + } + int textW = fr.getStringWidth(stateText); + fr.drawStringWithShadow(prefix + stateText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, toggleState > 0 ? 0x55FF55 : 0x888888); + } + } + } + + private void drawHotbarSection(int mouseX, int mouseY) { + FontRenderer fr = mc.fontRenderer; + + // Get selected ability key for yellow border + PlayerData selPlayerData = ClientCacheHandler.playerData; + String selectedKey = null; + if (selPlayerData != null && selPlayerData.abilityData != null) { + selectedKey = selPlayerData.abilityData.getSelectedAbilityKey(); + } + + // Hotbar slots centered within the panel + int panelWidth = hbRight - hbLeft; + int panelHeight = hbBottom - hbTop; + int totalSlotsWidth = hbSlots * (hbSlotSize + hbSlotGap) - hbSlotGap; + int hotbarStartX = guiLeft + hbLeft + (panelWidth - totalSlotsWidth) / 2; + int hotbarStartY = guiTop + hbTop + (panelHeight - hbSlotSize) / 2; + + for (int i = 0; i < hbSlots; i++) { + int sx = hotbarStartX + i * (hbSlotSize + hbSlotGap); + int sy = hotbarStartY; + + boolean isHovered = (hoveredSlotIndex == i); + boolean isDragHover = isDragging && isHovered; + + // Check if this slot's ability is the currently selected one + boolean isSelectedSlot = false; + if (selectedKey != null && !selectedKey.isEmpty() && selPlayerData.hotbarData != null) { + AbilityHotbarData slotData = selPlayerData.hotbarData.getSlot(i); + if (slotData != null && !slotData.isEmpty() && selectedKey.equals(slotData.abilityKey)) { + isSelectedSlot = true; + } + } + + // Slot background + int bgColor = isDragHover ? 0x80606030 : (isSelectedSlot ? 0x60505020 : (isHovered ? 0x60505050 : 0x50303030)); + drawRect(sx, sy, sx + hbSlotSize, sy + hbSlotSize, bgColor); + + // Slot border - yellow for selected, drag color, hover color, or default + int borderColor; + if (isDragHover) borderColor = 0xFFCCCC44; + else if (isSelectedSlot) borderColor = 0xFFCCCC00; + else if (isHovered) borderColor = 0xAA888888; + else borderColor = 0x60505050; + drawHorizontalLine(sx, sx + hbSlotSize - 1, sy, borderColor); + drawHorizontalLine(sx, sx + hbSlotSize - 1, sy + hbSlotSize - 1, borderColor); + drawVerticalLine(sx, sy, sy + hbSlotSize - 1, borderColor); + drawVerticalLine(sx + hbSlotSize - 1, sy, sy + hbSlotSize - 1, borderColor); + + GL11.glColor4f(1, 1, 1, 1); + + // Icon or slot number + AbilityIcon icon = hotbarIcons[i]; + boolean isSourceSlot = isDragging && dragSourceSlot == i; + + if (icon != null && !isSourceSlot) { + int iconCX = sx + hbSlotSize / 2; + int iconCY = sy + hbSlotSize / 2; + float targetPixels = hbSlotSize - 4; + float drawSize = icon.getDrawSize(); + float s = targetPixels / drawSize; + + // Compute toggle state for this hotbar slot's ability + int hbToggleState = 0; + if (selPlayerData != null && selPlayerData.abilityData != null && selPlayerData.hotbarData != null) { + AbilityHotbarData slotData = selPlayerData.hotbarData.getSlot(i); + if (slotData != null && !slotData.isEmpty()) { + hbToggleState = selPlayerData.abilityData.getToggleState(slotData.abilityKey); + } + } + + GL11.glPushMatrix(); + GL11.glTranslatef(iconCX, iconCY, 0); + GL11.glScalef(s, s, 1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + icon.draw(hbToggleState); + GL11.glPopMatrix(); + GL11.glColor4f(1, 1, 1, 1); + } else { + String slotLabel = String.valueOf(i + 1); + int labelColor = isSourceSlot ? 0x333333 : 0x666666; + int lx = sx + (hbSlotSize - fr.getStringWidth(slotLabel)) / 2; + int ly = sy + (hbSlotSize - fr.FONT_HEIGHT) / 2 + 1; + fr.drawString(slotLabel, lx, ly, labelColor); + } + } + + // Restore GL + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + } + + private void drawDragGhost(int mouseX, int mouseY) { + if (draggedIcon == null) return; + + float targetPixels = gridCellSize; + float drawSize = draggedIcon.getDrawSize(); + float ghostScale = targetPixels / drawSize; + + GL11.glPushMatrix(); + GL11.glTranslatef(mouseX, mouseY, 100); + GL11.glScalef(ghostScale, ghostScale, 1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(1, 1, 1, 0.8f); + draggedIcon.draw(0); + GL11.glColor4f(1, 1, 1, 1); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1, 1, 1, 1); + } + + private void drawTooltip(int mouseX, int mouseY) { + if (isDragging) return; + + if (hoveredGridIndex >= 0 && hoveredGridIndex < filteredDisplayNames.size()) { + List lines = new ArrayList<>(); + lines.add(filteredDisplayNames.get(hoveredGridIndex)); + + Ability ability = filteredAbilities.get(hoveredGridIndex); + if (ability != null) { + String typeId = ability.getTypeId(); + if (typeId != null && !typeId.isEmpty()) { + String translated = StatCollector.translateToLocal(typeId); + if (!translated.equals(typeId)) { + lines.add("\u00A77" + translated); + } + } + if (ability.isToggleable()) { + lines.add("\u00A7a[" + StatCollector.translateToLocal("gui.toggle") + "]"); + } + } + + drawHoveringText(lines, mouseX, mouseY, mc.fontRenderer); + GL11.glDisable(GL11.GL_LIGHTING); + } + + if (hoveredSlotIndex >= 0 && hoveredSlotIndex < hbSlots) { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData != null && playerData.hotbarData != null) { + AbilityHotbarData slotData = playerData.hotbarData.getSlot(hoveredSlotIndex); + if (slotData != null && !slotData.isEmpty()) { + String name = getHotbarSlotDisplayName(slotData); + if (name != null) { + List lines = new ArrayList<>(); + lines.add(name); + lines.add("\u00A78Right-click to remove"); + drawHoveringText(lines, mouseX, mouseY, mc.fontRenderer); + GL11.glDisable(GL11.GL_LIGHTING); + } + } + } + } + } + + private String getHotbarSlotDisplayName(AbilityHotbarData slotData) { + if (AbilityController.Instance == null) return null; + if (slotData.isChainKey()) { + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(slotData.getResolveKey()); + return chain != null ? chain.getDisplayName() : null; + } + Ability ability = AbilityController.Instance.resolveAbility(slotData.abilityKey); + return ability != null ? ability.getDisplayName() : null; + } + + // ═══════════════════════════════════════════════════════════════════ + // Hover & Animation + // ═══════════════════════════════════════════════════════════════════ + + private void updateHoverState(int mouseX, int mouseY) { + hoveredGridIndex = -1; + hoveredSlotIndex = -1; + + // Grid hit test (RIGHT panel - selection) + int visibleRows = getGridVisibleRows(); + int gridStartX = getGridStartX(); + int gridStartY = getGridStartY(); + int startIndex = scrollRow * gridCols; + + if (mouseX >= guiLeft + selLeft && mouseX < guiLeft + selRight + && mouseY >= guiTop + selTop && mouseY < guiTop + selBottom) { + for (int vi = 0; vi < gridCols * visibleRows; vi++) { + int absIndex = startIndex + vi; + if (absIndex >= filteredKeys.size()) break; + + int col = vi % gridCols; + int row = vi / gridCols; + int cellX = gridStartX + col * gridCellSize; + int cellY = gridStartY + row * gridCellSize; + + if (mouseX >= cellX && mouseX < cellX + gridCellSize - 1 + && mouseY >= cellY && mouseY < cellY + gridCellSize - 1) { + hoveredGridIndex = absIndex; + break; + } + } + } + + // Hotbar hit test + int panelWidth = hbRight - hbLeft; + int panelHeight = hbBottom - hbTop; + int totalSlotsWidth = hbSlots * (hbSlotSize + hbSlotGap) - hbSlotGap; + int hotbarStartX = guiLeft + hbLeft + (panelWidth - totalSlotsWidth) / 2; + int hotbarStartY = guiTop + hbTop + (panelHeight - hbSlotSize) / 2; + + for (int i = 0; i < hbSlots; i++) { + int sx = hotbarStartX + i * (hbSlotSize + hbSlotGap); + if (mouseX >= sx && mouseX < sx + hbSlotSize + && mouseY >= hotbarStartY && mouseY < hotbarStartY + hbSlotSize) { + hoveredSlotIndex = i; + break; + } + } + } + + private void updateHoverScales() { + if (gridHoverScale == null) return; + for (int i = 0; i < gridHoverScale.length; i++) { + float target = (i == hoveredGridIndex && !isDragging) ? 1.12f : 1.0f; + gridHoverScale[i] += (target - gridHoverScale[i]) * 0.3f; + if (Math.abs(gridHoverScale[i] - target) < 0.005f) { + gridHoverScale[i] = target; + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // Mouse Handlers + // ═══════════════════════════════════════════════════════════════════ + + @Override + public void mouseClicked(int mouseX, int mouseY, int button) { + if (hasSubGui()) { + super.mouseClicked(mouseX, mouseY, button); + return; + } + + // Right-click on hotbar slot -> clear + if (button == 1 && hoveredSlotIndex >= 0) { + saveHotbarSlot(hoveredSlotIndex, ""); + updateHotbarIcons(); + return; + } + + // Left-click: start potential drag or select + if (button == 0) { + if (hoveredGridIndex >= 0 && hoveredGridIndex < filteredKeys.size()) { + // Toggle double-tap detection on toggle tab + if (subTab == 1) { + long now = System.currentTimeMillis(); + if (lastToggleClickIndex == hoveredGridIndex && (now - lastToggleClickTime) < 400) { + String key = filteredKeys.get(hoveredGridIndex); + Ability ability = filteredAbilities.get(hoveredGridIndex); + if (ability != null && ability.isToggleable()) { + PacketHandler.Instance.sendToServer(new AbilityTogglePacket(key)); + lastToggleClickIndex = -1; + lastToggleClickTime = 0; + // Rebuild list after a short delay to reflect new state + // For now just select and return - the list rebuilds on next initGui + selectedIndex = hoveredGridIndex; + cachedDetailIcon = null; + cachedDetailIndex = -1; + return; + } + } + lastToggleClickIndex = hoveredGridIndex; + lastToggleClickTime = now; + } + + selectedIndex = hoveredGridIndex; + cachedDetailIcon = null; + cachedDetailIndex = -1; + dragPending = true; + draggedKey = filteredKeys.get(hoveredGridIndex); + draggedIcon = filteredIcons.get(hoveredGridIndex); + dragSourceSlot = -1; + dragStartX = mouseX; + dragStartY = mouseY; + return; + } + + if (hoveredSlotIndex >= 0 && hotbarIcons[hoveredSlotIndex] != null) { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData != null && playerData.hotbarData != null) { + AbilityHotbarData slotData = playerData.hotbarData.getSlot(hoveredSlotIndex); + if (slotData != null && !slotData.isEmpty()) { + dragPending = true; + draggedKey = slotData.abilityKey; + draggedIcon = hotbarIcons[hoveredSlotIndex]; + dragSourceSlot = hoveredSlotIndex; + dragStartX = mouseX; + dragStartY = mouseY; + return; + } + } + } + } + + super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public void mouseClickMove(int mouseX, int mouseY, int button, long timeSinceLastClick) { + if (dragPending && !isDragging) { + int dx = mouseX - dragStartX; + int dy = mouseY - dragStartY; + if (dx * dx + dy * dy > dragThreshold * dragThreshold) { + isDragging = true; + } + } + if (isDragging) { + dragMouseX = mouseX; + dragMouseY = mouseY; + } + + super.mouseClickMove(mouseX, mouseY, button, timeSinceLastClick); + } + + @Override + protected void mouseMovedOrUp(int mouseX, int mouseY, int state) { + if (isDragging && draggedKey != null) { + updateHoverState(mouseX, mouseY); + + if (hoveredSlotIndex >= 0) { + if (dragSourceSlot >= 0) { + swapHotbarSlots(dragSourceSlot, hoveredSlotIndex); + } else { + saveHotbarSlot(hoveredSlotIndex, draggedKey); + } + updateHotbarIcons(); + } else if (dragSourceSlot >= 0 && hoveredGridIndex >= 0) { + saveHotbarSlot(dragSourceSlot, ""); + updateHotbarIcons(); + } + } + + isDragging = false; + dragPending = false; + draggedKey = null; + draggedIcon = null; + dragSourceSlot = -1; + + super.mouseMovedOrUp(mouseX, mouseY, state); + } + + @Override + public void handleMouseInput() { + super.handleMouseInput(); + + int wheel = Mouse.getEventDWheel(); + if (wheel != 0) { + int maxScroll = getMaxScrollRow(); + if (wheel > 0) { + scrollRow = Math.max(0, scrollRow - 1); + } else { + scrollRow = Math.min(maxScroll, scrollRow + 1); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // Actions & Network + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void actionPerformed(GuiButton btn) { + if (btn instanceof AbstractTab) return; + + if (btn.id <= -100 && btn.id > -200) { + super.actionPerformed(btn); + return; + } + + if (btn.id == BTN_TAB_ABILITIES) { + subTab = 0; + initGui(); + return; + } + if (btn.id == BTN_TAB_TOGGLES) { + subTab = 1; + initGui(); + return; + } + } + + private void saveHotbarSlot(int slotIndex, String abilityKey) { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData != null && playerData.hotbarData != null) { + AbilityHotbarData slot = playerData.hotbarData.getSlot(slotIndex); + if (slot != null) { + slot.abilityKey = abilityKey; + } + } + + NBTTagCompound compound = new NBTTagCompound(); + NBTTagCompound slotTag = new NBTTagCompound(); + slotTag.setInteger("slot", slotIndex); + slotTag.setString("abilityKey", abilityKey != null ? abilityKey : ""); + compound.setTag("AbilityHotbar" + slotIndex, slotTag); + PacketHandler.Instance.sendToServer(new AbilityHotbarSavePacket(slotIndex, compound)); + } + + private void swapHotbarSlots(int fromSlot, int toSlot) { + if (fromSlot == toSlot) return; + + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData == null || playerData.hotbarData == null) return; + + AbilityHotbarData fromData = playerData.hotbarData.getSlot(fromSlot); + AbilityHotbarData toData = playerData.hotbarData.getSlot(toSlot); + if (fromData == null || toData == null) return; + + String fromKey = fromData.abilityKey; + String toKey = toData.abilityKey; + + saveHotbarSlot(fromSlot, toKey != null ? toKey : ""); + saveHotbarSlot(toSlot, fromKey != null ? fromKey : ""); + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + // Send selection sync if changed + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData != null && playerData.abilityData != null) { + String currentKey = playerData.abilityData.getSelectedAbilityKey(); + boolean changed = (initialSelectedKey == null && currentKey != null && !currentKey.isEmpty()) + || (initialSelectedKey != null && !initialSelectedKey.equals(currentKey)); + if (changed) { + PacketHandler.Instance.sendToServer(new AbilityHotbarSelectPacket(currentKey != null ? currentKey : "")); + } + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (i == 1 || isInventoryKey(i)) { + close(); + } + } + + @Override + public void save() { + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java index f27913d89..3e4b9706a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiCNPCInventory.java @@ -73,6 +73,14 @@ public class GuiCNPCInventory extends GuiNPCInterface { addSideButton(profileButton); } + y += 21; + GuiMenuSideButton abilitiesButton = new GuiMenuSideButton(-105, guiLeft + xSize + 37, this.guiTop + y, 22, 22, ""); + abilitiesButton.rightSided = true; + abilitiesButton.active = activeTab == -105; + abilitiesButton.renderIconPosX = 80; + abilitiesButton.renderResource = specialIcons; + addSideButton(abilitiesButton); + y += 21; GuiMenuSideButton clientButton = new GuiMenuSideButton(-103, guiLeft + xSize + 37, this.guiTop + y, 22, 22, ""); clientButton.rightSided = true; @@ -154,6 +162,10 @@ public class GuiCNPCInventory extends GuiNPCInterface { activeTab = -104; mc.displayGuiScreen(new GuiProfiles()); } + if (guibutton.id == -105 && activeTab != -105) { + activeTab = -105; + mc.displayGuiScreen(new GuiAbilities()); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/SubGuiSelectAbilityForSlot.java b/src/main/java/noppes/npcs/client/gui/player/inventory/SubGuiSelectAbilityForSlot.java new file mode 100644 index 000000000..a3d85f7e4 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/SubGuiSelectAbilityForSlot.java @@ -0,0 +1,155 @@ +package noppes.npcs.client.gui.player.inventory; + +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.ClientCacheHandler; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.data.AbilityHotbarData; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * SubGui for selecting an ability to assign to a hotbar slot. + */ +public class SubGuiSelectAbilityForSlot extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener { + private HashMap displayToKey = new HashMap<>(); + private GuiCustomScroll scrollAbilities; + private String selected = null; + private String search = ""; + + public boolean confirmed = false; + public String selectedAbilityKey = null; + public int slotIndex; + public boolean removeAbility = false; + + public SubGuiSelectAbilityForSlot(int slotIndex) { + this.slotIndex = slotIndex; + this.closeOnEsc = true; + this.drawDefaultBackground = true; + guiLeft -= 10; + xSize = 256 + 10; + this.setBackground("menubg.png"); + } + + @Override + public void initGui() { + super.initGui(); + guiTop += 10; + + if (scrollAbilities == null) { + scrollAbilities = new GuiCustomScroll(this, 0, 0); + scrollAbilities.setSize(177, 185); + } + + scrollAbilities.guiLeft = guiLeft + 4; + scrollAbilities.guiTop = guiTop + 4; + addScroll(scrollAbilities); + + loadAbilities(); + scrollAbilities.setList(getSearchList()); + + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 192, 177, 20, search)); + + addButton(new GuiNpcButton(0, guiLeft + 183, guiTop + 4, 79, 20, "gui.add")); + addButton(new GuiNpcButton(1, guiLeft + 183, guiTop + 26, 79, 20, "gui.cancel")); + addButton(new GuiNpcButton(2, guiLeft + 183, guiTop + 88, 79, 20, "gui.remove")); + } + + private void loadAbilities() { + displayToKey.clear(); + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData == null || playerData.abilityData == null) { + return; + } + + List abilities = playerData.abilityData.getUnlockedAbilityList(); + for (String key : abilities) { + String displayName; + if (key.startsWith(AbilityHotbarData.CHAIN_PREFIX)) { + String chainKey = key.substring(AbilityHotbarData.CHAIN_PREFIX.length()); + ChainedAbility chain = AbilityController.Instance != null ? + AbilityController.Instance.resolveChainedAbility(chainKey) : null; + displayName = chain != null ? "\u00A76\u2726 " + chain.getDisplayName() : key; + } else { + Ability ability = AbilityController.Instance != null ? + AbilityController.Instance.resolveAbility(key) : null; + displayName = ability != null ? ability.getDisplayName() : key; + } + displayToKey.put(displayName, key); + } + } + + @Override + public void actionPerformed(GuiButton button) { + int id = button.id; + + if (id == 0 && selected != null) { + confirmed = true; + selectedAbilityKey = displayToKey.get(selected); + this.close(); + } + if (id == 1) { + this.close(); + } + if (id == 2) { + this.removeAbility = true; + this.close(); + } + } + + @Override + public void customScrollClicked(int i, int i1, int i2, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll == scrollAbilities) + selected = scrollAbilities.getSelected(); + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll == scrollAbilities && selection != null) { + confirmed = true; + selectedAbilityKey = displayToKey.get(selection); + this.close(); + } + } + + @Override + public void unFocused(GuiNpcTextField guiNpcTextField) { + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(55) != null) { + if (getTextField(55).isFocused()) { + if (search.equals(getTextField(55).getText())) + return; + search = getTextField(55).getText().toLowerCase(); + scrollAbilities.resetScroll(); + scrollAbilities.setList(getSearchList()); + } + } + } + + private List getSearchList() { + if (search.isEmpty()) { + return new ArrayList<>(displayToKey.keySet()); + } + + List list = new ArrayList<>(); + for (String name : displayToKey.keySet()) { + if (name.toLowerCase().contains(search)) + list.add(name); + } + return list; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiMenuTopButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiMenuTopButton.java index 413ca7b39..8c8eca60d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiMenuTopButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiMenuTopButton.java @@ -24,10 +24,21 @@ public class GuiMenuTopButton extends GuiNpcButton { height = 20; } + /** + * Place to the RIGHT of the given button. + */ public GuiMenuTopButton(int i, GuiButton parent, String s) { this(i, parent.xPosition + parent.width, parent.yPosition, s); } + /** + * Place to the LEFT of the given button. + */ + public GuiMenuTopButton(int i, String s, GuiButton rightNeighbor) { + this(i, rightNeighbor.xPosition - (Minecraft.getMinecraft().fontRenderer.getStringWidth(StatCollector.translateToLocal(s)) + 12), + rightNeighbor.yPosition, s); + } + public GuiMenuTopButton(int i, GuiButton parent, String s, IButtonListener listener) { this(i, parent, s); diff --git a/src/main/java/noppes/npcs/config/ConfigClient.java b/src/main/java/noppes/npcs/config/ConfigClient.java index d8c093382..5c1dc6c1a 100644 --- a/src/main/java/noppes/npcs/config/ConfigClient.java +++ b/src/main/java/noppes/npcs/config/ConfigClient.java @@ -69,10 +69,10 @@ public class ConfigClient { // HUD PROPERTIES public static Property QuestOverlayXProperty; - public static int QuestOverlayX = 0; + public static float QuestOverlayX = 0; public static Property QuestOverlayYProperty; - public static int QuestOverlayY = 31; + public static float QuestOverlayY = 31; public static Property QuestOverlayScaleProperty; public static int QuestOverlayScale = 223; @@ -84,10 +84,10 @@ public class ConfigClient { public static boolean CompassEnabled = true; public static Property CompassOverlayXProperty; - public static int CompassOverlayX = 37; + public static float CompassOverlayX = 37; public static Property CompassOverlayYProperty; - public static int CompassOverlayY = 0; + public static float CompassOverlayY = 0; public static Property CompassOverlayScaleProperty; public static int CompassOverlayScale = 276; @@ -95,6 +95,37 @@ public class ConfigClient { public static Property CompassOverlayWidthProperty; public static int CompassOverlayWidth = 200; + // Ability Hotbar HUD + public static Property AbilityHotbarEnabledProperty; + public static boolean AbilityHotbarEnabled = true; + + public static Property AbilityHotbarXProperty; + public static float AbilityHotbarX = 3; + + public static Property AbilityHotbarYProperty; + public static float AbilityHotbarY = 50; + + public static Property AbilityHotbarScaleProperty; + public static int AbilityHotbarScale = 100; + + public static Property AbilityHotbarHorizontalProperty; + public static boolean AbilityHotbarHorizontal = false; + + public static Property AbilityHotbarAltTextureProperty; + public static boolean AbilityHotbarAltTexture = false; + + // 0=Hide, 1=Above(H)/Left(V), 2=Below(H)/Right(V) + public static Property AbilityHotbarTextPositionProperty; + public static int AbilityHotbarTextPosition = 2; + + // Max visible slots: 3, 5, or 7 + public static Property AbilityHotbarVisibleSlotsProperty; + public static int AbilityHotbarVisibleSlots = 5; + + // Show Always: true = always visible, false = only visible while HUD key held + public static Property AbilityHotbarShowAlwaysProperty; + public static boolean AbilityHotbarShowAlways = true; + /** * Texture Properties **/ @@ -125,11 +156,11 @@ public class ConfigClient { config.load(); // Hud Quest Overlay settings - QuestOverlayXProperty = config.get(HUD, "Quest Hud X", 0, "X position of the quest overlay."); - QuestOverlayX = QuestOverlayXProperty.getInt(0); + QuestOverlayXProperty = config.get(HUD, "Quest Hud X", 0.0, "X position of the quest overlay."); + QuestOverlayX = (float) QuestOverlayXProperty.getDouble(0); - QuestOverlayYProperty = config.get(HUD, "Quest Hud Y", 31, "Y position of the quest overlay."); - QuestOverlayY = QuestOverlayYProperty.getInt(31); + QuestOverlayYProperty = config.get(HUD, "Quest Hud Y", 31.0, "Y position of the quest overlay."); + QuestOverlayY = (float) QuestOverlayYProperty.getDouble(31); QuestOverlayScaleProperty = config.get(HUD, "Quest Hud Scale", 223, "Scale percentage of the quest overlay."); QuestOverlayScale = QuestOverlayScaleProperty.getInt(223); @@ -149,22 +180,18 @@ public class ConfigClient { CompassOverlayXProperty = config.get( HUD, "Compass Hud X", - 37, - "Horizontal position of compass overlay (0-100 percentage)", - 0, - 100 + 37.0, + "Horizontal position of compass overlay (0-100 percentage)" ); - CompassOverlayX = CompassOverlayXProperty.getInt(); + CompassOverlayX = (float) CompassOverlayXProperty.getDouble(); CompassOverlayYProperty = config.get( HUD, "Compass Hud Y", - 0, - "Vertical position of compass overlay (0-100 percentage)", - 0, - 100 + 0.0, + "Vertical position of compass overlay (0-100 percentage)" ); - CompassOverlayY = CompassOverlayYProperty.getInt(); + CompassOverlayY = (float) CompassOverlayYProperty.getDouble(); CompassOverlayScaleProperty = config.get( HUD, @@ -186,6 +213,33 @@ public class ConfigClient { ); CompassOverlayWidth = CompassOverlayWidthProperty.getInt(); + // Ability Hotbar HUD Configs + AbilityHotbarEnabledProperty = config.get(HUD, "Ability Hotbar Enabled", true, "Enable Ability Hotbar HUD Component"); + AbilityHotbarEnabled = AbilityHotbarEnabledProperty.getBoolean(); + + AbilityHotbarXProperty = config.get(HUD, "Ability Hotbar X", 3.0, "Horizontal position (0-100 percentage)"); + AbilityHotbarX = (float) AbilityHotbarXProperty.getDouble(); + + AbilityHotbarYProperty = config.get(HUD, "Ability Hotbar Y", 50.0, "Vertical position (0-100 percentage)"); + AbilityHotbarY = (float) AbilityHotbarYProperty.getDouble(); + + AbilityHotbarScaleProperty = config.get(HUD, "Ability Hotbar Scale", 100, "Scale percentage", 50, 300); + AbilityHotbarScale = AbilityHotbarScaleProperty.getInt(); + + AbilityHotbarHorizontalProperty = config.get(HUD, "Ability Hotbar Horizontal", false, "Display horizontally instead of vertically"); + AbilityHotbarHorizontal = AbilityHotbarHorizontalProperty.getBoolean(); + + AbilityHotbarAltTextureProperty = config.get(HUD, "Ability Hotbar Alt Texture", false, "Use rounded square instead of circle slots"); + AbilityHotbarAltTexture = AbilityHotbarAltTextureProperty.getBoolean(); + + AbilityHotbarTextPositionProperty = config.get(HUD, "Ability Hotbar Text Position", 2, "Text label position (0=Hide, 1=Above/Left, 2=Below/Right)"); + AbilityHotbarTextPosition = AbilityHotbarTextPositionProperty.getInt(2); + + AbilityHotbarVisibleSlotsProperty = config.get(HUD, "Ability Hotbar Visible Slots", 5, "Max visible slots in hotbar (3, 5, or 7)"); + AbilityHotbarVisibleSlots = AbilityHotbarVisibleSlotsProperty.getInt(5); + + AbilityHotbarShowAlwaysProperty = config.get(HUD, "Ability Hotbar Show Always", true, "Always show hotbar (false = only while HUD key is held)"); + AbilityHotbarShowAlways = AbilityHotbarShowAlwaysProperty.getBoolean(true); // General AllowClientScriptsProperty = config.get(GENERAL, "Allow Client Scripts", true, "Allow the server to run scripts on the client. If disabled, no server scripts will execute client-side."); diff --git a/src/main/java/noppes/npcs/controllers/data/AbilityHotbarData.java b/src/main/java/noppes/npcs/controllers/data/AbilityHotbarData.java new file mode 100644 index 000000000..7941c81f1 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AbilityHotbarData.java @@ -0,0 +1,47 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; + +public class AbilityHotbarData { + public static final String CHAIN_PREFIX = "chain:"; + public static final int TOTAL_SLOTS = 12; + + public int slot = -1; + public String abilityKey = ""; + + public AbilityHotbarData() { + } + + public AbilityHotbarData(int slot) { + this.slot = slot; + } + + public boolean isChainKey() { + return abilityKey != null && abilityKey.startsWith(CHAIN_PREFIX); + } + + public String getResolveKey() { + if (isChainKey()) return abilityKey.substring(CHAIN_PREFIX.length()); + return abilityKey; + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + NBTTagCompound tag = new NBTTagCompound(); + tag.setInteger("slot", slot); + tag.setString("abilityKey", abilityKey != null ? abilityKey : ""); + compound.setTag("AbilityHotbar" + slot, tag); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + abilityKey = compound.getString("abilityKey"); + } + + public void reset() { + abilityKey = ""; + } + + public boolean isEmpty() { + return abilityKey == null || abilityKey.isEmpty(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index d2afe1c64..4df2485e6 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -364,6 +364,12 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA // Regular ability activation Ability ability = (Ability) action; + // Toggle abilities cycle state instead of executing + if (ability.isToggleable()) { + toggleAbility(key); + return true; + } + // Check universal cooldown (abilities can optionally ignore it) if (!ability.isIgnoreCooldown() && isOnCooldown()) return false; @@ -460,7 +466,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } public void setSelectedIndex(int index) { - if (index >= 0 && index < unlockedAbilities.size()) { + if (index == -1 || (index >= 0 && index < unlockedAbilities.size())) { this.selectedIndex = index; syncToClient(); } @@ -935,8 +941,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } /** - * Remove any unlocked ability keys that can no longer be resolved. - * Called during load to clean up references to deleted abilities. + * Remove any unlocked ability keys that can no longer be resolved or + * are no longer allowed for players (e.g. NPC-only abilities). + * Called during load to clean up references to deleted/restricted abilities. * Supports both regular ability keys and "chain:" prefixed chained ability keys. */ private void validateUnlockedAbilities() { @@ -949,6 +956,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (resolved == null) { it.remove(); LogWriter.info("Removed invalid ability reference from player data: " + key); + } else if (!resolved.getAllowedBy().allowsPlayer()) { + it.remove(); + LogWriter.info("Removed non-player ability from player data: " + key); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java new file mode 100644 index 000000000..f062d891c --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityHotbarData.java @@ -0,0 +1,84 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; + +public class PlayerAbilityHotbarData { + public AbilityHotbarData[] slots = new AbilityHotbarData[AbilityHotbarData.TOTAL_SLOTS]; + + public PlayerAbilityHotbarData() { + for (int i = 0; i < slots.length; i++) { + slots[i] = new AbilityHotbarData(i); + } + } + + public AbilityHotbarData getSlot(int index) { + if (index < 0 || index >= slots.length) return null; + return slots[index]; + } + + public void setSlot(int index, String abilityKey) { + if (index < 0 || index >= slots.length) return; + slots[index].abilityKey = abilityKey != null ? abilityKey : ""; + } + + public void clearSlot(int index) { + if (index < 0 || index >= slots.length) return; + slots[index].reset(); + } + + public boolean hasAnyAbilities() { + for (AbilityHotbarData slot : slots) { + if (!slot.isEmpty()) return true; + } + return false; + } + + public void writeToNBT(NBTTagCompound compound) { + for (int i = 0; i < slots.length; i++) { + slots[i].writeToNBT(compound); + } + } + + public void readFromNBT(NBTTagCompound compound) { + for (int i = 0; i < slots.length; i++) { + NBTTagCompound slotTag = compound.getCompoundTag("AbilityHotbar" + i); + slots[i].readFromNBT(slotTag); + } + validateSlots(); + } + + public void validateSlots() { + if (AbilityController.Instance == null) return; + for (AbilityHotbarData slot : slots) { + if (slot.isEmpty()) continue; + boolean valid; + if (slot.isChainKey()) { + valid = AbilityController.Instance.canResolveChainedAbility(slot.getResolveKey()); + } else { + valid = AbilityController.Instance.canResolveAbility(slot.abilityKey); + // Also check that resolved ability allows player usage + if (valid) { + Ability ability = AbilityController.Instance.resolveAbility(slot.abilityKey); + if (ability != null && !ability.getAllowedBy().allowsPlayer()) { + valid = false; + } + } + } + if (!valid) { + slot.reset(); + } + } + } + + public void syncToClient(EntityPlayer player) { + if (player instanceof EntityPlayerMP) { + AbilityHotbarSyncPacket.sendToPlayer((EntityPlayerMP) player); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 9052811cf..8283d4f6c 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -65,6 +65,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public PlayerTradeData tradeData = new PlayerTradeData(this); public PlayerAbilityData abilityData = new PlayerAbilityData(this); + public PlayerAbilityHotbarData hotbarData = new PlayerAbilityHotbarData(); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -119,6 +120,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { // Sync player ability data to client abilityData.syncToClient(); + + // Sync hotbar data to client + hotbarData.syncToClient(player); } public void onLogout() { @@ -151,6 +155,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { magicData.readToNBT(data); tradeData.readFromNBT(data); abilityData.readFromNBT(data); + hotbarData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -195,6 +200,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { magicData.writeToNBT(compound); tradeData.writeToNBT(compound); abilityData.writeToNBT(compound); + hotbarData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); diff --git a/src/main/java/tconstruct/client/tabs/InventoryTabCustomNpc.java b/src/main/java/tconstruct/client/tabs/InventoryTabCustomNpc.java index e52742812..8c3fcde4c 100644 --- a/src/main/java/tconstruct/client/tabs/InventoryTabCustomNpc.java +++ b/src/main/java/tconstruct/client/tabs/InventoryTabCustomNpc.java @@ -4,6 +4,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import noppes.npcs.CustomItems; +import noppes.npcs.client.gui.player.inventory.GuiAbilities; import noppes.npcs.client.gui.player.inventory.GuiCNPCInventory; import noppes.npcs.client.gui.player.inventory.GuiFaction; import noppes.npcs.client.gui.player.inventory.GuiParty; @@ -58,5 +59,8 @@ public class InventoryTabCustomNpc extends AbstractTab { if (tab == -104) { mc.displayGuiScreen(new GuiProfiles()); } + if (tab == -105) { + mc.displayGuiScreen(new GuiAbilities()); + } } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index b963e5d34..810100e47 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2,6 +2,7 @@ customnpcs.defaultTextColor=404040 key.categories.customnpc=CustomNPC+ key.customnpcs.special=Special Key +key.customnpcs.abilityHudKey=Ability HUD Key key.npcInventory=NPC Inventory entity.CustomNpc.name=Custom Npc @@ -167,6 +168,12 @@ gui.apply=Apply gui.cancel=Cancel gui.continue=Continue gui.yes=Yes +gui.abilities=Abilities +gui.toggles=Toggles +gui.toggle=Toggle +gui.toggle.active=Active +gui.toggle.inactive=Inactive +gui.abilityHotbar=Ability Hotbar gui.lineColor1=Upper line color gui.lineColor2=Middle line color @@ -2055,6 +2062,10 @@ ability.userType.PLAYER_ONLY=Players ability.userType.BOTH=Both ability.dieOnImpact=Die on Impact +ability.icon.section.uv=UV Coordinates +ability.icon.x=Icon X +ability.icon.y=Icon Y + gui.both=Both gui.players=Players diff --git a/src/main/resources/assets/customnpcs/textures/gui/ability_fallback.png b/src/main/resources/assets/customnpcs/textures/gui/ability_fallback.png new file mode 100644 index 000000000..ad84e3327 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/ability_fallback.png differ