From 2599db2e6de252d70f7319ca33cca2cfbc259020 Mon Sep 17 00:00:00 2001 From: WooxSolo Date: Mon, 4 Jun 2018 12:38:35 -0400 Subject: [PATCH 1/2] perspective: fix getCanvasTileAreaPoly for even number sizes --- .../src/main/java/net/runelite/api/Perspective.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/runelite-api/src/main/java/net/runelite/api/Perspective.java b/runelite-api/src/main/java/net/runelite/api/Perspective.java index 1309ef598e..8b4e801f3d 100644 --- a/runelite-api/src/main/java/net/runelite/api/Perspective.java +++ b/runelite-api/src/main/java/net/runelite/api/Perspective.java @@ -291,14 +291,10 @@ public class Perspective public static Polygon getCanvasTileAreaPoly(@Nonnull Client client, @Nonnull LocalPoint localLocation, int size) { int plane = client.getPlane(); - int halfTile = LOCAL_TILE_SIZE / 2; - - // If the size is 5, we need to shift it up and left 2 units, then expand by 5 units to make a 5x5 - int aoeSize = size / 2; // Shift over one half tile as localLocation is the center point of the tile, and then shift the area size - Point southWestCorner = new Point(localLocation.getX() - (aoeSize * LOCAL_TILE_SIZE) - halfTile + 1, - localLocation.getY() - (aoeSize * LOCAL_TILE_SIZE) - halfTile + 1); + Point southWestCorner = new Point(localLocation.getX() - (size * LOCAL_TILE_SIZE / 2), + localLocation.getY() - (size * LOCAL_TILE_SIZE / 2)); // expand by size Point northEastCorner = new Point(southWestCorner.getX() + size * LOCAL_TILE_SIZE - 1, southWestCorner.getY() + size * LOCAL_TILE_SIZE - 1); From c2511d50c8705944eafc8878eeea89ef9d897791 Mon Sep 17 00:00:00 2001 From: WooxSolo Date: Mon, 4 Jun 2018 12:39:23 -0400 Subject: [PATCH 2/2] npc indicators: add respawn timer --- .../java/net/runelite/api/AnimationID.java | 1 + .../main/java/net/runelite/api/GraphicID.java | 1 + .../plugins/npchighlight/MemorizedNpc.java | 79 ++++++ .../npchighlight/NpcIndicatorsConfig.java | 10 + .../npchighlight/NpcIndicatorsPlugin.java | 254 +++++++++++++++++- ...ckboxOverlay.java => NpcSceneOverlay.java} | 98 ++++++- 6 files changed, 431 insertions(+), 12 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java rename runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/{NpcClickboxOverlay.java => NpcSceneOverlay.java} (52%) diff --git a/runelite-api/src/main/java/net/runelite/api/AnimationID.java b/runelite-api/src/main/java/net/runelite/api/AnimationID.java index 9bd54688ac..21341e549e 100644 --- a/runelite-api/src/main/java/net/runelite/api/AnimationID.java +++ b/runelite-api/src/main/java/net/runelite/api/AnimationID.java @@ -129,6 +129,7 @@ public final class AnimationID public static final int DEMONIC_GORILLA_AOE_ATTACK = 7228; public static final int DEMONIC_GORILLA_PRAYER_SWITCH = 7228; public static final int DEMONIC_GORILLA_DEFEND = 7224; + public static final int IMP_DEATH = 172; // NPC animations public static final int TZTOK_JAD_MAGIC_ATTACK = 2656; diff --git a/runelite-api/src/main/java/net/runelite/api/GraphicID.java b/runelite-api/src/main/java/net/runelite/api/GraphicID.java index f770d39ada..d6fc6a2fae 100644 --- a/runelite-api/src/main/java/net/runelite/api/GraphicID.java +++ b/runelite-api/src/main/java/net/runelite/api/GraphicID.java @@ -27,6 +27,7 @@ package net.runelite.api; public class GraphicID { public static final int TELEPORT = 111; + public static final int GREY_BUBBLE_TELEPORT = 86; public static final int ENTANGLE = 179; public static final int SNARE = 180; public static final int BIND = 181; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java new file mode 100644 index 0000000000..d9e1cad9a4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/MemorizedNpc.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018, Woox + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.npchighlight; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.coords.WorldPoint; + +class MemorizedNpc +{ + @Getter + private int npcIndex; + + @Getter + private String npcName; + + @Getter + private int npcSize; + + /** + * The time the npc died at, in game ticks, relative to the tick counter + */ + @Getter + @Setter + private int diedOnTick; + + /** + * The time it takes for the npc to respawn, in game ticks + */ + @Getter + @Setter + private int respawnTime; + + @Getter + @Setter + private List possibleRespawnLocations; + + MemorizedNpc(NPC npc) + { + this.npcName = npc.getName(); + this.npcIndex = npc.getIndex(); + this.possibleRespawnLocations = new ArrayList<>(); + this.respawnTime = -1; + this.diedOnTick = -1; + + final NPCComposition composition = npc.getTransformedComposition(); + + if (composition != null) + { + this.npcSize = composition.getSize(); + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java index 5772a8972c..a5fc0c332a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsConfig.java @@ -101,4 +101,14 @@ public interface NpcIndicatorsConfig extends Config { return false; } + + @ConfigItem( + position = 6, + keyName = "showRespawnTimer", + name = "Show respawn timer", + description = "Show respawn timer of tagged NPCs") + default boolean showRespawnTimer() + { + return false; + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java index 28e3751875..2ca74f4dcb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java @@ -28,12 +28,15 @@ package net.runelite.client.plugins.npchighlight; import com.google.common.base.Splitter; import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.inject.Inject; @@ -41,10 +44,15 @@ import lombok.AccessLevel; import lombok.Getter; import net.runelite.api.Client; import net.runelite.api.GameState; +import net.runelite.api.GraphicID; +import net.runelite.api.GraphicsObject; import net.runelite.api.NPC; +import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.FocusChanged; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.GraphicsObjectCreated; import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; @@ -59,6 +67,8 @@ import net.runelite.client.util.WildcardMatcher; @PluginDescriptor(name = "NPC Indicators") public class NpcIndicatorsPlugin extends Plugin { + private static final int MAX_ACTOR_VIEW_RANGE = 15; + // Option added to NPC menu private static final String TAG = "Tag"; @@ -75,7 +85,7 @@ public class NpcIndicatorsPlugin extends Plugin private NpcIndicatorsConfig config; @Inject - private NpcClickboxOverlay npcClickboxOverlay; + private NpcSceneOverlay npcSceneOverlay; @Inject private NpcMinimapOverlay npcMinimapOverlay; @@ -92,6 +102,24 @@ public class NpcIndicatorsPlugin extends Plugin @Getter(AccessLevel.PACKAGE) private final Set highlightedNpcs = new HashSet<>(); + /** + * Dead NPCs that should be displayed with a respawn indicator if the config is on. + */ + @Getter(AccessLevel.PACKAGE) + private final Map deadNpcsToDisplay = new HashMap<>(); + + /** + * The time when the last game tick event ran. + */ + @Getter(AccessLevel.PACKAGE) + private Instant lastTickUpdate; + + /** + * Tagged NPCs that have died at some point, which are memorized to + * remember when and where they will respawn + */ + private final Map memorizedNpcs = new HashMap<>(); + /** * Highlight strings from the configuration */ @@ -102,6 +130,35 @@ public class NpcIndicatorsPlugin extends Plugin */ private final Set npcTags = new HashSet<>(); + /** + * Tagged NPCs that spawned this tick, which need to be verified that + * they actually spawned and didn't just walk into view range. + */ + private final List spawnedNpcsThisTick = new ArrayList<>(); + + /** + * Tagged NPCs that despawned this tick, which need to be verified that + * they actually spawned and didn't just walk into view range. + */ + private final List despawnedNpcsThisTick = new ArrayList<>(); + + /** + * World locations of graphics object which indicate that an + * NPC teleported that were played this tick. + */ + private final Set teleportGraphicsObjectSpawnedThisTick = new HashSet<>(); + + /** + * The players location on the last game tick. + */ + private WorldPoint lastPlayerLocation; + + /** + * When hopping worlds, NPCs can spawn without them actually respawning, + * so we would not want to mark it as a real spawn in those cases. + */ + private boolean skipNextSpawnCheck = false; + private boolean hotKeyPressed = false; @Provides @@ -121,6 +178,11 @@ public class NpcIndicatorsPlugin extends Plugin @Override protected void shutDown() throws Exception { + deadNpcsToDisplay.clear(); + memorizedNpcs.clear(); + spawnedNpcsThisTick.clear(); + despawnedNpcsThisTick.clear(); + teleportGraphicsObjectSpawnedThisTick.clear(); npcTags.clear(); highlightedNpcs.clear(); keyManager.unregisterKeyListener(inputListener); @@ -129,9 +191,14 @@ public class NpcIndicatorsPlugin extends Plugin @Subscribe public void onGameStateChange(GameStateChanged event) { - if (event.getGameState() == GameState.LOGIN_SCREEN || event.getGameState() == GameState.HOPPING) + if (event.getGameState() == GameState.LOGIN_SCREEN || + event.getGameState() == GameState.HOPPING) { highlightedNpcs.clear(); + deadNpcsToDisplay.clear(); + memorizedNpcs.forEach((id, npc) -> npc.setDiedOnTick(-1)); + lastPlayerLocation = null; + skipNextSpawnCheck = true; } } @@ -172,9 +239,11 @@ public class NpcIndicatorsPlugin extends Plugin if (removed) { highlightedNpcs.remove(npc); + memorizedNpcs.remove(npc.getIndex()); } else { + memorizeNpc(npc); npcTags.add(id); highlightedNpcs.add(npc); } @@ -192,7 +261,9 @@ public class NpcIndicatorsPlugin extends Plugin { if (npcTags.contains(npc.getIndex())) { + memorizeNpc(npc); highlightedNpcs.add(npc); + spawnedNpcsThisTick.add(npc); return; } @@ -200,7 +271,9 @@ public class NpcIndicatorsPlugin extends Plugin { if (WildcardMatcher.matches(highlight, npcName)) { + memorizeNpc(npc); highlightedNpcs.add(npc); + spawnedNpcsThisTick.add(npc); break; } } @@ -210,13 +283,98 @@ public class NpcIndicatorsPlugin extends Plugin @Subscribe public void onNpcDespawned(NpcDespawned npcDespawned) { - highlightedNpcs.remove(npcDespawned.getNpc()); + final NPC npc = npcDespawned.getNpc(); + + if (memorizedNpcs.containsKey(npc.getIndex())) + { + despawnedNpcsThisTick.add(npc); + } + + highlightedNpcs.remove(npc); + } + + @Subscribe + public void onGraphicsObjectCreated(GraphicsObjectCreated event) + { + final GraphicsObject go = event.getGraphicsObject(); + + if (go.getId() == GraphicID.GREY_BUBBLE_TELEPORT) + { + teleportGraphicsObjectSpawnedThisTick.add(WorldPoint.fromLocal(client, go.getLocation())); + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + removeOldHighlightedRespawns(); + validateSpawnedNpcs(); + lastTickUpdate = Instant.now(); + lastPlayerLocation = client.getLocalPlayer().getWorldLocation(); } @Override public Collection getOverlays() { - return Arrays.asList(npcClickboxOverlay, npcMinimapOverlay); + return Arrays.asList(npcSceneOverlay, npcMinimapOverlay); + } + + private static boolean isInViewRange(WorldPoint wp1, WorldPoint wp2) + { + int distance = wp1.distanceTo(wp2); + return distance < MAX_ACTOR_VIEW_RANGE; + } + + private static WorldPoint getWorldLocationBehind(NPC npc) + { + final int orientation = npc.getOrientation() / 256; + int dx = 0, dy = 0; + + switch (orientation) + { + case 0: // South + dy = -1; + break; + case 1: // Southwest + dx = -1; + dy = -1; + break; + case 2: // West + dx = -1; + break; + case 3: // Northwest + dx = -1; + dy = 1; + break; + case 4: // North + dy = 1; + break; + case 5: // Northeast + dx = 1; + dy = 1; + break; + case 6: // East + dx = 1; + break; + case 7: // Southeast + dx = 1; + dy = -1; + break; + } + + final WorldPoint currWP = npc.getWorldLocation(); + return new WorldPoint(currWP.getX() - dx, currWP.getY() - dy, currWP.getPlane()); + } + + private void memorizeNpc(NPC npc) + { + final int npcIndex = npc.getIndex(); + memorizedNpcs.putIfAbsent(npcIndex, new MemorizedNpc(npc)); + } + + private void removeOldHighlightedRespawns() + { + deadNpcsToDisplay.values().removeIf(x -> x.getDiedOnTick() + x.getRespawnTime() <= client.getTickCount() + 1); } void updateNpcMenuOptions(boolean pressed) @@ -254,9 +412,10 @@ public class NpcIndicatorsPlugin extends Plugin { highlightedNpcs.clear(); + outer: for (NPC npc : client.getNpcs()) { - String npcName = npc.getName(); + final String npcName = npc.getName(); if (npcName == null) { @@ -273,11 +432,94 @@ public class NpcIndicatorsPlugin extends Plugin { if (WildcardMatcher.matches(highlight, npcName)) { + memorizeNpc(npc); highlightedNpcs.add(npc); - break; + continue outer; } } + + // NPC is not highlighted + memorizedNpcs.remove(npc.getIndex()); } } + private void validateSpawnedNpcs() + { + if (skipNextSpawnCheck) + { + skipNextSpawnCheck = false; + } + else + { + for (NPC npc : despawnedNpcsThisTick) + { + if (!teleportGraphicsObjectSpawnedThisTick.isEmpty()) + { + if (teleportGraphicsObjectSpawnedThisTick.contains(npc.getWorldLocation())) + { + // NPC teleported away, so we don't want to add the respawn timer + continue; + } + } + + if (isInViewRange(client.getLocalPlayer().getWorldLocation(), npc.getWorldLocation())) + { + final MemorizedNpc mn = memorizedNpcs.get(npc.getIndex()); + + if (mn != null) + { + mn.setDiedOnTick(client.getTickCount() + 1); // This runs before tickCounter updates, so we add 1 + + if (!mn.getPossibleRespawnLocations().isEmpty()) + { + deadNpcsToDisplay.put(mn.getNpcIndex(), mn); + } + } + } + } + + for (NPC npc : spawnedNpcsThisTick) + { + if (!teleportGraphicsObjectSpawnedThisTick.isEmpty()) + { + if (teleportGraphicsObjectSpawnedThisTick.contains(npc.getWorldLocation()) || + teleportGraphicsObjectSpawnedThisTick.contains(getWorldLocationBehind(npc))) + { + // NPC teleported here, so we don't want to update the respawn timer + continue; + } + } + + if (isInViewRange(lastPlayerLocation, npc.getWorldLocation())) + { + final MemorizedNpc mn = memorizedNpcs.get(npc.getIndex()); + + if (mn.getDiedOnTick() != -1) + { + mn.setRespawnTime(client.getTickCount() + 1 - mn.getDiedOnTick()); + mn.setDiedOnTick(-1); + } + + final WorldPoint npcLocation = npc.getWorldLocation(); + + // An NPC can move in the same tick as it spawns, so we also have + // to consider whatever tile is behind the npc + final WorldPoint possibleOtherNpcLocation = getWorldLocationBehind(npc); + + mn.getPossibleRespawnLocations().removeIf(x -> + x.distanceTo(npcLocation) != 0 && x.distanceTo(possibleOtherNpcLocation) != 0); + + if (mn.getPossibleRespawnLocations().isEmpty()) + { + mn.getPossibleRespawnLocations().add(npcLocation); + mn.getPossibleRespawnLocations().add(possibleOtherNpcLocation); + } + } + } + } + + spawnedNpcsThisTick.clear(); + despawnedNpcsThisTick.clear(); + teleportGraphicsObjectSpawnedThisTick.clear(); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcClickboxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java similarity index 52% rename from runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcClickboxOverlay.java rename to runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java index 6f5afa687e..850228656c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcClickboxOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java @@ -30,23 +30,45 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Polygon; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.time.Instant; +import java.util.Locale; import javax.inject.Inject; import net.runelite.api.Client; import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Perspective; import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayUtil; -public class NpcClickboxOverlay extends Overlay +public class NpcSceneOverlay extends Overlay { + // Anything but white text is quite hard to see since it is drawn on + // a dark background + private static final Color TEXT_COLOR = Color.WHITE; + + // Estimated time of a game tick in seconds + private static final double ESTIMATED_TICK_LENGTH = 0.6; + + private static final NumberFormat TIME_LEFT_FORMATTER = DecimalFormat.getInstance(Locale.US); + + static + { + ((DecimalFormat)TIME_LEFT_FORMATTER).applyPattern("#0.0"); + } + private final Client client; private final NpcIndicatorsConfig config; private final NpcIndicatorsPlugin plugin; @Inject - NpcClickboxOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin) + NpcSceneOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin) { this.client = client; this.config = config; @@ -58,6 +80,11 @@ public class NpcClickboxOverlay extends Overlay @Override public Dimension render(Graphics2D graphics) { + if (config.showRespawnTimer()) + { + plugin.getDeadNpcsToDisplay().forEach((id, npc) -> renderNpcRespawn(npc, graphics)); + } + for (NPC npc : plugin.getHighlightedNpcs()) { renderNpcOverlay(graphics, npc, npc.getName(), config.getHighlightColor()); @@ -66,22 +93,81 @@ public class NpcClickboxOverlay extends Overlay return null; } + private void renderNpcRespawn(final MemorizedNpc npc, final Graphics2D graphics) + { + if (npc.getPossibleRespawnLocations().isEmpty()) + { + return; + } + + final WorldPoint respawnLocation = npc.getPossibleRespawnLocations().get(0); + final LocalPoint lp = LocalPoint.fromWorld(client, respawnLocation.getX(), respawnLocation.getY()); + + if (lp == null) + { + return; + } + + final Color color = config.getHighlightColor(); + + final LocalPoint centerLp = new LocalPoint( + lp.getX() + Perspective.LOCAL_TILE_SIZE * (npc.getNpcSize() - 1) / 2, + lp.getY() + Perspective.LOCAL_TILE_SIZE * (npc.getNpcSize() - 1) / 2); + + final Polygon poly = Perspective.getCanvasTileAreaPoly(client, centerLp, npc.getNpcSize()); + + if (poly != null) + { + OverlayUtil.renderPolygon(graphics, poly, color); + } + + final Instant now = Instant.now(); + final double baseTick = (npc.getDiedOnTick() + npc.getRespawnTime()) - client.getTickCount() * ESTIMATED_TICK_LENGTH; + final double sinceLast = (now.toEpochMilli() - plugin.getLastTickUpdate().toEpochMilli()) / 1000.0; + final double timeLeft = Math.max(0.0, baseTick - sinceLast); + final String timeLeftStr = TIME_LEFT_FORMATTER.format(timeLeft); + + final int textWidth = graphics.getFontMetrics().stringWidth(timeLeftStr); + final int textHeight = graphics.getFontMetrics().getAscent(); + + final Point canvasPoint = Perspective + .worldToCanvas(client, centerLp.getX(), centerLp.getY(), respawnLocation.getPlane()); + + if (canvasPoint != null) + { + final Point canvasCenterPoint = new Point( + canvasPoint.getX() - textWidth / 2, + canvasPoint.getY() + textHeight / 2); + + OverlayUtil.renderTextLocation(graphics, canvasCenterPoint, timeLeftStr, TEXT_COLOR); + } + } + private void renderNpcOverlay(Graphics2D graphics, NPC actor, String name, Color color) { switch (config.renderStyle()) { case TILE: - Polygon objectTile = actor.getCanvasTilePoly(); + { + int size = 1; + NPCComposition composition = actor.getTransformedComposition(); + if (composition != null) + { + size = composition.getSize(); + } + LocalPoint lp = actor.getLocalLocation(); + Polygon tilePoly = Perspective.getCanvasTileAreaPoly(client, lp, size); - if (objectTile != null) + if (tilePoly != null) { graphics.setColor(color); graphics.setStroke(new BasicStroke(2)); - graphics.draw(objectTile); + graphics.draw(tilePoly); graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20)); - graphics.fill(objectTile); + graphics.fill(tilePoly); } break; + } case HULL: Polygon objectClickbox = actor.getConvexHull();