From 44118ec770122c57ffd8cd5de5f0f8c78c7ca089 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 20 Feb 2026 03:47:12 -0500 Subject: [PATCH] Complete Ability Hud System + Added-Per-Ability Cooldown System to be Reflected in Hud and Hud GUI --- src/api | 2 +- .../controllers/data/ability/Ability.java | 20 ++- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumDataPacket.java | 1 + .../ability/AbilityCooldownSyncPacket.java | 109 ++++++++++++++++ .../noppes/npcs/AbstractDataAbilities.java | 118 +++++++++++++++++- src/main/java/noppes/npcs/DataAbilities.java | 20 ++- .../hud/ability/AbilityHotbarComponent.java | 72 +++++++++-- .../ability/AbilityHotbarSlotRenderer.java | 72 ++++++++--- .../gui/player/inventory/GuiAbilities.java | 20 +++ .../java/noppes/npcs/config/ConfigClient.java | 11 +- .../controllers/data/PlayerAbilityData.java | 64 ++++++++-- .../assets/customnpcs/lang/en_US.lang | 3 +- 13 files changed, 467 insertions(+), 47 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java diff --git a/src/api b/src/api index a27e11595..72718c360 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit a27e1159508c799c1b7b612ee09574f207bd64e0 +Subproject commit 72718c36016c526b00681ea2ba8c506030aca555 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 10673068c..3563fe892 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -109,6 +109,10 @@ public abstract class Ability implements IAbility, IAbilityAction { // Cooldown override protected boolean ignoreCooldown = false; + // Per-ability cooldown: if true, this ability has its own independent cooldown + // instead of sharing the global cooldown with other abilities + protected boolean perAbilityCooldown = false; + // ═══════════════════════════════════════════════════════════════════ // BUILT-IN (set in constructor via configureAsBuiltIn, NOT persisted) // ═══════════════════════════════════════════════════════════════════ @@ -603,8 +607,10 @@ public abstract class Ability implements IAbility, IAbilityAction { FieldDef.boolField("ability.syncWindup", this::isSyncWindupWithAnimation, this::setSyncWindupWithAnimation) .hover("ability.hover.sync") ).tab("General").visibleWhen(() -> hasWindUpAnimation() && isSyncWindupWithAnimation())); - defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) - .tab("General").range(0, 10000)); + defs.add(FieldDef.row( + FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks).range(0, 10000), + FieldDef.boolField("ability.perAbilityCooldown", this::isPerAbilityCooldown, this::setPerAbilityCooldown) + ).tab("General")); defs.add(FieldDef.section("ability.section.movement").tab("General")); defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(), () -> this.getLockMovement().getDisplayKey(), @@ -1282,6 +1288,7 @@ public abstract class Ability implements IAbility, IAbilityAction { nbt.setTag("customData", (NBTTagCompound) customData.copy()); nbt.setInteger("allowedBy", allowedBy.ordinal()); nbt.setBoolean("ignoreCooldown", ignoreCooldown); + nbt.setBoolean("perAbilityCooldown", perAbilityCooldown); // Toggle nbt.setInteger("toggleStates", toggleStates); @@ -1363,6 +1370,7 @@ public abstract class Ability implements IAbility, IAbilityAction { customData = nbt.getCompoundTag("customData"); allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy")); ignoreCooldown = nbt.getBoolean("ignoreCooldown"); + perAbilityCooldown = nbt.getBoolean("perAbilityCooldown"); // Toggle toggleStates = nbt.getInteger("toggleStates"); @@ -1951,6 +1959,14 @@ public abstract class Ability implements IAbility, IAbilityAction { this.ignoreCooldown = ignoreCooldown; } + public boolean isPerAbilityCooldown() { + return perAbilityCooldown; + } + + public void setPerAbilityCooldown(boolean perAbilityCooldown) { + this.perAbilityCooldown = perAbilityCooldown; + } + public boolean isToggleable() { return toggleStates > 0; } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index ca172edc3..766210559 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.AbilityCooldownSyncPacket; import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; @@ -603,6 +604,7 @@ public class PacketHandler { DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); DATA_PACKET.registerPacket(new PlayerAbilityStatePacket()); DATA_PACKET.registerPacket(new AbilityHotbarSyncPacket()); + DATA_PACKET.registerPacket(new AbilityCooldownSyncPacket()); } public void registerPlayerPackets() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index e72cd2810..bc6d7d1f9 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -65,6 +65,7 @@ public enum EnumDataPacket { PLAYER_ABILITY_SYNC, PLAYER_ABILITY_STATE, ABILITY_HOTBAR_SYNC, + ABILITY_COOLDOWN_SYNC, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java new file mode 100644 index 000000000..484cc602c --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java @@ -0,0 +1,109 @@ +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.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 noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Lightweight packet that syncs cooldown state (global + per-ability) to the client. + * Sent when cooldowns are rolled, reset, or on login. + */ +public final class AbilityCooldownSyncPacket extends AbstractPacket { + public static final String packetName = "Data|AbilityCooldownSync"; + + private long globalCooldownEndTime; + private int globalCooldownDuration; + private HashMap perAbilityCooldownEndTimes; + private HashMap perAbilityCooldownDurations; + + public AbilityCooldownSyncPacket() { + } + + public AbilityCooldownSyncPacket(long globalEndTime, int globalDuration, + HashMap perEndTimes, + HashMap perDurations) { + this.globalCooldownEndTime = globalEndTime; + this.globalCooldownDuration = globalDuration; + this.perAbilityCooldownEndTimes = perEndTimes; + this.perAbilityCooldownDurations = perDurations; + } + + @Override + public Enum getType() { + return EnumDataPacket.ABILITY_COOLDOWN_SYNC; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeLong(globalCooldownEndTime); + out.writeInt(globalCooldownDuration); + + // Write per-ability cooldowns + int count = perAbilityCooldownEndTimes != null ? perAbilityCooldownEndTimes.size() : 0; + out.writeInt(count); + if (count > 0) { + for (Map.Entry entry : perAbilityCooldownEndTimes.entrySet()) { + ByteBufUtils.writeUTF8String(out, entry.getKey()); + out.writeLong(entry.getValue()); + Integer dur = perAbilityCooldownDurations.get(entry.getKey()); + out.writeInt(dur != null ? dur : 0); + } + } + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + long globalEndTime = in.readLong(); + int globalDuration = in.readInt(); + + int count = in.readInt(); + HashMap perEndTimes = new HashMap<>(); + HashMap perDurations = new HashMap<>(); + for (int i = 0; i < count; i++) { + String key = ByteBufUtils.readUTF8String(in); + long endTime = in.readLong(); + int duration = in.readInt(); + perEndTimes.put(key, endTime); + perDurations.put(key, duration); + } + + PlayerData data = PlayerData.get(player); + if (data != null && data.abilityData != null) { + data.abilityData.applyCooldownSync(globalEndTime, globalDuration, perEndTimes, perDurations); + } + } + + /** + * Send the player's current cooldown state to their client. + */ + public static void sendToPlayer(EntityPlayerMP player) { + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null || data.abilityData == null) return; + + PacketHandler.Instance.sendToPlayer(new AbilityCooldownSyncPacket( + data.abilityData.getCooldownEndTime(), + data.abilityData.getGlobalCooldownDurationValue(), + data.abilityData.getPerAbilityCooldownEndTimes(), + data.abilityData.getPerAbilityCooldownDurations() + ), player); + } +} diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 240714738..b78ca37e7 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -14,6 +14,7 @@ import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -44,10 +45,25 @@ public abstract class AbstractDataAbilities { protected Ability currentAbility; /** - * World time when cooldown ends + * World time when global cooldown ends */ protected long cooldownEndTime = 0; + /** + * Duration of the current global cooldown in ticks (for progress calculation) + */ + protected int globalCooldownDuration = 0; + + /** + * Per-ability cooldown end times. Key = ability key, Value = world time when cooldown ends. + */ + protected HashMap perAbilityCooldownEndTimes = new HashMap<>(); + + /** + * Per-ability cooldown durations in ticks (for progress calculation). + */ + protected HashMap perAbilityCooldownDurations = new HashMap<>(); + /** * Rotation lock state */ @@ -526,6 +542,106 @@ public abstract class AbstractDataAbilities { cooldownEndTime = endTime; } + /** + * Get the global cooldown end time. + */ + public long getCooldownEndTime() { + return cooldownEndTime; + } + + /** + * Get the global cooldown duration value. + */ + public int getGlobalCooldownDurationValue() { + return globalCooldownDuration; + } + + /** + * Get the per-ability cooldown end times map. + */ + public HashMap getPerAbilityCooldownEndTimes() { + return perAbilityCooldownEndTimes; + } + + /** + * Get the per-ability cooldown durations map. + */ + public HashMap getPerAbilityCooldownDurations() { + return perAbilityCooldownDurations; + } + + /** + * Apply cooldown sync data from server. Called on client side. + */ + public void applyCooldownSync(long globalEndTime, int globalDuration, + HashMap perEndTimes, + HashMap perDurations) { + this.cooldownEndTime = globalEndTime; + this.globalCooldownDuration = globalDuration; + this.perAbilityCooldownEndTimes = perEndTimes; + this.perAbilityCooldownDurations = perDurations; + } + + // ═══════════════════════════════════════════════════════════════════ + // PER-ABILITY COOLDOWN MANAGEMENT + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check if a specific ability is on its per-ability cooldown. + */ + public boolean isOnPerAbilityCooldown(String key) { + Long endTime = perAbilityCooldownEndTimes.get(key); + return endTime != null && getWorldTime() < endTime; + } + + /** + * Set a per-ability cooldown. + */ + public void setPerAbilityCooldown(String key, long endTime, int duration) { + perAbilityCooldownEndTimes.put(key, endTime); + perAbilityCooldownDurations.put(key, duration); + } + + /** + * Reset a specific per-ability cooldown. + */ + public void resetPerAbilityCooldown(String key) { + perAbilityCooldownEndTimes.remove(key); + perAbilityCooldownDurations.remove(key); + } + + /** + * Reset all per-ability cooldowns. + */ + public void resetAllPerAbilityCooldowns() { + perAbilityCooldownEndTimes.clear(); + perAbilityCooldownDurations.clear(); + } + + /** + * Get global cooldown progress for HUD rendering. + * @return 1.0 when cooldown just started (fully covered), 0.0 when done + */ + public float getGlobalCooldownProgress() { + if (globalCooldownDuration <= 0) return 0f; + long remaining = cooldownEndTime - getWorldTime(); + if (remaining <= 0) return 0f; + return Math.min(1f, (float) remaining / globalCooldownDuration); + } + + /** + * Get per-ability cooldown progress for HUD rendering. + * @return 1.0 when cooldown just started (fully covered), 0.0 when done + */ + public float getPerAbilityCooldownProgress(String key) { + Long endTime = perAbilityCooldownEndTimes.get(key); + Integer duration = perAbilityCooldownDurations.get(key); + if (endTime == null || duration == null || duration <= 0) return 0f; + long remaining = endTime - getWorldTime(); + if (remaining <= 0) return 0f; + return Math.min(1f, (float) remaining / duration); + } + // ═══════════════════════════════════════════════════════════════════ // POSITION LOCKING // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 44fb852c6..548939a54 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -222,13 +222,18 @@ public class DataAbilities extends AbstractDataAbilities { @Override protected void rollCooldown(Ability ability) { + // Always roll NPC global cooldown (pacing timer) int baseCooldown = minCooldown; if (maxCooldown > minCooldown) { baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); } - cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown; - // Add ability-specific cooldown offset - cooldownEndTime += ability.getCooldownTicks(); + cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + ability.getCooldownTicks(); + + // Additionally set per-ability cooldown if enabled + if (ability.isPerAbilityCooldown() && ability.getCooldownTicks() > 0) { + long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks(); + setPerAbilityCooldown(ability.getName(), endTime, ability.getCooldownTicks()); + } } @Override @@ -482,6 +487,14 @@ public class DataAbilities extends AbstractDataAbilities { return false; } + // Per-ability cooldown filter: ability may have its own independent cooldown + if (!action.isChain()) { + Ability ab = (Ability) action; + if (ab.isPerAbilityCooldown() && isOnPerAbilityCooldown(ab.getName())) { + return false; + } + } + return true; } @@ -726,6 +739,7 @@ public class DataAbilities extends AbstractDataAbilities { stopCurrentAbility(); clearActiveToggles(); interruptConcurrentSlots(); + resetAllPerAbilityCooldowns(); // Roll cooldown so NPC doesn't immediately attack after reset rollCooldownOnReset(); 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 index 56189a698..4b3be2d6a 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java @@ -56,7 +56,8 @@ public class AbilityHotbarComponent extends HudComponent { // 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 long lastFadeTime = 0; + private static final float FADE_SPEED = 3f; // per second — ~333ms full transition private Minecraft mc; @@ -124,6 +125,7 @@ public class AbilityHotbarComponent extends HudComponent { ConfigClient.AbilityHotbarTextPositionProperty.set(ConfigClient.AbilityHotbarTextPosition); ConfigClient.AbilityHotbarVisibleSlotsProperty.set(ConfigClient.AbilityHotbarVisibleSlots); ConfigClient.AbilityHotbarShowAlwaysProperty.set(ConfigClient.AbilityHotbarShowAlways); + ConfigClient.AbilityHotbarTextVisibilityProperty.set(ConfigClient.AbilityHotbarTextVisibility); if (ConfigClient.config.hasChanged()) { ConfigClient.config.save(); @@ -138,9 +140,12 @@ public class AbilityHotbarComponent extends HudComponent { // Fade logic: when ShowAlways=false, only show while HUD key is held boolean showAlways = ConfigClient.AbilityHotbarShowAlways; + long now = Minecraft.getSystemTime(); if (!showAlways) { boolean hudKeyHeld = isHudKeyHeld(); - float dt = partialTicks > 0 ? 1f / 20f : 0.05f; // approximate frame time + float dt = lastFadeTime > 0 ? (now - lastFadeTime) / 1000f : 0f; + dt = Math.min(dt, 0.1f); // cap to avoid jumps after pauses + lastFadeTime = now; if (hudKeyHeld || pendingSelectKey != null || scrollOffset != 0f) { fadeAlpha = Math.min(1f, fadeAlpha + FADE_SPEED * dt); } else { @@ -149,6 +154,7 @@ public class AbilityHotbarComponent extends HudComponent { if (fadeAlpha <= 0f) return; } else { fadeAlpha = 1f; + lastFadeTime = now; } long currentTime = Minecraft.getSystemTime(); @@ -184,12 +190,15 @@ public class AbilityHotbarComponent extends HudComponent { 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; - } + // Compute text visibility: 0=Shown, 1=Hidden, 2=Held + boolean showText; + int textVis = ConfigClient.AbilityHotbarTextVisibility; + if (textVis == 1) { + showText = false; + } else if (textVis == 2) { + showText = isHudKeyHeld(); + } else { + showText = true; } for (int i = 0; i < TOTAL_SLOTS; i++) { @@ -228,13 +237,18 @@ public class AbilityHotbarComponent extends HudComponent { 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; + + // Compute per-slot cooldown progress (all slots, not just center) + float slotCooldown = 0f; + if (rawSlotIndex >= 0 && slots[rawSlotIndex].abilityKey != null) { + slotCooldown = getCooldownProgressForSlot(rawSlotIndex); + } 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); + slots[rawSlotIndex].drawCarousel(mc, sr, slotCooldown, cx, cy, scaledSize, isCenter, slotAlpha, showText); } } @@ -441,6 +455,7 @@ public class AbilityHotbarComponent extends HudComponent { 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")); + buttonList.add(new GuiButton(6, 0, 0, 120, 20, getTextVisibilityLabel())); } @Override @@ -453,7 +468,8 @@ public class AbilityHotbarComponent extends HudComponent { ConfigClient.AbilityHotbarAltTexture = !ConfigClient.AbilityHotbarAltTexture; button.displayString = ConfigClient.AbilityHotbarAltTexture ? "Square" : "Circle"; } else if (button.id == 3) { - ConfigClient.AbilityHotbarTextPosition = (ConfigClient.AbilityHotbarTextPosition + 1) % 3; + // Toggle between 1 (Above/Left) and 2 (Below/Right) + ConfigClient.AbilityHotbarTextPosition = ConfigClient.AbilityHotbarTextPosition == 1 ? 2 : 1; button.displayString = getTextPositionLabel(); } else if (button.id == 4) { int current = ConfigClient.AbilityHotbarVisibleSlots; @@ -465,6 +481,9 @@ public class AbilityHotbarComponent extends HudComponent { } else if (button.id == 5) { ConfigClient.AbilityHotbarShowAlways = !ConfigClient.AbilityHotbarShowAlways; button.displayString = ConfigClient.AbilityHotbarShowAlways ? "Show: Always" : "Show: Hold Key"; + } else if (button.id == 6) { + ConfigClient.AbilityHotbarTextVisibility = (ConfigClient.AbilityHotbarTextVisibility + 1) % 3; + button.displayString = getTextVisibilityLabel(); } else { super.onEditorButtonPressed(button); } @@ -473,10 +492,18 @@ public class AbilityHotbarComponent extends HudComponent { 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"; + default: return h ? "Text: Below" : "Text: Right"; + } + } + + private String getTextVisibilityLabel() { + switch (ConfigClient.AbilityHotbarTextVisibility) { + case 0: return "Text: Shown"; + case 1: return "Text: Hidden"; + case 2: return "Text: Held"; + default: return "Text: Shown"; } } @@ -578,6 +605,25 @@ public class AbilityHotbarComponent extends HudComponent { return playerData.hotbarData.hasAnyAbilities(); } + /** + * Get cooldown progress for a hotbar slot. + * Per-ability cooldown abilities use their own cooldown; others use global. + * @return 1.0 = fully on cooldown, 0.0 = ready + */ + private float getCooldownProgressForSlot(int slotIndex) { + PlayerData playerData = ClientCacheHandler.playerData; + if (playerData == null || playerData.abilityData == null) return 0f; + + String key = slots[slotIndex].abilityKey; + Ability ability = slots[slotIndex].ability; + + if (ability != null && ability.isPerAbilityCooldown()) { + return playerData.abilityData.getPerAbilityCooldownProgress(key); + } else { + return playerData.abilityData.getGlobalCooldownProgress(); + } + } + public void onCycleNext() { cycle(1); } 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 index f23ac583a..c91ad016b 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarSlotRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarSlotRenderer.java @@ -47,7 +47,8 @@ public class AbilityHotbarSlotRenderer extends Gui { } public void drawCarousel(Minecraft mc, ScaledResolution sr, float cooldownProgress, - int cx, int cy, int size, boolean isCenter, float slotAlpha) { + int cx, int cy, int size, boolean isCenter, float slotAlpha, + boolean showText) { float nameAlpha = this.nameAlpha; boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal; if (size <= 0 || slotAlpha <= 0) return; @@ -91,16 +92,16 @@ public class AbilityHotbarSlotRenderer extends Gui { GL11.glPopMatrix(); } - // Cooldown arc + // Cooldown overlay (all slots) if (cooldownProgress > 0 && abilityKey != null) { - drawCooldownArc(radius, cooldownProgress); + drawCooldownOverlay(radius, cooldownProgress, altTexture, slotAlpha); } GL11.glPopMatrix(); - // Ability name on center slot (respects text position config) + // Ability name on center slot (respects text position and visibility config) int textPos = ConfigClient.AbilityHotbarTextPosition; - if (textPos != 0 && isCenter && action != null && nameAlpha > 0) { + if (showText && isCenter && action != null && nameAlpha > 0) { FontRenderer fr = mc.fontRenderer; String name = getAbilityName(); if (name != null && !name.isEmpty()) { @@ -185,26 +186,65 @@ public class AbilityHotbarSlotRenderer extends Gui { 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; + /** + * Draw a cooldown overlay on the slot. + * progress: 1.0 = fully on cooldown (just started), 0.0 = ready + * Renders a filled shape covering the entire slot that transitions from red to green. + */ + private void drawCooldownOverlay(float radius, float progress, boolean altTexture, float slotAlpha) { + progress = Math.min(1f, Math.max(0f, progress)); + if (progress <= 0f) return; + + // Color: red (progress=1.0) → orange → yellow-green → green (progress→0.0) + float cr, cg, cb; + if (progress > 0.5f) { + float t = (progress - 0.5f) * 2f; // 1.0 at progress=1.0, 0.0 at progress=0.5 + cr = 0.8f; + cg = 0.15f + 0.25f * (1f - t); // 0.15 → 0.4 + cb = 0.1f; + } else if (progress > 0.2f) { + float t = (progress - 0.2f) / 0.3f; // 1.0 at 0.5, 0.0 at 0.2 + cr = 0.8f * t + 0.3f * (1f - t); // 0.8 → 0.3 + cg = 0.4f * t + 0.7f * (1f - t); // 0.4 → 0.7 + cb = 0.1f; + } else { + float t = progress / 0.2f; // 1.0 at 0.2, 0.0 at 0.0 + cr = 0.3f * t + 0.1f * (1f - t); // 0.3 → 0.1 + cg = 0.7f * t + 0.7f * (1f - t); // 0.7 + cb = 0.1f + 0.1f * (1f - t); // 0.1 → 0.2 + } + + // Semi-transparent so icon is visible underneath; fade out near end + float overlayAlpha = 0.5f * slotAlpha * Math.min(1f, progress * 3f); 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(0.1f, 0.1f, 0.1f, 0.7f); + GL11.glColor4f(cr, cg, cb, overlayAlpha); - 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)); + if (altTexture) { + // Square slot: full filled rectangle + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex2f(-radius, -radius); + GL11.glVertex2f(radius, -radius); + GL11.glVertex2f(radius, radius); + GL11.glVertex2f(-radius, radius); + GL11.glEnd(); + } else { + // Circle slot: full filled circle + 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.glEnd(); GL11.glDisable(GL11.GL_BLEND); GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_CULL_FACE); GL11.glColor4f(1, 1, 1, 1); } 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 index 0f66036ae..1d0180463 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java @@ -525,6 +525,22 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { } int textW = fr.getStringWidth(stateText); fr.drawStringWithShadow(prefix + stateText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, toggleState > 0 ? 0x55FF55 : 0x888888); + y += fr.FONT_HEIGHT + 3; + } + + // Cooldown info + if (ability.getCooldownTicks() > 0) { + float seconds = ability.getCooldownTicks() / 20.0f; + String cdText = StatCollector.translateToLocal("ability.cooldown") + ": " + String.format("%.1f", seconds) + "s"; + int textW2 = fr.getStringWidth(cdText); + fr.drawStringWithShadow("\u00A77" + cdText, panelInnerLeft + (panelInnerWidth - textW2) / 2, y, 0xAAAAAA); + y += fr.FONT_HEIGHT + 3; + + if (ability.isPerAbilityCooldown()) { + String perText = "[" + StatCollector.translateToLocal("ability.perAbilityCooldown") + "]"; + int ptW = fr.getStringWidth(perText); + fr.drawStringWithShadow("\u00A7e" + perText, panelInnerLeft + (panelInnerWidth - ptW) / 2, y, 0xFFFF55); + } } } } @@ -663,6 +679,10 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { if (ability.isToggleable()) { lines.add("\u00A7a[" + StatCollector.translateToLocal("gui.toggle") + "]"); } + if (ability.getCooldownTicks() > 0) { + float seconds = ability.getCooldownTicks() / 20.0f; + lines.add("\u00A77" + StatCollector.translateToLocal("ability.cooldown") + ": " + String.format("%.1f", seconds) + "s"); + } } drawHoveringText(lines, mouseX, mouseY, mc.fontRenderer); diff --git a/src/main/java/noppes/npcs/config/ConfigClient.java b/src/main/java/noppes/npcs/config/ConfigClient.java index 5c1dc6c1a..3616627f4 100644 --- a/src/main/java/noppes/npcs/config/ConfigClient.java +++ b/src/main/java/noppes/npcs/config/ConfigClient.java @@ -114,7 +114,7 @@ public class ConfigClient { public static Property AbilityHotbarAltTextureProperty; public static boolean AbilityHotbarAltTexture = false; - // 0=Hide, 1=Above(H)/Left(V), 2=Below(H)/Right(V) + // 1=Above(H)/Left(V), 2=Below(H)/Right(V) public static Property AbilityHotbarTextPositionProperty; public static int AbilityHotbarTextPosition = 2; @@ -126,6 +126,10 @@ public class ConfigClient { public static Property AbilityHotbarShowAlwaysProperty; public static boolean AbilityHotbarShowAlways = true; + // Text Visibility: 0=Shown, 1=Hidden, 2=Held (only while HUD key held) + public static Property AbilityHotbarTextVisibilityProperty; + public static int AbilityHotbarTextVisibility = 0; + /** * Texture Properties **/ @@ -232,7 +236,7 @@ public class ConfigClient { 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)"); + AbilityHotbarTextPositionProperty = config.get(HUD, "Ability Hotbar Text Position", 2, "Text label position (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)"); @@ -241,6 +245,9 @@ public class ConfigClient { AbilityHotbarShowAlwaysProperty = config.get(HUD, "Ability Hotbar Show Always", true, "Always show hotbar (false = only while HUD key is held)"); AbilityHotbarShowAlways = AbilityHotbarShowAlwaysProperty.getBoolean(true); + AbilityHotbarTextVisibilityProperty = config.get(HUD, "Ability Hotbar Text Visibility", 0, "Text visibility (0=Shown, 1=Hidden, 2=Held)"); + AbilityHotbarTextVisibility = AbilityHotbarTextVisibilityProperty.getInt(0); + // 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."); AllowClientScripts = AllowClientScriptsProperty.getBoolean(true); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 72df5b16b..0e32a37e7 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -8,6 +8,7 @@ import kamkeel.npcs.controllers.data.ability.IAbilityAction; import kamkeel.npcs.controllers.data.ability.ToggleEntry; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; @@ -225,14 +226,30 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA @Override protected void rollCooldown(Ability ability) { - if (!ability.isIgnoreCooldown()) { - cooldownEndTime = getWorldTime() + ability.getCooldownTicks(); + if (ability.isIgnoreCooldown()) return; + + int duration = ability.getCooldownTicks(); + long endTime = getWorldTime() + duration; + + if (ability.isPerAbilityCooldown()) { + // Per-ability: only this ability goes on cooldown + if (currentAbilityKey != null) { + setPerAbilityCooldown(currentAbilityKey, endTime, duration); + } + } else { + // Global: all global-cooldown abilities share this cooldown + cooldownEndTime = endTime; + globalCooldownDuration = duration; } + syncCooldownToClient(); } @Override protected void rollChainCooldown(ChainedAbility chain) { - cooldownEndTime = getWorldTime() + chain.getCooldownTicks(); + int duration = chain.getCooldownTicks(); + cooldownEndTime = getWorldTime() + duration; + globalCooldownDuration = duration; + syncCooldownToClient(); } @Override @@ -372,8 +389,14 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA return true; } - // Check universal cooldown (abilities can optionally ignore it) - if (!ability.isIgnoreCooldown() && isOnCooldown()) return false; + // Check cooldown (per-ability cooldowns are independent from global) + if (!ability.isIgnoreCooldown()) { + if (ability.isPerAbilityCooldown()) { + if (isOnPerAbilityCooldown(key)) return false; + } else { + if (isOnCooldown()) return false; + } + } // Fire extender start hook (e.g., resource cost checks) if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) { @@ -428,6 +451,18 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA EntityPlayer player = playerData.player; if (player instanceof EntityPlayerMP) { PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player); + syncCooldownToClient(); + } + } + + /** + * Sync cooldown state (global + per-ability) to the client. + * Sent as a lightweight packet separate from full ability sync. + */ + public void syncCooldownToClient() { + EntityPlayer player = playerData.player; + if (player instanceof EntityPlayerMP) { + AbilityCooldownSyncPacket.sendToPlayer((EntityPlayerMP) player); } } @@ -547,9 +582,15 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA /** * Check if a specific ability key is on cooldown. - * Currently uses universal cooldown (same for all abilities). + * Respects per-ability cooldown if the ability has it enabled. */ public boolean isOnCooldown(String key, EntityPlayer player) { + if (AbilityController.Instance != null) { + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability != null && ability.isPerAbilityCooldown()) { + return isOnPerAbilityCooldown(key); + } + } return isOnCooldown(player); } @@ -560,17 +601,21 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA /** * Reset cooldown for a specific ability key. - * Currently resets the universal cooldown. + * Resets per-ability cooldown if applicable, and global cooldown. */ public void resetCooldown(String key) { + resetPerAbilityCooldown(key); cooldownEndTime = 0; + syncCooldownToClient(); } /** - * Reset all cooldowns. + * Reset all cooldowns (global + all per-ability). */ public void resetAllCooldowns() { cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); + syncCooldownToClient(); } // ═══════════════════════════════════════════════════════════════════ @@ -604,6 +649,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA @Override public void resetCooldown() { cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); + syncCooldownToClient(); } // ═══════════════════════════════════════════════════════════════════ @@ -656,6 +703,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (clearCooldowns) { cooldownEndTime = 0; + resetAllPerAbilityCooldowns(); } interruptCooldownRolled = false; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 810100e47..c2d575224 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2,7 +2,7 @@ customnpcs.defaultTextColor=404040 key.categories.customnpc=CustomNPC+ key.customnpcs.special=Special Key -key.customnpcs.abilityHudKey=Ability HUD Key +key.customnpcs.abilityHudKey=HUD Key key.npcInventory=NPC Inventory entity.CustomNpc.name=Custom Npc @@ -1672,6 +1672,7 @@ ability.cnpc.sweeper=Sweeper ability.weight=Weight ability.windUpTicks=Wind Up ability.cooldownTicks=Cooldown +ability.perAbilityCooldown=Per-Ability ability.lockMovement=Lock Movement ability.rotationMode=Rotation ability.rotationPhase=Phase