From 9fe186cad22033484b4d81b800a24c1c47e35c99 Mon Sep 17 00:00:00 2001 From: Hexagon Date: Thu, 26 May 2022 21:02:21 -0300 Subject: [PATCH] spec counter: add spec drops Co-authored-by: Adam --- .../specialcounter/PlayerInfoDrop.java | 85 +++++++++++ .../specialcounter/PlayerInfoDropOverlay.java | 137 ++++++++++++++++++ .../specialcounter/SpecialCounterConfig.java | 31 +++- .../specialcounter/SpecialCounterPlugin.java | 51 ++++++- .../specialcounter/SpecialCounterUpdate.java | 2 + .../SpecialCounterPluginTest.java | 14 ++ 6 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDrop.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDropOverlay.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDrop.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDrop.java new file mode 100644 index 0000000000..09b95d7111 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDrop.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022, Hexagon + * 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.specialcounter; + +import java.awt.Color; +import java.awt.Font; +import java.awt.image.BufferedImage; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.runelite.client.ui.FontManager; + +@Data +@AllArgsConstructor(access = AccessLevel.PRIVATE) +class PlayerInfoDrop +{ + private final int startCycle; + private final int endCycle; + private final int playerIdx; + private final String text; + private final int startHeightOffset; + private final int endHeightOffset; + private final Font font; + private final Color color; + private final BufferedImage image; + + public static Builder builder(int startCycle, int endCycle, int playerIdx, String text) + { + return new Builder(startCycle, endCycle, playerIdx, text); + } + + @RequiredArgsConstructor + @Accessors(fluent = true) + @Setter + static class Builder + { + private final int startCycle; + private final int endCycle; + private final int playerIdx; + private final String text; + private int startHeightOffset = 0; + private int endHeightOffset = 200; + private Font font = FontManager.getRunescapeBoldFont(); + private Color color = Color.WHITE; + private BufferedImage image; + + public PlayerInfoDrop build() + { + if (startCycle > endCycle) + { + throw new IllegalArgumentException("endCycle must be after startCycle"); + } + if (playerIdx < 0 || playerIdx > 2047) + { + throw new IllegalArgumentException("playerIdx must be between 0-2047"); + } + return new PlayerInfoDrop(startCycle, endCycle, playerIdx, text, startHeightOffset, endHeightOffset, font, color, image); + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDropOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDropOverlay.java new file mode 100644 index 0000000000..497eb0d3d5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/PlayerInfoDropOverlay.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2022, Hexagon + * 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.specialcounter; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.Iterator; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Client; +import net.runelite.api.Player; +import net.runelite.api.Point; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.ImageUtil; + +@Singleton +class PlayerInfoDropOverlay extends Overlay +{ + private final SpecialCounterPlugin plugin; + private final SpecialCounterConfig config; + private final Client client; + + @Inject + private PlayerInfoDropOverlay(SpecialCounterPlugin plugin, SpecialCounterConfig config, Client client) + { + this.plugin = plugin; + this.config = config; + this.client = client; + setPosition(OverlayPosition.DYNAMIC); + setPriority(OverlayPriority.MED); + } + + @Override + public Dimension render(Graphics2D graphics) + { + final List infoDrops = plugin.getPlayerInfoDrops(); + if (infoDrops.isEmpty()) + { + return null; + } + + final int cycle = client.getGameCycle(); + for (Iterator iterator = infoDrops.iterator(); iterator.hasNext();) + { + PlayerInfoDrop infoDrop = iterator.next(); + + if (cycle < infoDrop.getStartCycle()) + { + continue; + } + + if (cycle > infoDrop.getEndCycle()) + { + iterator.remove(); + continue; + } + + if (!config.specDrops()) + { + continue; + } + + Player player = client.getCachedPlayers()[infoDrop.getPlayerIdx()]; + if (player == null) + { + continue; + } + + int elapsed = cycle - infoDrop.getStartCycle(); + int percent = elapsed * 100 / (infoDrop.getEndCycle() - infoDrop.getStartCycle()); + int currentHeight = infoDrop.getEndHeightOffset() * percent / 100; + String text = infoDrop.getText(); + + graphics.setFont(infoDrop.getFont()); + Point textLocation = player.getCanvasTextLocation(graphics, text, player.getLogicalHeight() + infoDrop.getStartHeightOffset() + currentHeight); + if (textLocation == null) + { + continue; + } + + int alpha = 255 - (255 * percent / 100); + BufferedImage image = infoDrop.getImage(); + if (image != null) + { + int textHeight = graphics.getFontMetrics().getHeight() - graphics.getFontMetrics().getMaxDescent(); + int textMargin = image.getWidth() / 2; + int x = textLocation.getX() - textMargin - 1; + int y = textLocation.getY() - textHeight / 2 - image.getHeight() / 2; + Point imageLocation = new Point(x, y); + + textLocation = new Point(textLocation.getX() + textMargin, textLocation.getY()); + + OverlayUtil.renderImageLocation(graphics, imageLocation, ImageUtil.alphaOffset(image, alpha - 255)); + } + + drawText(graphics, textLocation, text, infoDrop.getColor(), alpha); + } + return null; + } + + private static void drawText(Graphics2D g, Point point, String text, Color color, int colorAlpha) + { + g.setColor(ColorUtil.colorWithAlpha(Color.BLACK, colorAlpha)); + g.drawString(text, point.getX() + 1, point.getY() + 1); + g.setColor(ColorUtil.colorWithAlpha(color, colorAlpha)); + g.drawString(text, point.getX(), point.getY()); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java index 7eb596275a..4249fa3be8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java @@ -25,6 +25,7 @@ */ package net.runelite.client.plugins.specialcounter; +import java.awt.Color; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; @@ -45,6 +46,28 @@ public interface SpecialCounterConfig extends Config @ConfigItem( position = 1, + keyName = "specDrops", + name = "Spec Drops", + description = "Draws an overlay over the player when a special attack hits" + ) + default boolean specDrops() + { + return true; + } + + @ConfigItem( + position = 2, + keyName = "specDropColor", + name = "Spec Drop Color", + description = "Text color for spec drops" + ) + default Color specDropColor() + { + return Color.WHITE; + } + + @ConfigItem( + position = 10, keyName = "dragonWarhammerThreshold", name = "Dragon Warhammer", description = "Threshold for Dragon Warhammer (0 to disable)" @@ -55,7 +78,7 @@ public interface SpecialCounterConfig extends Config } @ConfigItem( - position = 2, + position = 20, keyName = "arclightThreshold", name = "Arclight", description = "Threshold for Arclight (0 to disable)" @@ -66,7 +89,7 @@ public interface SpecialCounterConfig extends Config } @ConfigItem( - position = 3, + position = 30, keyName = "darklightThreshold", name = "Darklight", description = "Threshold for Darklight (0 to disable)" @@ -77,7 +100,7 @@ public interface SpecialCounterConfig extends Config } @ConfigItem( - position = 4, + position = 40, keyName = "bandosGodswordThreshold", name = "Bandos Godsword", description = "Threshold for Bandos Godsword (0 to disable)" @@ -88,7 +111,7 @@ public interface SpecialCounterConfig extends Config } @ConfigItem( - position = 5, + position = 50, keyName = "bulwarkThreshold", name = "Dinh's Bulwark", description = "Threshold for Dinh's Bulwark (0 to disable)" diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java index 78fadb0242..b55c8c16b5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterPlugin.java @@ -26,11 +26,16 @@ package net.runelite.client.plugins.specialcounter; import com.google.common.collect.ImmutableSet; import com.google.inject.Provides; +import java.awt.image.BufferedImage; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Actor; import net.runelite.api.Client; @@ -44,6 +49,7 @@ import net.runelite.api.NPC; import net.runelite.api.NpcID; import net.runelite.api.VarPlayer; import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.CommandExecuted; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; import net.runelite.api.events.HitsplatApplied; @@ -57,7 +63,9 @@ import net.runelite.client.eventbus.Subscribe; import net.runelite.client.game.ItemManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.util.ImageUtil; import net.runelite.client.ws.PartyService; import net.runelite.client.ws.WSClient; @@ -95,6 +103,9 @@ public class SpecialCounterPlugin extends Plugin private final Set interactedNpcIds = new HashSet<>(); private final SpecialCounter[] specialCounter = new SpecialCounter[SpecialWeapon.values().length]; + @Getter(AccessLevel.PACKAGE) + private final List playerInfoDrops = new ArrayList<>(); + @Inject private Client client; @@ -119,6 +130,12 @@ public class SpecialCounterPlugin extends Plugin @Inject private SpecialCounterConfig config; + @Inject + private OverlayManager overlayManager; + + @Inject + private PlayerInfoDropOverlay playerInfoDropOverlay; + @Provides SpecialCounterConfig getConfig(ConfigManager configManager) { @@ -128,6 +145,7 @@ public class SpecialCounterPlugin extends Plugin @Override protected void startUp() { + overlayManager.add(playerInfoDropOverlay); wsClient.registerMessage(SpecialCounterUpdate.class); currentWorld = -1; specialPercentage = -1; @@ -140,6 +158,7 @@ public class SpecialCounterPlugin extends Plugin protected void shutDown() { removeCounters(); + overlayManager.remove(playerInfoDropOverlay); wsClient.unregisterMessage(SpecialCounterUpdate.class); } @@ -267,15 +286,18 @@ public class SpecialCounterPlugin extends Plugin if (wasSpec && specialWeapon != null && hitsplat.getAmount() > 0) { int hit = getHit(specialWeapon, hitsplat); + int localPlayerId = client.getLocalPlayer().getId(); updateCounter(specialWeapon, null, hit); if (!party.getMembers().isEmpty()) { - final SpecialCounterUpdate specialCounterUpdate = new SpecialCounterUpdate(interactingId, specialWeapon, hit); + final SpecialCounterUpdate specialCounterUpdate = new SpecialCounterUpdate(interactingId, specialWeapon, hit, client.getWorld(), localPlayerId); specialCounterUpdate.setMemberId(party.getLocalMember().getMemberId()); wsClient.send(specialCounterUpdate); } + + playerInfoDrops.add(createSpecInfoDrop(specialWeapon, hit, localPlayerId)); } } @@ -334,9 +356,23 @@ public class SpecialCounterPlugin extends Plugin { updateCounter(event.getWeapon(), name, event.getHit()); } + + if (event.getWorld() == client.getWorld()) + { + playerInfoDrops.add(createSpecInfoDrop(event.getWeapon(), event.getHit(), event.getPlayerId())); + } }); } + @Subscribe + public void onCommandExecuted(CommandExecuted commandExecuted) + { + if (commandExecuted.getCommand().equals("spec")) + { + playerInfoDrops.add(createSpecInfoDrop(SpecialWeapon.BANDOS_GODSWORD, 42, client.getLocalPlayer().getId())); + } + } + private SpecialWeapon usedSpecialWeapon() { ItemContainer equipment = client.getItemContainer(InventoryID.EQUIPMENT); @@ -424,4 +460,17 @@ public class SpecialCounterPlugin extends Plugin { return specialWeapon.isDamage() ? hitsplat.getAmount() : 1; } + + private PlayerInfoDrop createSpecInfoDrop(SpecialWeapon weapon, int hit, int playerId) + { + int cycle = client.getGameCycle(); + BufferedImage image = ImageUtil.resizeImage(itemManager.getImage(weapon.getItemID()[0]), 24, 24); + + return PlayerInfoDrop.builder(cycle, cycle + 100, playerId, Integer.toString(hit)) + .color(config.specDropColor()) + .startHeightOffset(100) + .endHeightOffset(400) + .image(image) + .build(); + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterUpdate.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterUpdate.java index 16eff1af25..2c7bcbe21b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterUpdate.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterUpdate.java @@ -35,4 +35,6 @@ public class SpecialCounterUpdate extends PartyMemberMessage private final int npcId; private final SpecialWeapon weapon; private final int hit; + private final int world; + private final int playerId; } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java index 977e379a9a..43241d448c 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/specialcounter/SpecialCounterPluginTest.java @@ -28,6 +28,7 @@ import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.awt.image.BufferedImage; import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.EquipmentInventorySlot; @@ -44,13 +45,16 @@ import net.runelite.api.events.InteractingChanged; import net.runelite.api.events.VarbitChanged; import net.runelite.client.Notifier; import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.util.AsyncBufferedImage; import net.runelite.client.ws.PartyService; import net.runelite.client.ws.WSClient; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import org.mockito.Mock; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -90,6 +94,14 @@ public class SpecialCounterPluginTest @Bind private SpecialCounterConfig specialCounterConfig; + @Mock + @Bind + private OverlayManager overlayManager; + + @Mock + @Bind + private PlayerInfoDropOverlay playerInfoDropOverlay; + @Inject private SpecialCounterPlugin specialCounterPlugin; @@ -107,6 +119,8 @@ public class SpecialCounterPluginTest when(client.getVar(VarPlayer.SPECIAL_ATTACK_PERCENT)).thenReturn(100); specialCounterPlugin.onVarbitChanged(new VarbitChanged()); + // Set up item image for spec info drop + when(itemManager.getImage(anyInt())).thenReturn(new AsyncBufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB)); } private static HitsplatApplied hitsplat(Actor target, Hitsplat.HitsplatType type)