Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 58 additions & 18 deletions src/main/java/com/attacktimer/AttackTimerBarOverlay.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.attacktimer;

/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2018, Chdata
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* Copyright (c) 2022, Nick Graves <https://github.com/ngraves95>
* Copyright (c) 2025-2026, Pedro Alves <https://github.com/PedroSilvaAlves>
* Copyright (c) 2024-2026, Lexer747 <https://github.com/Lexer747>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand Down
Loading