diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java index 56691e4a24..db9f9908d3 100644 --- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java +++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java @@ -55,6 +55,7 @@ public enum VarPlayer NMZ_REWARD_POINTS(1060), ATTACKING_PLAYER(1075), + /** * -1 : Poison immune * Normal poison damage is ceil( this / 5.0f ) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/poison/ActorPoisonInfo.java b/runelite-client/src/main/java/net/runelite/client/plugins/poison/ActorPoisonInfo.java new file mode 100644 index 0000000000..dc9754be81 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/poison/ActorPoisonInfo.java @@ -0,0 +1,31 @@ +package net.runelite.client.plugins.poison; + +import lombok.Data; + +@Data +class ActorPoisonInfo +{ + /** + * Plain and simple, the last poison damage this actor received + */ + private int lastDamage; + + /** + * What the poison varp for this actor would be. -1 if unknown + * This should always be known if the actor is venomed + */ + private int accurateDamage; + + /** + * How many ticks after the tickcount is divisible by 30 the poison will hit. + * + * For instance, if something gets hit by poison on tick 607 this will be 7, as 607 % 30 == 7. + * With this info you know all the times to expect a poison hit + */ + private int cycle; + + /** + * This is to be able to remove the overlay if someone doesn't get hit + */ + private int lastDamageTick; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonActorOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonActorOverlay.java new file mode 100644 index 0000000000..755e49e950 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonActorOverlay.java @@ -0,0 +1,144 @@ +package net.runelite.client.plugins.poison; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.Map; +import javax.inject.Inject; +import lombok.Setter; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.Constants; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.SpriteID; +import net.runelite.api.coords.LocalPoint; +import static net.runelite.client.plugins.poison.PoisonPlugin.POISON_TICK_TICKS; +import static net.runelite.client.plugins.poison.PoisonPlugin.VENOM_THRESHOLD; +import static net.runelite.client.plugins.poison.PoisonPlugin.nextDamage; +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 PoisonActorOverlay extends Overlay +{ + private final PoisonPlugin plugin; + private final Client client; + private int fontSize; + private Font font; + + @Inject + PoisonActorOverlay(PoisonPlugin plugin, Client client) + { + this.plugin = plugin; + this.client = client; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + } + + @Override + public Dimension render(Graphics2D g) + { + final Map actors = plugin.getPoisonedActors(); + + if (actors.isEmpty()) + { + return null; + } + + final int tickCount = client.getTickCount(); + final int modTickCount = tickCount % 30; + + if (font == null) + { + font = g.getFont().deriveFont(fontSize * 2.0f); + } + + g.setFont(font); + + for (Map.Entry entry : actors.entrySet()) + { + Actor a = entry.getKey(); + + if (a == null) + { + continue; + } + + ActorPoisonInfo i = entry.getValue(); + + int accurateDamage = i.getAccurateDamage(); + int damage; + + if (accurateDamage != -1) + { + damage = nextDamage(accurateDamage); + } + else + { + damage = i.getLastDamage(); + } + + boolean venomed = accurateDamage >= VENOM_THRESHOLD; + + String timeLeft = getTimeLeft(modTickCount, i.getCycle()); + + renderOverlayFor(g, a, damage, timeLeft, venomed); + } + + return null; + } + + private String getTimeLeft(int tickCount, int cycle) + { + if (tickCount > cycle) + { + cycle += POISON_TICK_TICKS; + } + + int timeLeftMillis = (cycle - tickCount) * Constants.GAME_TICK_LENGTH; + + return String.valueOf(timeLeftMillis / 1000); + } + + private void renderOverlayFor(Graphics2D g, Actor actor, int damage, String timeLeft, boolean venomed) + { + BufferedImage splat = plugin.getSplat(venomed ? SpriteID.HITSPLAT_DARK_GREEN_VENOM : SpriteID.HITSPLAT_GREEN_POISON, damage); + + LocalPoint localLocation = actor.getLocalLocation(); + if (localLocation == null) + { + return; + } + + Point overlayLocation = Perspective.getCanvasImageLocation(client, localLocation, splat, 0); + + if (overlayLocation == null) + { + return; + } + + int textOffset = splat.getHeight() - (splat.getHeight() - fontSize) / 2; + + Point textLocation = new Point(overlayLocation.getX() + splat.getWidth() + 3, overlayLocation.getY() + textOffset); + + g.drawImage(splat, overlayLocation.getX(), overlayLocation.getY(), null); + OverlayUtil.renderTextLocation(g, textLocation, timeLeft, Color.WHITE); + } + + void setFontSize(int size) + { + if (font != null) + { + fontSize = size; + font = font.deriveFont(fontSize * 2.0f); + } + else + { + fontSize = size; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonConfig.java index 6c723d4f71..3f86cf9395 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonConfig.java @@ -52,4 +52,37 @@ public interface PoisonConfig extends Config { return true; } + + @ConfigItem( + keyName = "showPlayers", + name = "Show for players", + description = "Show poison timers for other players", + position = 1 + ) + default boolean showForPlayers() + { + return false; + } + + @ConfigItem( + keyName = "showNpcs", + name = "Show for NPCs", + description = "Show poison timers for NPCs", + position = 2 + ) + default boolean showForNpcs() + { + return false; + } + + @ConfigItem( + keyName = "fontsize", + name = "Font size", + description = "The size the time left text for other players/npc's will be", + position = 3 + ) + default int fontSize() + { + return 8; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonPlugin.java index 67a4136d34..ff39332758 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/poison/PoisonPlugin.java @@ -34,13 +34,23 @@ import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; import javax.inject.Inject; import lombok.Getter; +import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.GameState; +import net.runelite.api.Hitsplat; +import net.runelite.api.NPC; +import net.runelite.api.Player; import net.runelite.api.SpriteID; import net.runelite.api.VarPlayer; import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.PlayerDespawned; import net.runelite.api.events.VarbitChanged; import net.runelite.client.callback.ClientThread; import net.runelite.client.config.ConfigManager; @@ -62,8 +72,10 @@ import net.runelite.client.util.ImageUtil; public class PoisonPlugin extends Plugin { private static final int POISON_TICK_MILLIS = 18000; - private static final int VENOM_THRESHOLD = 1000000; + static final int VENOM_THRESHOLD = 1000000; + private static final int VENOM_UTILITY = 999997; private static final int VENOM_MAXIMUM_DAMAGE = 20; + static final int POISON_TICK_TICKS = 30; private static final BufferedImage HEART_DISEASE; private static final BufferedImage HEART_POISON; @@ -97,8 +109,12 @@ public class PoisonPlugin extends Plugin @Inject private PoisonConfig config; + @Inject + private PoisonActorOverlay actorOverlay; + @Getter private int lastDamage; + private boolean envenomed; private PoisonInfobox infobox; private Instant nextPoisonTick; @@ -107,6 +123,9 @@ public class PoisonPlugin extends Plugin private BufferedImage heart; private int nextTickCount; + @Getter + private Map poisonedActors = new HashMap<>(); + @Provides PoisonConfig getConfig(ConfigManager configManager) { @@ -116,8 +135,14 @@ public class PoisonPlugin extends Plugin @Override protected void startUp() throws Exception { + actorOverlay.setFontSize(config.fontSize()); overlayManager.add(poisonOverlay); + if (config.showForNpcs() || config.showForPlayers()) + { + overlayManager.add(actorOverlay); + } + if (client.getGameState() == GameState.LOGGED_IN) { clientThread.invoke(this::checkHealthIcon); @@ -140,6 +165,7 @@ public class PoisonPlugin extends Plugin nextPoisonTick = null; lastValue = 0; lastDiseaseValue = 0; + overlayManager.remove(actorOverlay); clientThread.invoke(this::resetHealthIcon); } @@ -193,6 +219,92 @@ public class PoisonPlugin extends Plugin } } + @Subscribe + private void onHitsplatApplied(HitsplatApplied event) + { + Hitsplat.HitsplatType type = event.getHitsplat().getHitsplatType(); + + if (type != Hitsplat.HitsplatType.POISON && type != Hitsplat.HitsplatType.VENOM) + { + return; + } + + Actor actor = event.getActor(); + + if (actor == client.getLocalPlayer() || + actor instanceof NPC && !config.showForNpcs() || + actor instanceof Player && !config.showForPlayers()) + { + return; + } + + int tickCount = client.getTickCount(); + int damage = event.getHitsplat().getAmount(); + + ActorPoisonInfo info = poisonedActors.get(actor); + + if (info == null) + { + info = new ActorPoisonInfo(); + info.setAccurateDamage(-1); + info.setLastDamage(damage); + + poisonedActors.put(actor, info); + } + + if (info.getAccurateDamage() != -1) + { + int accurateDamage = info.getAccurateDamage(); + accurateDamage -= 1; + + if (accurateDamage == 0) + { + poisonedActors.remove(actor); + return; + } + + info.setAccurateDamage(accurateDamage); + } + + if (type == Hitsplat.HitsplatType.VENOM) + { + info.setAccurateDamage(damage / 2 + VENOM_UTILITY + 1); + } + else if (info.getLastDamage() != damage) + { + // The damage changed so we know the accurate value! + // This may of course not be 100% accurate + // (if someone gets repoisoned for instance) + info.setAccurateDamage(damage * 5 - 1); + + info.setLastDamage(damage); + } + + info.setCycle(tickCount % POISON_TICK_TICKS); + info.setLastDamageTick(tickCount); + } + + @Subscribe + private void onGameTick(GameTick event) + { + int tickCount = client.getTickCount(); + + // Remove the actor if the last damage tick was over 35 ticks ago. + poisonedActors.values().removeIf(info -> info.getLastDamageTick() + POISON_TICK_TICKS + 5 < tickCount); + } + + @Subscribe + private void onNpcDespawned(NpcDespawned event) + { + poisonedActors.remove(event.getNpc()); + } + + @Subscribe + private void onPlayerDespawned(PlayerDespawned event) + { + poisonedActors.remove(event.getPlayer()); + } + @Subscribe public void onConfigChanged(ConfigChanged event) { @@ -215,9 +327,38 @@ public class PoisonPlugin extends Plugin { clientThread.invoke(this::resetHealthIcon); } + + if (event.getKey().startsWith("show")) + { + overlayManager.remove(actorOverlay); + + if (!config.showForPlayers() && !config.showForNpcs()) + { + poisonedActors.clear(); + } + else + { + if (!config.showForNpcs()) + { + poisonedActors.entrySet().removeIf(a -> a instanceof NPC); + } + + if (!config.showForPlayers()) + { + poisonedActors.entrySet().removeIf(a -> a instanceof Player); + } + + overlayManager.add(actorOverlay); + } + } + + if (event.getKey().equals("fontsize")) + { + actorOverlay.setFontSize(config.fontSize()); + } } - private static int nextDamage(int poisonValue) + static int nextDamage(int poisonValue) { int damage; @@ -225,7 +366,7 @@ public class PoisonPlugin extends Plugin { //Venom Damage starts at 6, and increments in twos; //The VarPlayer increments in values of 1, however. - poisonValue -= VENOM_THRESHOLD - 3; + poisonValue -= VENOM_UTILITY; damage = poisonValue * 2; //Venom Damage caps at 20, but the VarPlayer keeps increasing if (damage > VENOM_MAXIMUM_DAMAGE) @@ -241,7 +382,7 @@ public class PoisonPlugin extends Plugin return damage; } - private BufferedImage getSplat(int id, int damage) + BufferedImage getSplat(int id, int damage) { //Get a copy of the hitsplat to get a clean one each time final BufferedImage rawSplat = spriteManager.getSprite(id, 0);