Complete Ability Hud System + Added-Per-Ability Cooldown System to be Reflected in Hud and Hud GUI

This commit is contained in:
Kam committed 2026-02-20 03:47:12 -05:00
1 parent 877fe2b275
commit 44118ec770
13 files changed
+467 -47

No files matched your search

@@ -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;
}
@@ -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() {
@@ -65,6 +65,7 @@ public enum EnumDataPacket {
PLAYER_ABILITY_SYNC,
PLAYER_ABILITY_STATE,
ABILITY_HOTBAR_SYNC,
ABILITY_COOLDOWN_SYNC,
// Auction System
AUCTION_DATA,
@@ -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<String, Long> perAbilityCooldownEndTimes;
private HashMap<String, Integer> perAbilityCooldownDurations;
public AbilityCooldownSyncPacket() {
}
public AbilityCooldownSyncPacket(long globalEndTime, int globalDuration,
HashMap<String, Long> perEndTimes,
HashMap<String, Integer> 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<String, Long> 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<String, Long> perEndTimes = new HashMap<>();
HashMap<String, Integer> 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);
}
}
@@ -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<String, Long> perAbilityCooldownEndTimes = new HashMap<>();
/**
* Per-ability cooldown durations in ticks (for progress calculation).
*/
protected HashMap<String, Integer> 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<String, Long> getPerAbilityCooldownEndTimes() {
return perAbilityCooldownEndTimes;
}
/**
* Get the per-ability cooldown durations map.
*/
public HashMap<String, Integer> getPerAbilityCooldownDurations() {
return perAbilityCooldownDurations;
}
/**
* Apply cooldown sync data from server. Called on client side.
*/
public void applyCooldownSync(long globalEndTime, int globalDuration,
HashMap<String, Long> perEndTimes,
HashMap<String, Integer> 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
// ═══════════════════════════════════════════════════════════════════
+17 -3
View File
@@ -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();
@@ -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);
}
@@ -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);
}
@@ -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);
@@ -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);
@@ -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;