diff --git a/src/main/java/com/attacktimer/AttackTimerBarOverlay.java b/src/main/java/com/attacktimer/AttackTimerBarOverlay.java index 2bb1eb2..e6039fa 100644 --- a/src/main/java/com/attacktimer/AttackTimerBarOverlay.java +++ b/src/main/java/com/attacktimer/AttackTimerBarOverlay.java @@ -1,7 +1,12 @@ package com.attacktimer; + /* - * Copyright (c) 2018, Tomas Slusny * Copyright (c) 2018, Chdata + * Copyright (c) 2018, Tomas Slusny + * Copyright (c) 2022, Nick Graves + * Copyright (c) 2025-2026, Pedro Alves + * Copyright (c) 2024-2026, Lexer747 + * * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,6 +36,7 @@ import java.awt.image.BufferedImage; import javax.inject.Inject; import javax.inject.Singleton; +import lombok.NonNull; import net.runelite.api.Client; import net.runelite.api.Perspective; import net.runelite.api.Point; @@ -42,23 +48,25 @@ import net.runelite.client.ui.overlay.OverlayPriority; import net.runelite.client.util.ImageUtil; - @Singleton class AttackTimerBarOverlay extends Overlay { private static final Color BAR_FILL_COLOR = new Color(201, 161, 28); private static final Color BAR_BG_COLOR = Color.black; private static final Dimension ATTACK_BAR_SIZE = new Dimension(30, 5); - private static final BufferedImage HD_FRONT_BAR = ImageUtil.loadImageResource(AttackTimerMetronomePlugin.class, "/front.png"); - private static final BufferedImage HD_BACK_BAR = ImageUtil.loadImageResource(AttackTimerMetronomePlugin.class, "/back.png"); - private final Client client; + private static final BufferedImage HD_FRONT_BAR = ImageUtil.loadImageResource(AttackTimerMetronomePlugin.class, + "/front.png"); + private static final BufferedImage HD_BACK_BAR = ImageUtil.loadImageResource(AttackTimerMetronomePlugin.class, + "/back.png"); + private final @NonNull Client client; private final AttackTimerMetronomeConfig config; private final AttackTimerMetronomePlugin plugin; private boolean shouldShowBar = false; @Inject - private AttackTimerBarOverlay(final Client client, final AttackTimerMetronomeConfig config, final AttackTimerMetronomePlugin plugin) + private AttackTimerBarOverlay(final Client client, final AttackTimerMetronomeConfig config, + final AttackTimerMetronomePlugin plugin) { this.client = client; this.config = config; @@ -78,23 +86,37 @@ public Dimension render(Graphics2D graphics) { return null; } + if (client == null) + { + return null; + } final int height = client.getLocalPlayer().getLogicalHeight() + config.heightOffset() + 5; final LocalPoint localLocation = client.getLocalPlayer().getLocalLocation(); + if (localLocation == null) + { + return null; + } final Point canvasPoint = Perspective.localToCanvas(client, localLocation, client.getTopLevelWorldView().getPlane(), height); + if (canvasPoint == null) + { + return null; + } int denomMod = (config.barEmpties()) ? 1 : 0; int numerMod = (config.barFills()) ? 1 : 0; float ratio = (float) (plugin.getTicksUntilNextAttack() - numerMod) / (float) (plugin.getWeaponPeriod() - denomMod); - if (!config.barDirection()) { - ratio = (float)Math.max(1.0f - ratio, 0f); - } + // barDirection: + // true -> drain -> moves right to left each tick + // false -> fills -> move left to right each tick + ratio = config.barDirection() ? clampBetween0and1(ratio) : clampBetween0and1(1.0f - ratio); AttackBarStyle barStyle = config.barStyle(); - boolean useHD = barStyle == AttackBarStyle.HIGH_DETAIL - || (barStyle == AttackBarStyle.AUTO && client.getSpriteOverrides().containsKey(SpriteID.HEALTHBAR_DEFAULT_FRONT_30PX)); + boolean useHD = barStyle == AttackBarStyle.HIGH_DETAIL || (barStyle == AttackBarStyle.AUTO + && client.getSpriteOverrides().containsKey(SpriteID.HEALTHBAR_DEFAULT_FRONT_30PX)); - if (useHD) { + if (useHD) + { final int barWidth = HD_FRONT_BAR.getWidth(); final int barHeight = HD_FRONT_BAR.getHeight(); final int barX = canvasPoint.getX() - barWidth / 2; @@ -103,8 +125,16 @@ public Dimension render(Graphics2D graphics) final int progressFill = (int) Math.ceil(Math.min((barWidth * ratio), barWidth)); graphics.drawImage(HD_BACK_BAR, barX, barY, barWidth, barHeight, null); - // Use a sub-image to create the same effect the HD Health Bar has - graphics.drawImage(HD_FRONT_BAR.getSubimage(0, 0, progressFill, barHeight), barX, barY, progressFill, barHeight, null); + // if progress is less than 1 then there's no "sub-bar" to draw as it would either have fractional + // pixels (not possible) or negative pixels (also not possible) fixes + // (https://github.com/ngraves95/attacktimer/issues/92) + if (progressFill < 1) + { + return null; + } + // else draw a smaller bar as the "filler" image + var subImage = HD_FRONT_BAR.getSubimage(0, 0, progressFill, barHeight); + graphics.drawImage(subImage, barX, barY, progressFill, barHeight, null); return null; } // Draw bar @@ -113,7 +143,8 @@ public Dimension render(Graphics2D graphics) final int barWidth = ATTACK_BAR_SIZE.width; final int barHeight = ATTACK_BAR_SIZE.height; - // Restricted by the width to prevent the bar from being too long while you are boosted above your real prayer level. + // Restricted by the width to prevent the bar from being too long while you are boosted above your + // real prayer level. final int progressFill = (int) Math.ceil(Math.min((barWidth * ratio), barWidth)); graphics.setColor(BAR_BG_COLOR); @@ -123,19 +154,28 @@ public Dimension render(Graphics2D graphics) return null; } + + private float clampBetween0and1(float x) + { + return Math.max(Math.min(x, 1.0f), 0.0f); + } + private void onTick() { shouldShowBar = true; - if (!config.enableMetronome()) { + if (!config.enableMetronome()) + { shouldShowBar = false; } - if (!config.showBar()) { + if (!config.showBar()) + { shouldShowBar = false; } - if (!plugin.isAttackCooldownPending()) { + if (!plugin.isAttackCooldownPending()) + { shouldShowBar = false; } }