diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java index 8bf97e3988..7ef58aaf4e 100644 --- a/runelite-api/src/main/java/net/runelite/api/Varbits.java +++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java @@ -433,6 +433,11 @@ public enum Varbits */ ACCOUNT_TYPE(1777), + /** + * Varbit used for Slayer reward points + */ + SLAYER_REWARD_POINTS(4068), + /** * The varbit that stores the oxygen percentage for player */ diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index 5c8a344756..e21a3796d3 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -54,12 +54,14 @@ import net.runelite.client.game.ClanManager; import net.runelite.client.game.ItemManager; import net.runelite.client.game.LootManager; import net.runelite.client.game.chatbox.ChatboxPanelManager; +import net.runelite.client.graphics.ModelOutlineRenderer; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginInstantiationException; import net.runelite.client.plugins.PluginManager; import net.runelite.client.plugins.config.ConfigPanel; import net.runelite.client.rs.ClientUpdateCheckMode; +import net.runelite.client.task.Scheduler; import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.DrawManager; import net.runelite.client.ui.RuneLiteSplashScreen; @@ -166,6 +168,12 @@ public class RuneLite @Nullable private Client client; + @Inject + private Provider modelOutlineRenderer; + + @Inject + private Scheduler scheduler; + public static void main(String[] args) throws Exception { Locale.setDefault(Locale.ENGLISH); @@ -337,6 +345,12 @@ public class RuneLite // Start plugins pluginManager.startCorePlugins(); + + // Register additional schedulers + if (this.client != null) + { + scheduler.registerObject(modelOutlineRenderer.get()); + } } public void shutdown() diff --git a/runelite-client/src/main/java/net/runelite/client/graphics/ModelOutlineRenderer.java b/runelite-client/src/main/java/net/runelite/client/graphics/ModelOutlineRenderer.java new file mode 100644 index 0000000000..a1a0cb0c99 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/graphics/ModelOutlineRenderer.java @@ -0,0 +1,973 @@ +/* + * 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.graphics; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import net.runelite.api.Client; +import net.runelite.api.MainBufferProvider; +import net.runelite.api.Model; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Perspective; +import net.runelite.api.Player; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.task.Schedule; + +@Singleton +public class ModelOutlineRenderer +{ + /* + * This class doesn't really "need" static variables, but they are + * static for performance reasons. Arrays are kept outside methods + * to avoid frequent big allocations. Arrays should mostly be seen + * as ArrayLists. The size of them is increased whenever they need + * to become bigger. + */ + + private final Client client; + + private boolean isReset; + private boolean usedSinceLastCheck; + + // Dimensions of the underlying image + private int imageWidth; + private int imageHeight; + + // Boundaries for the current rasterization + private int clipX1; + private int clipY1; + private int clipX2; + private int clipY2; + + // Pixel points that would be rendered to + private int[] visited; + private int currentVisitedNumber = 0; + + // Transformed vertex positions + private int[] projectedVerticesX; + private int[] projectedVerticesY; + private boolean[] projectedVerticesRenderable; + + // An array of pixel points to raster onto the image. These are checked against + // clip boundaries and the visited array to prevent drawing on top of the model + // and outside the scene area. They are grouped per distance to the closest pixel + // drawn on the model. + private int[][] outlinePixels; + private int[] outlinePixelsLengths; // outlinePixelsLength[i] is the used length of outlinePixels[i] + private int outlineArrayWidth; + + // A list of pixel distances ordered from shortest to longest distance for + // each outline width. These are calculated once upon first usage and then + // stored here to prevent reevaluation. + private List> precomputedDistancePriorities; + + @Inject + private ModelOutlineRenderer(Client client) + { + this.client = client; + + reset(); + } + + @Schedule(period = 5, unit = ChronoUnit.SECONDS) + public void checkUsage() + { + if (!isReset && !usedSinceLastCheck) + { + // Reset memory allocated when the rasterizer becomes inactive + reset(); + } + usedSinceLastCheck = false; + } + + /** + * Reset memory used by the rasterizer + */ + private void reset() + { + visited = new int[0]; + projectedVerticesX = new int[0]; + projectedVerticesY = new int[0]; + projectedVerticesRenderable = new boolean[0]; + outlinePixels = new int[0][]; + outlinePixelsLengths = new int[0]; + precomputedDistancePriorities = new ArrayList<>(0); + isReset = true; + } + + /** + * Calculate the next power of two of a value + * + * @param value The value to find the next power of two of + * @return Returns the next power of two + */ + private static int nextPowerOfTwo(int value) + { + value--; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value++; + return value; + } + + /** + * Determine if a triangle goes counter clockwise + * + * @return Returns true if the triangle goes counter clockwise and should be culled, otherwise false + */ + private static boolean cullFace(int x1, int y1, int x2, int y2, int x3, int y3) + { + return + (y2 - y1) * (x3 - x2) - + (x2 - x1) * (y3 - y2) < 0; + } + + /** + * Gets the list of pixel distances ordered by distance from closest pixel for a specific outline width. + * + * @param outlineWidth The outline width + * @return Returns the list of pixel distances + */ + private List getPriorityList(int outlineWidth) + { + while (precomputedDistancePriorities.size() <= outlineWidth) + { + precomputedDistancePriorities.add(null); + } + + // Grab the cached outline width if we have one + if (precomputedDistancePriorities.get(outlineWidth) != null) + { + return precomputedDistancePriorities.get(outlineWidth); + } + + List ps = new ArrayList<>(); + for (int x = 0; x <= outlineWidth; x++) + { + for (int y = 0; y <= outlineWidth; y++) + { + if (x == 0 && y == 0) + { + continue; + } + + double dist = Math.sqrt(x * x + y * y); + if (dist > outlineWidth) + { + continue; + } + + int outerAlpha = outlineWidth == 1 ? 255 // For preventing division by 0 + : (int) (255 * (dist - 1) / (outlineWidth - 1)); + ps.add(new PixelDistanceAlpha(outerAlpha, x + y * outlineArrayWidth)); + } + } + ps.sort(Comparator.comparingDouble(PixelDistanceAlpha::getOuterAlpha)); + precomputedDistancePriorities.set(outlineWidth, ps); + + return ps; + } + + /** + * Checks that the size of outlinePixels is big enough to hold a specific + * amount of elements. This is used to reduce the amount of if checks needed + * when adding elements to outlinePixels. + * + * @param distArrayPos The position in the array + * @param additionalMinimumSize The additional minimum size required + */ + private void ensureMinimumOutlineQueueSize(int distArrayPos, int additionalMinimumSize) + { + int minimumSize = outlinePixelsLengths[distArrayPos] + additionalMinimumSize; + while (outlinePixels[distArrayPos].length < minimumSize) + { + int[] newArr = new int[nextPowerOfTwo(minimumSize)]; + System.arraycopy(outlinePixels[distArrayPos], 0, newArr, 0, + outlinePixels[distArrayPos].length); + outlinePixels[distArrayPos] = newArr; + } + } + + /** + * Resets the visited flag for a specific amount of pixels + * + * @param pixelAmount The amount of pixels to reset + */ + private void resetVisited(int pixelAmount) + { + // The visited array is essentially a boolean array, but by + // making it an int array and checking if visited[i] == currentVisitedNumber + // and changing currentVisitedNumber for every new outline, we can essentially + // reset the whole array without having to iterate over every element + + if (visited.length < pixelAmount) + { + visited = new int[nextPowerOfTwo(pixelAmount)]; + currentVisitedNumber = 0; + } + + currentVisitedNumber++; + } + + /** + * Resets the pixels that are queued for outlining + * + * @param outlineWidth The width of the outline to reset pixels for + */ + private void resetOutline(int outlineWidth) + { + outlineArrayWidth = outlineWidth + 2; + + int arraySizes = outlineArrayWidth * outlineArrayWidth; + if (outlinePixels.length < arraySizes) + { + outlinePixels = new int[arraySizes][]; + outlinePixelsLengths = new int[arraySizes]; + for (int i = 0; i < arraySizes; i++) + { + outlinePixels[i] = new int[4]; + } + } + else + { + for (int i = 0; i < arraySizes; i++) + { + outlinePixelsLengths[i] = 0; + } + } + } + + /** + * Simulates a horizontal line rasterization and adds the pixels to the left + * and to the right to the outline queue if they are within the clip area. + * + * @param pixelPos The pixel position in the line where x == 0 + * @param x1 The starting x position + * @param x2 The ending x position + */ + private void simulateHorizontalLineRasterizationForOutline( + int pixelPos, int x1, int x2) + { + if (x2 > clipX2) + { + x2 = clipX2; + } + if (x1 < clipX1) + { + x1 = clipX1; + } + if (x1 >= x2) + { + return; + } + + // Queue the pixel positions to the left and to the right of the line + ensureMinimumOutlineQueueSize(1, 2); + if (x2 < clipX2) + { + outlinePixels[1][outlinePixelsLengths[1]++] = pixelPos + x2; + } + if (x1 > clipX1) + { + outlinePixels[1][outlinePixelsLengths[1]++] = pixelPos + x1 - 1; + } + + // Divide by 4 to account for loop unrolling + int xDist = x2 - x1 >> 2; + pixelPos += x1; + + // This loop could run over 100m times per second without loop unrolling in some cases, + // so unrolling it can give a noticeable performance boost. + while (xDist-- > 0) + { + visited[pixelPos++] = currentVisitedNumber; + visited[pixelPos++] = currentVisitedNumber; + visited[pixelPos++] = currentVisitedNumber; + visited[pixelPos++] = currentVisitedNumber; + } + + // Draw up to 3 more pixels if there were any left + xDist = (x2 - x1) & 3; + while (xDist-- > 0) + { + visited[pixelPos++] = currentVisitedNumber; + } + } + + /** + * Queues the pixel positions above and below two horizontal lines, excluding those + * where the x positions of the lines intersect. + * + * @param pixelPos The pixel position at x == 0 of the second line + * @param x1 The starting x position of the first line + * @param x2 The ending x position of the first line + * @param x3 The starting x position of the second line + * @param x4 The ending x position of the second line + */ + private void outlineAroundHorizontalLine( + int pixelPos, int x1, int x2, int x3, int x4) + { + if (x1 < clipX1) + { + x1 = clipX1; + } + if (x2 < clipX1) + { + x2 = clipX1; + } + if (x3 < clipX1) + { + x3 = clipX1; + } + if (x4 < clipX1) + { + x4 = clipX1; + } + + if (x1 > clipX2) + { + x1 = clipX2; + } + if (x2 > clipX2) + { + x2 = clipX2; + } + if (x3 > clipX2) + { + x3 = clipX2; + } + if (x4 > clipX2) + { + x4 = clipX2; + } + + if (x1 < x3) + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x3 - x1); + for (int x = x1; x < x3; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos - imageWidth + x; + } + } + else + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x1 - x3); + for (int x = x3; x < x1; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos + x; + } + } + + if (x2 < x4) + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x4 - x2); + for (int x = x2; x < x4; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos + x; + } + } + else + { + ensureMinimumOutlineQueueSize(outlineArrayWidth, x2 - x4); + for (int x = x4; x < x2; x++) + { + outlinePixels[outlineArrayWidth][outlinePixelsLengths[outlineArrayWidth]++] = pixelPos - imageWidth + x; + } + } + } + + /** + * Simulates rasterization of a triangle and adds every pixel outside the triangle + * to the outline queue. + * + * @param x1 The x position of the first vertex in the triangle + * @param y1 The y position of the first vertex in the triangle + * @param x2 The x position of the second vertex in the triangle + * @param y2 The y position of the second vertex in the triangle + * @param x3 The x position of the third vertex in the triangle + * @param y3 The y position of the third vertex in the triangle + */ + private void simulateTriangleRasterizationForOutline( + int x1, int y1, int x2, int y2, int x3, int y3) + { + // Swap vertices so y1 <= y2 <= y3 using bubble sort + if (y1 > y2) + { + int yp = y1; + int xp = x1; + y1 = y2; + y2 = yp; + x1 = x2; + x2 = xp; + } + if (y2 > y3) + { + int yp = y2; + int xp = x2; + y2 = y3; + y3 = yp; + x2 = x3; + x3 = xp; + } + if (y1 > y2) + { + int yp = y1; + int xp = x1; + y1 = y2; + y2 = yp; + x1 = x2; + x2 = xp; + } + + if (y1 > clipY2) + { + // All points are outside clip boundaries + return; + } + + int slope1 = 0; + if (y1 != y2) + { + slope1 = (x2 - x1 << 14) / (y2 - y1); + } + + int slope2 = 0; + if (y3 != y2) + { + slope2 = (x3 - x2 << 14) / (y3 - y2); + } + + int slope3 = 0; + if (y1 != y3) + { + slope3 = (x1 - x3 << 14) / (y1 - y3); + } + + if (y2 > clipY2) + { + y2 = clipY2; + } + if (y3 > clipY2) + { + y3 = clipY2; + } + if (y1 == y3 || y3 < 0) + { + return; + } + + x1 <<= 14; + x2 <<= 14; + x3 = x1; + + if (y1 < 0) + { + x3 -= y1 * slope3; + x1 -= y1 * slope1; + y1 = 0; + } + if (y2 < 0) + { + x2 -= slope2 * y2; + y2 = 0; + } + + int pixelPos = y1 * imageWidth; + int currX1; + int currX2; + if (y1 != y2 && slope3 < slope1 || y1 == y2 && slope3 > slope2) + { + int height1 = y2 - y1; + int height2 = y3 - y2; + + int prevX1; + int prevX2; + if (height1 <= 0) + { + prevX1 = x3 >> 14; + prevX2 = x2 >> 14; + } + else + { + prevX1 = x3 >> 14; + prevX2 = x1 >> 14; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX2, prevX2, prevX2); + + while (height1-- > 0) + { + currX1 = x3 >> 14; + currX2 = x1 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x3 += slope3; + x1 += slope1; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + while (height2-- > 0) + { + currX1 = x3 >> 14; + currX2 = x2 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x3 += slope3; + x2 += slope2; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX1, prevX1, prevX2); + } + else + { + int height1 = y2 - y1; + int height2 = y3 - y2; + + int prevX1; + int prevX2; + if (height1 <= 0) + { + prevX1 = x2 >> 14; + prevX2 = x3 >> 14; + } + else + { + prevX1 = x1 >> 14; + prevX2 = x3 >> 14; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX2, prevX2, prevX2); + + while (height1-- > 0) + { + currX1 = x1 >> 14; + currX2 = x3 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x1 += slope1; + x3 += slope3; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + while (height2-- > 0) + { + currX1 = x2 >> 14; + currX2 = x3 >> 14; + outlineAroundHorizontalLine(pixelPos, currX1, currX2, prevX1, prevX2); + simulateHorizontalLineRasterizationForOutline(pixelPos, currX1, currX2); + x3 += slope3; + x2 += slope2; + pixelPos += imageWidth; + prevX1 = currX1; + prevX2 = currX2; + } + + outlineAroundHorizontalLine(pixelPos, prevX1, prevX1, prevX1, prevX2); + } + } + + /** + * Translates the vertices 3D points to the screen canvas 2D points + * + * @param localX The local x position of the vertices + * @param localY The local y position of the vertices + * @param localZ The local z position of the vertices + * @param vertexOrientation The orientation of the vertices + * @return Returns true if any of them are inside the clip area, otherwise false + */ + private boolean projectVertices(Model model, + final int localX, final int localY, final int localZ, final int vertexOrientation) + { + final int cameraX = client.getCameraX(); + final int cameraY = client.getCameraY(); + final int cameraZ = client.getCameraZ(); + final int cameraYaw = client.getCameraYaw(); + final int cameraPitch = client.getCameraPitch(); + final int scale = client.getScale(); + final int orientationSin = Perspective.SINE[vertexOrientation]; + final int orientationCos = Perspective.COSINE[vertexOrientation]; + final int pitchSin = Perspective.SINE[cameraPitch]; + final int pitchCos = Perspective.COSINE[cameraPitch]; + final int yawSin = Perspective.SINE[cameraYaw]; + final int yawCos = Perspective.COSINE[cameraYaw]; + final int vertexCount = model.getVerticesCount(); + final int[] verticesX = model.getVerticesX(); + final int[] verticesY = model.getVerticesY(); + final int[] verticesZ = model.getVerticesZ(); + + boolean anyVisible = false; + + // Make sure the arrays are big enough + while (projectedVerticesX.length < vertexCount) + { + int newSize = nextPowerOfTwo(vertexCount); + projectedVerticesX = new int[newSize]; + projectedVerticesY = new int[newSize]; + projectedVerticesRenderable = new boolean[newSize]; + } + + for (int i = 0; i < vertexCount; i++) + { + int vx = verticesX[i]; + int vy = verticesZ[i]; + int vz = verticesY[i]; + int vh; // Value holder + + // Rotate based on orientation + vh = vx * orientationCos + vy * orientationSin >> 16; + vy = vy * orientationCos - vx * orientationSin >> 16; + vx = vh; + + // Translate to local coords + vx += localX; + vy += localY; + vz += localZ; + + // Translate to camera + vx -= cameraX; + vy -= cameraY; + vz -= cameraZ; + + // Transform to canvas + vh = vx * yawCos + vy * yawSin >> 16; + vy = vy * yawCos - vx * yawSin >> 16; + vx = vh; + vh = vz * pitchCos - vy * pitchSin >> 16; + vz = vz * pitchSin + vy * pitchCos >> 16; + vy = vh; + + if (vz >= 50) + { + projectedVerticesX[i] = (clipX1 + clipX2) / 2 + vx * scale / vz; + projectedVerticesY[i] = (clipY1 + clipY2) / 2 + vy * scale / vz; + + projectedVerticesRenderable[i] = true; + anyVisible |= + projectedVerticesX[i] >= clipX1 && projectedVerticesX[i] < clipX2 && + projectedVerticesY[i] >= clipY1 && projectedVerticesY[i] < clipY2; + } + else + { + projectedVerticesRenderable[i] = false; + } + } + + return anyVisible; + } + + /** + * Simulate rendering of the model and puts every pixel of the wireframe of + * the non-culled and non-transparent faces into the outline pixel queue. + */ + private void simulateModelRasterizationForOutline(Model model) + { + final int triangleCount = model.getTrianglesCount(); + final int[] indices1 = model.getTrianglesX(); + final int[] indices2 = model.getTrianglesY(); + final int[] indices3 = model.getTrianglesZ(); + final byte[] triangleTransparencies = model.getTriangleTransparencies(); + + for (int i = 0; i < triangleCount; i++) + { + if (projectedVerticesRenderable[indices1[i]] && + projectedVerticesRenderable[indices2[i]] && + projectedVerticesRenderable[indices3[i]] && + // 254 and 255 counts as fully transparent + (triangleTransparencies == null || (triangleTransparencies[i] & 255) < 254)) + { + final int index1 = indices1[i]; + final int index2 = indices2[i]; + final int index3 = indices3[i]; + final int v1x = projectedVerticesX[index1]; + final int v1y = projectedVerticesY[index1]; + final int v2x = projectedVerticesX[index2]; + final int v2y = projectedVerticesY[index2]; + final int v3x = projectedVerticesX[index3]; + final int v3y = projectedVerticesY[index3]; + + if (!cullFace(v1x, v1y, v2x, v2y, v3x, v3y)) + { + simulateTriangleRasterizationForOutline( + v1x, v1y, v2x, v2y, v3x, v3y); + } + } + } + } + + /** + * Draws an outline of the pixels in the outline queue to an image + * + * @param image The image to draw the outline to + * @param outlineWidth The width of the outline + * @param innerColor The color of the pixels of the outline closest to the model + * @param outerColor The color of the pixels of the outline furthest away from the model + */ + private void renderOutline(BufferedImage image, int outlineWidth, + Color innerColor, Color outerColor) + { + int[] imageData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + List ps = getPriorityList(outlineWidth); + + for (PixelDistanceAlpha p : ps) + { + int color; + int alpha; + if (outlineWidth == 1) + { + color = + ((innerColor.getRed() + outerColor.getRed()) << 15) | + ((innerColor.getGreen() + outerColor.getGreen() << 7)) | + ((innerColor.getBlue() + outerColor.getBlue() >> 1)); + alpha = (innerColor.getAlpha() + outerColor.getAlpha()) >> 1; + } + else + { + int outerAlpha = p.getOuterAlpha(); + int innerAlpha = 255 - outerAlpha; + int innerAlphaFraction = (innerAlpha * innerColor.getAlpha()) / 255; + int outerAlphaFraction = (outerAlpha * outerColor.getAlpha()) / 255; + alpha = innerAlphaFraction + outerAlphaFraction; + if (alpha != 0) + { + color = + ((innerColor.getRed() * innerAlphaFraction + + outerColor.getRed() * outerAlphaFraction) / alpha << 16) | + ((innerColor.getGreen() * innerAlphaFraction + + outerColor.getGreen() * outerAlphaFraction) / alpha << 8) | + ((innerColor.getBlue() * innerAlphaFraction + + outerColor.getBlue() * outerAlphaFraction) / alpha); + } + else + { + color = 0; + } + } + + final int distArrayPos = p.getDistArrayPos(); + final int nextDistArrayPosY = distArrayPos + outlineArrayWidth; + final int nextDistArrayPosX = distArrayPos + 1; + ensureMinimumOutlineQueueSize(nextDistArrayPosX, outlinePixelsLengths[distArrayPos] * 2); + ensureMinimumOutlineQueueSize(nextDistArrayPosY, outlinePixelsLengths[distArrayPos] * 2); + + // The following 3 branches do the same thing, but when the requirements are simple, + // there are less checks needed which can give a performance boost. + if (alpha == 255) + { + if (outlineWidth == 1) + { + for (int i2 = 0; i2 < outlinePixelsLengths[distArrayPos]; i2++) + { + int pixelPos = outlinePixels[distArrayPos][i2]; + int x = pixelPos % imageWidth; + int y = pixelPos / imageWidth; + if (x < clipX1 || x >= clipX2 || + y < clipY1 || y >= clipY2 || + visited[pixelPos] == currentVisitedNumber) + { + continue; + } + + imageData[pixelPos] = color; + } + } + else + { + for (int i2 = 0; i2 < outlinePixelsLengths[distArrayPos]; i2++) + { + int pixelPos = outlinePixels[distArrayPos][i2]; + int x = pixelPos % imageWidth; + int y = pixelPos / imageWidth; + if (x < clipX1 || x >= clipX2 || + y < clipY1 || y >= clipY2 || + visited[pixelPos] == currentVisitedNumber) + { + continue; + } + visited[pixelPos] = currentVisitedNumber; + + imageData[pixelPos] = color; + + if (pixelPos % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos - 1; + } + if ((pixelPos + 1) % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos + 1; + } + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos - imageWidth; + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos + imageWidth; + } + } + } + else + { + for (int i2 = 0; i2 < outlinePixelsLengths[distArrayPos]; i2++) + { + int pixelPos = outlinePixels[distArrayPos][i2]; + int x = pixelPos % imageWidth; + int y = pixelPos / imageWidth; + if (x < clipX1 || x >= clipX2 || + y < clipY1 || y >= clipY2 || + visited[pixelPos] == currentVisitedNumber) + { + continue; + } + visited[pixelPos] = currentVisitedNumber; + + imageData[pixelPos] = + ((((color & 0xFF0000) * alpha + (imageData[pixelPos] & 0xFF0000) * (255 - alpha)) / 255) & 0xFF0000) + + ((((color & 0xFF00) * alpha + (imageData[pixelPos] & 0xFF00) * (255 - alpha)) / 255) & 0xFF00) + + ((((color & 0xFF) * alpha + (imageData[pixelPos] & 0xFF) * (255 - alpha)) / 255) & 0xFF); + + if (pixelPos % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos - 1; + } + if ((pixelPos + 1) % imageWidth != 0) + { + outlinePixels[nextDistArrayPosX][outlinePixelsLengths[nextDistArrayPosX]++] = pixelPos + 1; + } + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos - imageWidth; + outlinePixels[nextDistArrayPosY][outlinePixelsLengths[nextDistArrayPosY]++] = pixelPos + imageWidth; + } + } + } + } + + /** + * Draws an outline around a model to an image + * + * @param localX The local x position of the model + * @param localY The local y position of the model + * @param localZ The local z position of the model + * @param orientation The orientation of the model + * @param outlineWidth The width of the outline + * @param innerColor The color of the pixels of the outline closest to the model + * @param outerColor The color of the pixels of the outline furthest away from the model + */ + private void drawModelOutline(Model model, + int localX, int localY, int localZ, int orientation, + int outlineWidth, Color innerColor, Color outerColor) + { + if (outlineWidth <= 0) + { + return; + } + + isReset = false; + usedSinceLastCheck = true; + + MainBufferProvider bufferProvider = (MainBufferProvider) client.getBufferProvider(); + BufferedImage image = (BufferedImage) bufferProvider.getImage(); + + clipX1 = client.getViewportXOffset(); + clipY1 = client.getViewportYOffset(); + clipX2 = client.getViewportWidth() + clipX1; + clipY2 = client.getViewportHeight() + clipY1; + imageWidth = image.getWidth(); + imageHeight = image.getHeight(); + final int pixelAmount = imageWidth * imageHeight; + + resetVisited(pixelAmount); + resetOutline(outlineWidth); + + if (!projectVertices(model, + localX, localY, localZ, orientation)) + { + // No vertex of the model is visible on the screen, so we can + // assume there are no parts of the model to outline. + return; + } + + simulateModelRasterizationForOutline(model); + + renderOutline(image, outlineWidth, innerColor, outerColor); + } + + public void drawOutline(NPC npc, int outlineWidth, Color color) + { + drawOutline(npc, outlineWidth, color, color); + } + + public void drawOutline(NPC npc, int outlineWidth, + Color innerColor, Color outerColor) + { + int size = 1; + NPCComposition composition = npc.getTransformedComposition(); + if (composition != null) + { + size = composition.getSize(); + } + + LocalPoint lp = npc.getLocalLocation(); + if (lp != null) + { + // NPCs z position are calculated based on the tile height of the northeastern tile + final int northEastX = lp.getX() + Perspective.LOCAL_TILE_SIZE * (size - 1) / 2; + final int northEastY = lp.getY() + Perspective.LOCAL_TILE_SIZE * (size - 1) / 2; + final LocalPoint northEastLp = new LocalPoint(northEastX, northEastY); + + drawModelOutline(npc.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, northEastLp, client.getPlane()), + npc.getOrientation(), outlineWidth, innerColor, outerColor); + } + } + + public void drawOutline(Player player, int outlineWidth, Color color) + { + drawOutline(player, outlineWidth, color, color); + } + + public void drawOutline(Player player, int outlineWidth, + Color innerColor, Color outerColor) + { + LocalPoint lp = player.getLocalLocation(); + if (lp != null) + { + drawModelOutline(player.getModel(), lp.getX(), lp.getY(), + Perspective.getTileHeight(client, lp, client.getPlane()), + player.getOrientation(), outlineWidth, innerColor, outerColor); + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/graphics/PixelDistanceAlpha.java b/runelite-client/src/main/java/net/runelite/client/graphics/PixelDistanceAlpha.java new file mode 100644 index 0000000000..4c3232399d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/graphics/PixelDistanceAlpha.java @@ -0,0 +1,36 @@ +/* + * 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.graphics; + +import lombok.RequiredArgsConstructor; +import lombok.Value; + +@Value +@RequiredArgsConstructor +class PixelDistanceAlpha +{ + private final int outerAlpha; + private final int distArrayPos; +} \ 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 1ef10c50d8..2b35ee2bf0 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 @@ -40,7 +40,7 @@ public interface NpcIndicatorsConfig extends Config ) default RenderStyle renderStyle() { - return RenderStyle.HULL; + return RenderStyle.THIN_OUTLINE; } @ConfigItem( diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java index 6c41b2f24d..3f8b225d59 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcSceneOverlay.java @@ -42,6 +42,7 @@ 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.graphics.ModelOutlineRenderer; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; @@ -50,6 +51,8 @@ import net.runelite.client.util.Text; public class NpcSceneOverlay extends Overlay { + private static final Color TRANSPARENT = new Color(0, 0, 0, 0); + // 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; @@ -67,13 +70,15 @@ public class NpcSceneOverlay extends Overlay private final Client client; private final NpcIndicatorsConfig config; private final NpcIndicatorsPlugin plugin; + private final ModelOutlineRenderer modelOutliner; @Inject - NpcSceneOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin) + NpcSceneOverlay(Client client, NpcIndicatorsConfig config, NpcIndicatorsPlugin plugin, ModelOutlineRenderer modelOutliner) { this.client = client; this.config = config; this.plugin = plugin; + this.modelOutliner = modelOutliner; setPosition(OverlayPosition.DYNAMIC); setLayer(OverlayLayer.ABOVE_SCENE); } @@ -173,6 +178,22 @@ public class NpcSceneOverlay extends Overlay renderPoly(graphics, color, objectClickbox); break; + + case THIN_OUTLINE: + modelOutliner.drawOutline(actor, 1, color); + break; + + case OUTLINE: + modelOutliner.drawOutline(actor, 2, color); + break; + + case THIN_GLOW: + modelOutliner.drawOutline(actor, 4, color, TRANSPARENT); + break; + + case GLOW: + modelOutliner.drawOutline(actor, 8, color, TRANSPARENT); + break; } if (config.drawNames()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/RenderStyle.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/RenderStyle.java index d00029abe0..3dc2204da0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/RenderStyle.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/RenderStyle.java @@ -29,7 +29,11 @@ public enum RenderStyle OFF("Off"), TILE("Tile"), HULL("Hull"), - SOUTH_WEST_TILE("South West Tile"); + SOUTH_WEST_TILE("South West Tile"), + THIN_OUTLINE("Thin outline"), + OUTLINE("Outline"), + THIN_GLOW("Thin glow"), + GLOW("Glow"); private final String name; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/KnapsackSolver.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/KnapsackSolver.java new file mode 100644 index 0000000000..e00bef4443 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/KnapsackSolver.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2018, Davis Cook + * 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.slayer; + +import java.util.ArrayList; +import java.util.List; + +public class KnapsackSolver +{ + + private List reconstructItemsInSack(int [] [] sackMatrix, List items, int i, int w) + { + if (i == 0) + { + return new ArrayList<>(); + } + if (sackMatrix[i][w] > sackMatrix[i - 1][w]) + { + List list = reconstructItemsInSack(sackMatrix, items, + i - 1, w - items.get(i - 1)); + list.add(items.get(i - 1)); + return list; + } + else + { + return reconstructItemsInSack(sackMatrix, items, i - 1, w); + } + } + + public int howMuchFitsInSack(List items, int maxWeight) + { + int itemCount = items.size(); + + int[] [] sackMatrix = new int[itemCount + 1] [maxWeight + 1]; + for (int i = 1; i <= itemCount; i++) + { + for (int j = 0; j <= maxWeight; j++) + { + if (items.get(i - 1) > j) + { + sackMatrix[i] [j] = sackMatrix[i - 1] [j]; + } + else + { + sackMatrix[i] [j] = Math.max( + sackMatrix[i - 1] [j], + sackMatrix[i - 1] [j - items.get(i - 1)] + items.get(i - 1) + ); + } + } + } + + return reconstructItemsInSack(sackMatrix, items, itemCount, maxWeight).size(); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/NPCPresence.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/NPCPresence.java new file mode 100644 index 0000000000..0924660d6c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/NPCPresence.java @@ -0,0 +1,47 @@ +package net.runelite.client.plugins.slayer; + +import lombok.Getter; +import net.runelite.api.NPC; + +public class NPCPresence +{ + + private static final int FADE_TIMER_START = 20; + + @Getter + private String name; + + @Getter + private int combatLevel; + + private int fadeTimer; + + private NPCPresence(String name, int combatLevel) + { + this.name = name; + this.combatLevel = combatLevel; + this.fadeTimer = FADE_TIMER_START; + } + + @Override + public String toString() + { + return name + "[" + combatLevel + "]"; + } + + public boolean shouldExist() + { + return fadeTimer > 0; + } + + public void tickExistence() + { + fadeTimer--; + } + + public static NPCPresence buildPresence(NPC npc) + { + return new NPCPresence(npc.getName(), npc.getCombatLevel()); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/RenderStyle.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/RenderStyle.java new file mode 100644 index 0000000000..409f4c2729 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/RenderStyle.java @@ -0,0 +1,26 @@ +package net.runelite.client.plugins.slayer; + +public enum RenderStyle +{ + TILE("Tile"), + HULL("Hull"), + SOUTH_WEST_TILE("South West Tile"), + THIN_OUTLINE("Thin outline"), + OUTLINE("Outline"), + THIN_GLOW("Thin glow"), + GLOW("Glow"), + TRUE_LOCATIONS("True Location"); + + private final String name; + + RenderStyle(String name) + { + this.name = name; + } + + @Override + public String toString() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java index 4bf8864336..07e68ee198 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerConfig.java @@ -90,7 +90,18 @@ public interface SlayerConfig extends Config } @ConfigItem( - position = 6, + position = 7, + keyName = "highlightStyle", + name = "Highlight Style", + description = "Highlight setting" + ) + default RenderStyle renderStyle() + { + return RenderStyle.THIN_OUTLINE; + } + + @ConfigItem( + position = 7, keyName = "targetColor", name = "Target Color", description = "Color of the highlighted targets" @@ -101,7 +112,7 @@ public interface SlayerConfig extends Config } @ConfigItem( - position = 7, + position = 8, keyName = "superiorColor", name = "Superior Color", description = "Color of the highlighted superior slayer creatures" @@ -112,7 +123,29 @@ public interface SlayerConfig extends Config } @ConfigItem( - position = 8, + position = 9, + keyName = "drawNames", + name = "Draw names above NPC", + description = "Configures whether or not NPC names should be drawn above the NPC" + ) + default boolean drawNames() + { + return false; + } + + @ConfigItem( + position = 10, + keyName = "drawMinimapNames", + name = "Draw names on minimap", + description = "Configures whether or not NPC names should be drawn on the minimap" + ) + default boolean drawMinimapNames() + { + return false; + } + + @ConfigItem( + position = 12, keyName = "weaknessPrompt", name = "Show Monster Weakness", description = "Show an overlay on a monster when it is weak enough to finish off (Only Lizards, Gargoyles & Rockslugs)" @@ -123,7 +156,7 @@ public interface SlayerConfig extends Config } @ConfigItem( - position = 8, + position = 13, keyName = "taskCommand", name = "Task Command", description = "Configures whether the slayer task command is enabled
!task" @@ -180,6 +213,7 @@ public interface SlayerConfig extends Config { return -1; } + @ConfigItem( keyName = "initialAmount", name = "", @@ -205,6 +239,24 @@ public interface SlayerConfig extends Config ) void taskLocation(String key); + @ConfigItem( + keyName = "lastCertainAmount", + name = "", + description = "", + hidden = true + ) + default int lastCertainAmount() + { + return -1; + } + + @ConfigItem( + keyName = "lastCertainAmount", + name = "", + description = "" + ) + void lastCertainAmount(int lastCertainAmount); + @ConfigItem( keyName = "streak", name = "", @@ -222,58 +274,4 @@ public interface SlayerConfig extends Config description = "" ) void streak(int streak); - - @ConfigItem( - keyName = "points", - name = "", - description = "", - hidden = true - ) - default int points() - { - return -1; - } - - @ConfigItem( - keyName = "points", - name = "", - description = "" - ) - void points(int points); - - @ConfigItem( - keyName = "expeditious", - name = "", - description = "", - hidden = true - ) - default int expeditious() - { - return -1; - } - - @ConfigItem( - keyName = "expeditious", - name = "", - description = "" - ) - void expeditious(int expeditious); - - @ConfigItem( - keyName = "slaughter", - name = "", - description = "", - hidden = true - ) - default int slaughter() - { - return -1; - } - - @ConfigItem( - keyName = "slaughter", - name = "", - description = "" - ) - void slaughter(int slaughter); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerOverlay.java index 97a191d608..a084858458 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerOverlay.java @@ -104,32 +104,23 @@ class SlayerOverlay extends WidgetItemOverlay return; } - int amount = plugin.getAmount(); - if (amount <= 0) + if (plugin.getCurrentTask() == null) { return; } - int slaughterCount = plugin.getSlaughterChargeCount(); - int expeditiousCount = plugin.getExpeditiousChargeCount(); + int amount = plugin.getCurrentTask().getAmount(); + if (amount <= 0) + { + return; + } graphics.setFont(FontManager.getRunescapeSmallFont()); final Rectangle bounds = itemWidget.getCanvasBounds(); final TextComponent textComponent = new TextComponent(); - switch (itemId) - { - case ItemID.EXPEDITIOUS_BRACELET: - textComponent.setText(String.valueOf(expeditiousCount)); - break; - case ItemID.BRACELET_OF_SLAUGHTER: - textComponent.setText(String.valueOf(slaughterCount)); - break; - default: - textComponent.setText(String.valueOf(amount)); - break; - } + textComponent.setText(String.valueOf(amount)); // Draw the counter in the bottom left for equipment, and top left for jewelry textComponent.setPosition(new Point(bounds.x, bounds.y + (SLAYER_JEWELRY.contains(itemId) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java index 0bf59363d5..1ae0dc2fa4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java @@ -26,40 +26,49 @@ package net.runelite.client.plugins.slayer; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import com.google.inject.Provides; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.inject.Inject; import joptsimple.internal.Strings; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; -import net.runelite.api.GameState; import net.runelite.api.ItemID; import net.runelite.api.MessageNode; import net.runelite.api.NPC; import net.runelite.api.NPCComposition; import static net.runelite.api.Skill.SLAYER; +import net.runelite.api.SpriteID; +import net.runelite.api.Varbits; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.ExperienceChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.InteractingChanged; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.VarbitChanged; import net.runelite.api.vars.SlayerUnlock; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; @@ -70,14 +79,23 @@ import net.runelite.client.chat.ChatCommandManager; import net.runelite.client.chat.ChatMessageBuilder; import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ChatInput; +import net.runelite.client.game.AsyncBufferedImage; import net.runelite.client.game.ItemManager; +import net.runelite.client.game.SpriteManager; import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDependency; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.xptracker.XpTrackerPlugin; +import net.runelite.client.task.Schedule; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.ImageUtil; import net.runelite.client.util.Text; import net.runelite.http.api.chat.ChatClient; @@ -86,6 +104,7 @@ import net.runelite.http.api.chat.ChatClient; description = "Show additional slayer task related information", tags = {"combat", "notifications", "overlay", "tasks"} ) +@PluginDependency(XpTrackerPlugin.class) @Slf4j public class SlayerPlugin extends Plugin { @@ -96,14 +115,6 @@ public class SlayerPlugin extends Plugin private static final String CHAT_CANCEL_MESSAGE = "Your task has been cancelled."; private static final String CHAT_CANCEL_MESSAGE_JAD = "You no longer have a slayer task as you left the fight cave."; private static final String CHAT_SUPERIOR_MESSAGE = "A superior foe has appeared..."; - private static final String CHAT_BRACELET_SLAUGHTER = "Your bracelet of slaughter prevents your slayer"; - private static final Pattern CHAT_BRACELET_SLAUGHTER_REGEX = Pattern.compile("Your bracelet of slaughter prevents your slayer count decreasing. It has (\\d{1,2}) charge[s]? left."); - private static final String CHAT_BRACELET_EXPEDITIOUS = "Your expeditious bracelet helps you progress your"; - private static final Pattern CHAT_BRACELET_EXPEDITIOUS_REGEX = Pattern.compile("Your expeditious bracelet helps you progress your slayer (?:task )?faster. It has (\\d{1,2}) charge[s]? left."); - private static final String CHAT_BRACELET_SLAUGHTER_CHARGE = "Your bracelet of slaughter has "; - private static final Pattern CHAT_BRACELET_SLAUGHTER_CHARGE_REGEX = Pattern.compile("Your bracelet of slaughter has (\\d{1,2}) charge[s]? left."); - private static final String CHAT_BRACELET_EXPEDITIOUS_CHARGE = "Your expeditious bracelet has "; - private static final Pattern CHAT_BRACELET_EXPEDITIOUS_CHARGE_REGEX = Pattern.compile("Your expeditious bracelet has (\\d{1,2}) charge[s]? left."); private static final Pattern COMBAT_BRACELET_TASK_UPDATE_MESSAGE = Pattern.compile("^You still need to kill (\\d+) monsters to complete your current Slayer assignment"); //NPC messages @@ -112,13 +123,10 @@ public class SlayerPlugin extends Plugin private static final Pattern NPC_ASSIGN_FIRST_MESSAGE = Pattern.compile("^We'll start you off hunting (.*), you'll need to kill (\\d*) of them."); private static final Pattern NPC_CURRENT_MESSAGE = Pattern.compile("^You're still (?:hunting|bringing balance to) (?.+)(?: (?:in|on) (?:the )?(?.+), with|; you have) (?\\d+) to go\\..*"); - //Reward UI - private static final Pattern REWARD_POINTS = Pattern.compile("Reward points: ((?:\\d+,)*\\d+)"); - private static final int GROTESQUE_GUARDIANS_REGION = 6727; - private static final int EXPEDITIOUS_CHARGE = 30; - private static final int SLAUGHTER_CHARGE = 30; + private static final Set weaknessTasks = ImmutableSet.of(Task.DESERT_LIZARDS, Task.GARGOYLES, + Task.GROTESQUE_GUARDIANS, Task.GROTESQUE_GUARDIANS, Task.MUTATED_ZYGOMITES, Task.ROCKSLUGS); // Chat Command private static final String TASK_COMMAND_STRING = "!task"; @@ -152,6 +160,12 @@ public class SlayerPlugin extends Plugin "night beast", "nuclear smoke devil"); + @Inject + private ClientToolbar clientToolbar; + + @Inject + private SpriteManager spriteManager; + @Inject private Client client; @@ -197,32 +211,15 @@ public class SlayerPlugin extends Plugin @Inject private ChatClient chatClient; + @Inject + private EventBus eventBus; + @Getter(AccessLevel.PACKAGE) private List highlightedTargets = new ArrayList<>(); @Getter(AccessLevel.PACKAGE) @Setter(AccessLevel.PACKAGE) - private int amount; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private int initialAmount; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private String taskLocation; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private int expeditiousChargeCount; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private int slaughterChargeCount; - - @Getter(AccessLevel.PACKAGE) - @Setter(AccessLevel.PACKAGE) - private String taskName; + private TaskData currentTask = new TaskData(0, 0, 0, 0, 0, 0, null, null, true); @Getter(AccessLevel.PACKAGE) private int streak; @@ -230,11 +227,29 @@ public class SlayerPlugin extends Plugin @Getter(AccessLevel.PACKAGE) private int points; + @Getter(AccessLevel.PACKAGE) + private Task weaknessTask = null; + private TaskCounter counter; private int cachedXp; + private int cachedPoints; private Instant infoTimer; - private boolean loginFlag; private List targetNames = new ArrayList<>(); + private List targetIds = new ArrayList<>(); + private boolean checkAsTokens = true; + + private List lingeringPresences = new ArrayList<>(); + private SlayerXpDropLookup slayerXpDropLookup = null; + + private SlayerTaskPanel panel; + private NavigationButton navButton; + private long lastTickMillis = 0; + + private void clearTrackedNPCs() + { + highlightedTargets.clear(); + lingeringPresences.clear(); + } @Override protected void startUp() throws Exception @@ -244,15 +259,32 @@ public class SlayerPlugin extends Plugin overlayManager.add(targetWeaknessOverlay); overlayManager.add(targetMinimapOverlay); - if (client.getGameState() == GameState.LOGGED_IN - && config.amount() != -1 + if (slayerXpDropLookup == null) + { + // create this in startup since it needs to pull files during creation + slayerXpDropLookup = new SlayerXpDropLookup(); + } + + panel = new SlayerTaskPanel(this); + + spriteManager.getSpriteAsync(SpriteID.SKILL_SLAYER, 0, panel::loadHeaderIcon); + + final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "panel_icon.png"); + + navButton = NavigationButton.builder() + .tooltip("Slayer Tracker") + .icon(icon) + .priority(6) + .panel(panel) + .build(); + + clientToolbar.addNavigation(navButton); + + if (config.amount() != -1 && !config.taskName().isEmpty()) { - points = config.points(); streak = config.streak(); - setExpeditiousChargeCount(config.expeditious()); - setSlaughterChargeCount(config.slaughter()); - clientThread.invoke(() -> setTask(config.taskName(), config.amount(), config.initialAmount(), config.taskLocation())); + clientThread.invoke(() -> setTask(config.taskName(), config.amount(), config.initialAmount(), true, config.taskLocation(), config.lastCertainAmount())); } chatCommandManager.registerCommandAsync(TASK_COMMAND_STRING, this::taskLookup, this::taskSubmit); @@ -266,9 +298,10 @@ public class SlayerPlugin extends Plugin overlayManager.remove(targetWeaknessOverlay); overlayManager.remove(targetMinimapOverlay); removeCounter(); - highlightedTargets.clear(); + clearTrackedNPCs(); chatCommandManager.unregisterCommand(TASK_COMMAND_STRING); + clientToolbar.removeNavigation(navButton); } @Provides @@ -284,47 +317,33 @@ public class SlayerPlugin extends Plugin { case HOPPING: case LOGGING_IN: - cachedXp = 0; - taskName = ""; - amount = 0; - loginFlag = true; - highlightedTargets.clear(); - break; - case LOGGED_IN: - if (config.amount() != -1 - && !config.taskName().isEmpty() - && loginFlag) - { - points = config.points(); - streak = config.streak(); - setExpeditiousChargeCount(config.expeditious()); - setSlaughterChargeCount(config.slaughter()); - setTask(config.taskName(), config.amount(), config.initialAmount(), config.taskLocation()); - loginFlag = false; - } + cachedPoints = 0; + clearTrackedNPCs(); break; + case LOGIN_SCREEN: + currentTask.setPaused(true); } } private void save() { - config.amount(amount); - config.initialAmount(initialAmount); - config.taskName(taskName); - config.taskLocation(taskLocation); - config.points(points); + config.amount(currentTask.getAmount()); + config.initialAmount(currentTask.getInitialAmount()); + config.taskName(currentTask.getTaskName()); + config.taskLocation(currentTask.getTaskLocation()); + config.lastCertainAmount(currentTask.getLastCertainAmount()); config.streak(streak); - config.expeditious(expeditiousChargeCount); - config.slaughter(slaughterChargeCount); } @Subscribe public void onNpcSpawned(NpcSpawned npcSpawned) { NPC npc = npcSpawned.getNpc(); - if (isTarget(npc)) + if (isTarget(npc, targetNames)) { highlightedTargets.add(npc); + NPCPresence newPresence = NPCPresence.buildPresence(npc); + log.debug("New presence of " + newPresence.toString()); } } @@ -332,14 +351,143 @@ public class SlayerPlugin extends Plugin public void onNpcDespawned(NpcDespawned npcDespawned) { NPC npc = npcDespawned.getNpc(); - highlightedTargets.remove(npc); + boolean contained = highlightedTargets.remove(npc); + if (contained) + { + NPCPresence lingeringPresence = NPCPresence.buildPresence(npc); + lingeringPresences.add(lingeringPresence); + log.debug("Presence of " + lingeringPresence.toString() + " now lingering"); + } } + @Subscribe + public void onVarbitChanged(VarbitChanged event) + { + if (client.getVar(Varbits.SLAYER_REWARD_POINTS) == cachedPoints) + { + return; + } + + setPoints(client.getVar(Varbits.SLAYER_REWARD_POINTS)); + + if (!config.showInfobox()) + { + return; + } + + addCounter(); + + if (counter != null) + { + counter.setCount(currentTask.getAmount()); + } + } + + int estimateKillCount(List potentialKills, int gains) + { + // failsafe to avoid calculating kill count if there were no slayer monsters around that could be killed on task + // this failsafe *WILL FAIL* if someone decides to lamp their slayer in the middle of a task next to on task creatures + // but will prevent any other kind of slayer xp from triggering the kill count going down + // the main problem this causes is a genie random event during a slayer task and the player pops the lamp + // this will think that some of the monsters around were slain - e.g. lvl 50 slayer pops lamp for 500 xp around a + // 70 xp per kill slayer monster and now the slayer plugin thinks that 7 more kill count were completed (at 99 slayer + // the 990 xp drop would really mess with the kc tracker in a noticeable way) + if (potentialKills.size() < 1) + { + return 0; + } + + StringBuilder debugString = new StringBuilder(); + for (NPCPresence presence : potentialKills) + { + debugString.append(presence.toString()); + debugString.append(", "); + } + log.debug("Estimating kc of xp drop " + gains + " for presences {" + + debugString.toString() + "}"); + + // first determine potential xp drops given by all npcs that died this tick by grabbing the slayer xp + // info from the map made out of the data in slayer_xp.json + List potentialXpDrops = new ArrayList<>(); + for (NPCPresence potentialDead : potentialKills) + { + double xp = slayerXpDropLookup.findXpForNpc(potentialDead); + if (xp > 0) + { + potentialXpDrops.add(xp); + } + } + + debugString = new StringBuilder(); + for (Double drop : potentialXpDrops) + { + debugString.append(drop); + debugString.append(", "); + } + log.debug("Determined xp drop " + gains + " can be made of {" + debugString.toString() + + "}"); + + // we can attempt to determine exactly how many npcs died to give this amount of xp + // by using a solver for the knapsack problem + + // add one to max gains allowed for knapsack optimization + // since xp is only sent to us as integers but is stored on servers + // (and therefore gained as) a double + int fudgedGains = gains + 1; + + // scale the problem up by a factor of 10 since knapsack problem is solved better with integers + // and xp drops can have a single number after the decimal point + int tenFudgedGains = fudgedGains * 10; + List potentialXpDropsAsInts = potentialXpDrops.stream() + .map(xpDrop -> (int) (xpDrop * 10)) + .collect(Collectors.toCollection(ArrayList::new)); + + KnapsackSolver solver = new KnapsackSolver(); + + int estimatedCount = solver.howMuchFitsInSack(potentialXpDropsAsInts, tenFudgedGains); + + if (estimatedCount > potentialXpDrops.size()) + { + estimatedCount = potentialXpDrops.size(); + } + if (estimatedCount < 1) + { + estimatedCount = 1; + } + return estimatedCount; + } + + // b/c dialog can stay up on screen for multiple ticks in a row we want to make sure we only set a task once + // for the dialog that appears so we need to basically do a rising edge detection that only allows for a dialog + // check to be performed if in the previous ticks there was a period of no dialog + // i.e. once a dialog has been matched dialog cannot be matched again until npc dialog goes away for a tick + // this will work because in order for a new slayer task to happen the player either has to go complete the assignment + // (and close npc dialog) or go into the rewards screen which also closes npc dialog + private boolean canMatchDialog = true; + + // rising edge detection isn't enough for some reason (don't know why) so in addition to a rising edge rather than + // instantly allowing for another assignment we'll do a 2 tick refractory period + private static final int FORCED_WAIT = 2; + private int forcedWait = -1; + @Subscribe public void onGameTick(GameTick tick) { + // update the lingering presence of npcs in the slayer xp consideration list + Iterator presenceIterator = lingeringPresences.iterator(); + while (presenceIterator.hasNext()) + { + NPCPresence presence = presenceIterator.next(); + presence.tickExistence(); + if (!presence.shouldExist()) + { + log.debug("Lingering presence of " + presence.toString() + " expired"); + presenceIterator.remove(); + } + } + Widget npcDialog = client.getWidget(WidgetInfo.DIALOG_NPC_TEXT); - if (npcDialog != null) + if (npcDialog != null && canMatchDialog) { String npcText = Text.sanitizeMultilineText(npcDialog.getText()); //remove color and linebreaks final Matcher mAssign = NPC_ASSIGN_MESSAGE.matcher(npcText); // amount, name, (location) @@ -352,17 +500,23 @@ public class SlayerPlugin extends Plugin String name = mAssign.group("name"); int amount = Integer.parseInt(mAssign.group("amount")); String location = mAssign.group("location"); - setTask(name, amount, amount, location); + setTask(name, amount, amount, true, location, 0); + canMatchDialog = false; + forcedWait = FORCED_WAIT; } else if (mAssignFirst.find()) { int amount = Integer.parseInt(mAssignFirst.group(2)); - setTask(mAssignFirst.group(1), amount, amount); + setTask(mAssignFirst.group(1), amount, amount, true, 0); + canMatchDialog = false; + forcedWait = FORCED_WAIT; } else if (mAssignBoss.find()) { int amount = Integer.parseInt(mAssignBoss.group(2)); - setTask(mAssignBoss.group(1), amount, amount); + setTask(mAssignBoss.group(1), amount, amount, true, 0); + canMatchDialog = false; + forcedWait = FORCED_WAIT; points = Integer.parseInt(mAssignBoss.group(3).replaceAll(",", "")); } else if (mCurrent.find()) @@ -370,46 +524,18 @@ public class SlayerPlugin extends Plugin String name = mCurrent.group("name"); int amount = Integer.parseInt(mCurrent.group("amount")); String location = mCurrent.group("location"); - setTask(name, amount, initialAmount, location); + setTask(name, amount, currentTask.getInitialAmount(), false, location, 0); + canMatchDialog = false; + forcedWait = FORCED_WAIT; } } - - Widget braceletBreakWidget = client.getWidget(WidgetInfo.DIALOG_SPRITE_TEXT); - if (braceletBreakWidget != null) + else if (npcDialog == null) { - String braceletText = Text.removeTags(braceletBreakWidget.getText()); //remove color and linebreaks - if (braceletText.contains("bracelet of slaughter")) + if (forcedWait <= 0) { - slaughterChargeCount = SLAUGHTER_CHARGE; - config.slaughter(slaughterChargeCount); - } - else if (braceletText.contains("expeditious bracelet")) - { - expeditiousChargeCount = EXPEDITIOUS_CHARGE; - config.expeditious(expeditiousChargeCount); - } - } - - Widget rewardsBarWidget = client.getWidget(WidgetInfo.SLAYER_REWARDS_TOPBAR); - if (rewardsBarWidget != null) - { - for (Widget w : rewardsBarWidget.getDynamicChildren()) - { - Matcher mPoints = REWARD_POINTS.matcher(w.getText()); - if (mPoints.find()) - { - final int prevPoints = points; - points = Integer.parseInt(mPoints.group(1).replaceAll(",", "")); - - if (prevPoints != points) - { - removeCounter(); - addCounter(); - } - - break; - } + canMatchDialog = true; } + forcedWait--; } if (infoTimer != null) @@ -434,48 +560,6 @@ public class SlayerPlugin extends Plugin String chatMsg = Text.removeTags(event.getMessage()); //remove color and linebreaks - if (chatMsg.startsWith(CHAT_BRACELET_SLAUGHTER)) - { - Matcher mSlaughter = CHAT_BRACELET_SLAUGHTER_REGEX.matcher(chatMsg); - - amount++; - slaughterChargeCount = mSlaughter.find() ? Integer.parseInt(mSlaughter.group(1)) : SLAUGHTER_CHARGE; - config.slaughter(slaughterChargeCount); - } - - if (chatMsg.startsWith(CHAT_BRACELET_EXPEDITIOUS)) - { - Matcher mExpeditious = CHAT_BRACELET_EXPEDITIOUS_REGEX.matcher(chatMsg); - - amount--; - expeditiousChargeCount = mExpeditious.find() ? Integer.parseInt(mExpeditious.group(1)) : EXPEDITIOUS_CHARGE; - config.expeditious(expeditiousChargeCount); - } - - if (chatMsg.startsWith(CHAT_BRACELET_EXPEDITIOUS_CHARGE)) - { - Matcher mExpeditious = CHAT_BRACELET_EXPEDITIOUS_CHARGE_REGEX.matcher(chatMsg); - - if (!mExpeditious.find()) - { - return; - } - - expeditiousChargeCount = Integer.parseInt(mExpeditious.group(1)); - config.expeditious(expeditiousChargeCount); - } - if (chatMsg.startsWith(CHAT_BRACELET_SLAUGHTER_CHARGE)) - { - Matcher mSlaughter = CHAT_BRACELET_SLAUGHTER_CHARGE_REGEX.matcher(chatMsg); - if (!mSlaughter.find()) - { - return; - } - - slaughterChargeCount = Integer.parseInt(mSlaughter.group(1)); - config.slaughter(slaughterChargeCount); - } - if (chatMsg.endsWith("; return to a Slayer master.")) { Matcher mComplete = CHAT_COMPLETE_MESSAGE.matcher(chatMsg); @@ -496,18 +580,22 @@ public class SlayerPlugin extends Plugin break; case 3: streak = Integer.parseInt(matches.get(0)); - points = Integer.parseInt(matches.get(2)); break; default: log.warn("Unreachable default case for message ending in '; return to Slayer master'"); } - setTask("", 0, 0); + + log.debug("Slayer task completed with " + currentTask.getAmount() + " remaining"); + log.debug("Last certain amount was " + currentTask.getLastCertainAmount() + + " so error rate is " + currentTask.getAmount() + " in " + currentTask.getLastCertainAmount()); + + setTask("", 0, 0, true, 0); return; } if (chatMsg.equals(CHAT_GEM_COMPLETE_MESSAGE) || chatMsg.equals(CHAT_CANCEL_MESSAGE) || chatMsg.equals(CHAT_CANCEL_MESSAGE_JAD)) { - setTask("", 0, 0); + setTask("", 0, 0, true, 0); return; } @@ -524,7 +612,7 @@ public class SlayerPlugin extends Plugin String name = mProgress.group("name"); int gemAmount = Integer.parseInt(mProgress.group("amount")); String location = mProgress.group("location"); - setTask(name, gemAmount, initialAmount, location); + setTask(name, gemAmount, currentTask.getInitialAmount(), false, location, gemAmount); return; } @@ -533,10 +621,10 @@ public class SlayerPlugin extends Plugin if (bracerProgress.find()) { final int taskAmount = Integer.parseInt(bracerProgress.group(1)); - setTask(taskName, taskAmount, initialAmount); + setTask(currentTask.getTaskName(), taskAmount, currentTask.getInitialAmount(), false, taskAmount); // Avoid race condition (combat brace message goes through first before XP drop) - amount++; + currentTask.setAmount(currentTask.getAmount() + 1); } } @@ -555,15 +643,70 @@ public class SlayerPlugin extends Plugin return; } - if (cachedXp == 0) + if (cachedXp != 0) + { + // this is not the initial xp sent on login so these are new xp gains + int gains = slayerExp - cachedXp; + + log.debug("Slayer xp drop received"); + + StringBuilder debugString = new StringBuilder(); + + // potential npcs to give xp drop are current highlighted npcs and the lingering presences + List potentialNPCs = new ArrayList<>(); + debugString.append("Lingering presences {"); + for (NPCPresence presence : lingeringPresences) + { + potentialNPCs.add(presence); + debugString.append(presence.toString()); + debugString.append(", "); + } + debugString.append("}\nCurrent presences {"); + for (NPC npc : highlightedTargets) + { + NPCPresence currentPresence = NPCPresence.buildPresence(npc); + potentialNPCs.add(currentPresence); + debugString.append(currentPresence.toString()); + debugString.append(", "); + } + debugString.append("}"); + log.debug(debugString.toString()); + + int killCount = estimateKillCount(potentialNPCs, gains); + for (int i = 0; i < killCount; i++) + { + killedOne(); + int delta = slayerExp - cachedXp; + currentTask.setElapsedXp(currentTask.getElapsedXp() + delta); + } + } + cachedXp = slayerExp; + } + + @Subscribe + public void onInteractingChanged(InteractingChanged event) + { + if (client.getLocalPlayer() == null) { + return; + } + final Actor interacting = client.getLocalPlayer().getInteracting(); + weaknessTask = null; + + if (interacting == null || !(interacting instanceof NPC)) { - // this is the initial xp sent on login - cachedXp = slayerExp; return; } - killedOne(); - cachedXp = slayerExp; + final NPC npc = (NPC) interacting; + + for (Task task : weaknessTasks) + { + if (isTarget(npc, buildTargetNames(task))) + { + weaknessTask = task; + return; + } + } } boolean isSuperior(String name) @@ -592,18 +735,22 @@ public class SlayerPlugin extends Plugin @VisibleForTesting void killedOne() { - if (amount == 0) + if (currentTask.getAmount() == 0) { return; } - amount--; + currentTask.setAmount(currentTask.getAmount() - 1); + currentTask.setElapsedKills(currentTask.getElapsedKills() + 1); if (doubleTroubleExtraKill()) { - amount--; + currentTask.setAmount(currentTask.getAmount() - 1); + currentTask.setElapsedKills(currentTask.getElapsedKills() + 1); } - config.amount(amount); // save changed value + config.amount(currentTask.getAmount()); // save changed value + currentTask.setPaused(false); // no longer paused since xp is gained + panel.updateCurrentTask(true, currentTask.isPaused(), currentTask, false); if (!config.showInfobox()) { @@ -612,7 +759,7 @@ public class SlayerPlugin extends Plugin // add and update counter, set timer addCounter(); - counter.setCount(amount); + counter.setCount(currentTask.getAmount()); infoTimer = Instant.now(); } @@ -622,9 +769,45 @@ public class SlayerPlugin extends Plugin SlayerUnlock.GROTESQUE_GARDIAN_DOUBLE_COUNT.isEnabled(client); } - private boolean isTarget(NPC npc) + // checks if any contiguous subsequence of seq0 exactly matches the String toMatch + boolean contiguousSubsequenceMatches(String[] seq0, String toMatch) { - if (targetNames.isEmpty()) + for (int i = 0; i < seq0.length; i++) + { + for (int j = i; j < seq0.length; j++) + { + String sub0 = ""; + for (int k = i; k <= j; k++) + { + sub0 += seq0[k] + " "; + } + sub0 = sub0.substring(0, sub0.length() - 1); // remove extra space + if (sub0.equals(toMatch)) + { + return true; + } + } + } + return false; + } + + private boolean isValidComposition(NPCComposition composition) + { + if (composition != null) + { + List actions = Arrays.asList(composition.getActions()); + if (actions.contains("Attack") || actions.contains("Pick") || actions.contains("Poke")) //Pick action is for zygomite-fungi + { + return true; + } + } + + return false; + } + + private boolean isTarget(NPC npc, List names) + { + if (names.isEmpty() && targetIds.isEmpty()) { return false; } @@ -637,36 +820,85 @@ public class SlayerPlugin extends Plugin name = name.toLowerCase(); - for (String target : targetNames) - { - if (name.contains(target)) - { - NPCComposition composition = npc.getTransformedComposition(); + // in order to avoid issues like pirates being highlighted on a rats task + // rather than checking if the name contains any of the targets we do a complete + // token check which is where we tokenize the name on the space character + // then we check if any contiguous subsequence (of at least length 1) from the name matches + // the target. - if (composition != null) + // we have a boolean flag that also allows the behavior of just doing a contains check to happen + // this is done specifically for the tzhaar task because it's much easier to just check if the enemy + // contains "Tz-" then listing the many many many types of tzhaar. + + for (String target : names) + { + if (!checkAsTokens) + { + if (name.contains(target) && isValidComposition(npc.getTransformedComposition())) { - List actions = Arrays.asList(composition.getActions()); - if (actions.contains("Attack") || actions.contains("Pick")) //Pick action is for zygomite-fungi - { - return true; - } + return true; + } + } + else + { + String[] nameTokens = name.split(" "); + if (contiguousSubsequenceMatches(nameTokens, target) && isValidComposition(npc.getTransformedComposition())) + { + return true; } } } + + int id = npc.getId(); + if (id <= 0) + { + return false; + } + + for (int target : targetIds) + { + if (id == target && isValidComposition(npc.getTransformedComposition())) + { + return true; + } + } + return false; } - private void rebuildTargetNames(Task task) + private List buildTargetNames(Task task) { - targetNames.clear(); + List names = new ArrayList<>(); if (task != null) { - Arrays.stream(task.getTargetNames()) + task.getTargetNames().stream() .map(String::toLowerCase) - .forEach(targetNames::add); + .forEach(names::add); - targetNames.add(taskName.toLowerCase().replaceAll("s$", "")); + //TODO + names.add(task.getName().toLowerCase().replaceAll("s$", "")); + } + + return names; + } + + private void rebuildTargetIds(Task task) + { + targetIds.clear(); + + if (task != null) + { + task.getNpcIds().stream() + .forEach(targetIds::add); + } + } + + private void rebuildCheckAsTokens(Task task) + { + if (task != null) + { + checkAsTokens = task.isCheckAsTokens(); } } @@ -676,54 +908,67 @@ public class SlayerPlugin extends Plugin for (NPC npc : client.getNpcs()) { - if (isTarget(npc)) + if (isTarget(npc, targetNames)) { highlightedTargets.add(npc); } } } - private void setTask(String name, int amt, int initAmt) + private void setTask(String name, int amt, int initAmt, boolean isNewAssignment, int lastCertainAmt) { - setTask(name, amt, initAmt, null); + setTask(name, amt, initAmt, isNewAssignment, null, lastCertainAmt); } - private void setTask(String name, int amt, int initAmt, String location) + private void setTask(String name, int amt, int initAmt, boolean isNewAssignment, String location, int lastCertainAmt) { - taskName = name; - amount = amt; - initialAmount = initAmt; - taskLocation = location; + currentTask = new TaskData(isNewAssignment ? 0 : currentTask.getElapsedTime(), + isNewAssignment ? 0 : currentTask.getElapsedKills(), + isNewAssignment ? 0 : currentTask.getElapsedXp(), + amt, initAmt, lastCertainAmt, location, name, + isNewAssignment ? true : currentTask.isPaused()); + if (panel != null) + { + panel.updateCurrentTask(true, currentTask.isPaused(), currentTask, isNewAssignment); + } + save(); removeCounter(); addCounter(); infoTimer = Instant.now(); Task task = Task.getTask(name); - rebuildTargetNames(task); + targetNames.clear(); + targetNames = buildTargetNames(task); + rebuildTargetIds(task); + rebuildCheckAsTokens(task); rebuildTargetList(); } - private void addCounter() + public AsyncBufferedImage getImageForTask(Task task) { - if (!config.showInfobox() || counter != null || Strings.isNullOrEmpty(taskName)) - { - return; - } - - Task task = Task.getTask(taskName); int itemSpriteId = ItemID.ENCHANTED_GEM; if (task != null) { itemSpriteId = task.getItemSpriteId(); } + return itemManager.getImage(itemSpriteId); + } - BufferedImage taskImg = itemManager.getImage(itemSpriteId); + private void addCounter() + { + if (!config.showInfobox() || counter != null || currentTask == null || Strings.isNullOrEmpty(currentTask.getTaskName())) + { + return; + } + + Task task = Task.getTask(currentTask.getTaskName()); + AsyncBufferedImage taskImg = getImageForTask(task); String taskTooltip = ColorUtil.wrapWithColorTag("%s", new Color(255, 119, 0)) + "
"; - if (taskLocation != null && !taskLocation.isEmpty()) + if (currentTask.getTaskLocation() != null && !currentTask.getTaskLocation().isEmpty()) { - taskTooltip += taskLocation + "
"; + taskTooltip += currentTask.getTaskLocation() + "
"; } taskTooltip += ColorUtil.wrapWithColorTag("Pts:", Color.YELLOW) @@ -731,15 +976,15 @@ public class SlayerPlugin extends Plugin + ColorUtil.wrapWithColorTag("Streak:", Color.YELLOW) + " %s"; - if (initialAmount > 0) + if (currentTask.getInitialAmount() > 0) { taskTooltip += "
" + ColorUtil.wrapWithColorTag("Start:", Color.YELLOW) - + " " + initialAmount; + + " " + currentTask.getInitialAmount() ; } - counter = new TaskCounter(taskImg, this, amount); - counter.setTooltip(String.format(taskTooltip, capsString(taskName), points, streak)); + counter = new TaskCounter(taskImg, this, currentTask.getAmount()); + counter.setTooltip(String.format(taskTooltip, capsString(currentTask.getTaskName()), points, streak)); infoBoxManager.addInfoBox(counter); } @@ -824,9 +1069,42 @@ public class SlayerPlugin extends Plugin client.refreshChat(); } + /* package access method for changing the pause state of the time tracker for the current task */ + void setPaused(boolean paused) + { + currentTask.setPaused(paused); + panel.updateCurrentTask(false, currentTask.isPaused(), currentTask, false); + } + + @Schedule( + period = 1, + unit = ChronoUnit.SECONDS + ) + public void tickTaskTimes() + { + if (lastTickMillis == 0) + { + lastTickMillis = System.currentTimeMillis(); + return; + } + + final long nowMillis = System.currentTimeMillis(); + final long tickDelta = nowMillis - lastTickMillis; + lastTickMillis = nowMillis; + + + if (currentTask == null) + { + return; + } + currentTask.tick(tickDelta); + + panel.updateCurrentTask(false, currentTask.isPaused(), currentTask, false); + } + private boolean taskSubmit(ChatInput chatInput, String value) { - if (Strings.isNullOrEmpty(taskName)) + if (Strings.isNullOrEmpty(currentTask.getTaskName())) { return false; } @@ -837,7 +1115,7 @@ public class SlayerPlugin extends Plugin { try { - chatClient.submitTask(playerName, capsString(taskName), amount, initialAmount, taskLocation); + chatClient.submitTask(playerName, capsString(currentTask.getTaskName()), currentTask.getAmount(), currentTask.getInitialAmount(), currentTask.getTaskLocation()); } catch (Exception ex) { @@ -853,8 +1131,14 @@ public class SlayerPlugin extends Plugin } //Utils - private String capsString(String str) + private static String capsString(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1); } + + void setPoints(int points) + { + this.points = points; + this.cachedPoints = points; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerTaskPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerTaskPanel.java new file mode 100644 index 0000000000..06684bab67 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerTaskPanel.java @@ -0,0 +1,368 @@ +package net.runelite.client.plugins.slayer; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.PluginErrorPanel; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.StackFormatter; + +public class SlayerTaskPanel extends PluginPanel +{ + private static final long MILLIS_PER_SECOND = 1000; + private static final long SECONDS_PER_MINUTE = 60; + private static final long MINUTES_PER_HOUR = 60; + + // Templates + private static final String HTML_LABEL_TEMPLATE = + "%s%s"; + private static final String HTML_TIME_LABEL_TEMPLATE = + "%s%02d:%02d:%02d"; + + + private static final ImageIcon PAUSE, PAUSE_FADED, PAUSE_HOVER; + private static final ImageIcon PLAY, PLAY_FADED, PLAY_HOVER; + + // TODO: set some kind of maximum for the amount of tasks to be tracked in a session + private static final int MAX_TASK_BOXES = 50; + + // When there are no tasks, display this + private final PluginErrorPanel errorPanel = new PluginErrorPanel(); + + // Handle task boxes + private final JPanel tasksContainer = new JPanel(); + + // Handle overall slayer session data + private final JPanel overallPanel = new JPanel(); + private final JLabel overallKillsLabel = new JLabel(); + private final JLabel overallTimeLabel = new JLabel(); + private final JLabel overallIcon = new JLabel(); + + // Actions + private final JPanel actionsContainer = new JPanel(); + private final JLabel playBtn = new JLabel(); + private final JLabel pauseBtn = new JLabel(); + + // Log tasks + private final List tasks = new ArrayList<>(); + + private SlayerPlugin slayerPlugin; + + static + { + final BufferedImage pauseImg = ImageUtil.getResourceStreamFromClass(SlayerPlugin.class, "pause_icon.png"); + final BufferedImage playImg = ImageUtil.getResourceStreamFromClass(SlayerPlugin.class, "play_icon.png"); + + PAUSE = new ImageIcon(pauseImg); + PAUSE_FADED = new ImageIcon(ImageUtil.alphaOffset(pauseImg, -180)); + PAUSE_HOVER = new ImageIcon(ImageUtil.alphaOffset(pauseImg, -220)); + + PLAY = new ImageIcon(playImg); + PLAY_FADED = new ImageIcon(ImageUtil.alphaOffset(playImg, -180)); + PLAY_HOVER = new ImageIcon(ImageUtil.alphaOffset(playImg, -220)); + } + + public SlayerTaskPanel(SlayerPlugin slayerPlugin) + { + this.slayerPlugin = slayerPlugin; + + setBorder(new EmptyBorder(6, 6, 6, 6)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + setLayout(new BorderLayout()); + + // Create layout panel for wrapping + final JPanel layoutPanel = new JPanel(); + layoutPanel.setLayout(new BoxLayout(layoutPanel, BoxLayout.Y_AXIS)); + add(layoutPanel, BorderLayout.NORTH); + + actionsContainer.setLayout(new BorderLayout()); + actionsContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + actionsContainer.setPreferredSize(new Dimension(0, 30)); + actionsContainer.setBorder(new EmptyBorder(5, 5, 5, 10)); + actionsContainer.setVisible(false); + + final JPanel controlsPanel = new JPanel(new GridLayout(1, 2, 10, 0)); + controlsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + playBtn.setIcon(PLAY); + playBtn.setToolTipText("Resume the current slayer task"); + playBtn.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + slayerPlugin.setPaused(false); + changePauseState(false); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + boolean paused = true; + TaskData currentTask = slayerPlugin.getCurrentTask(); + if (currentTask != null) + { + paused = currentTask.isPaused(); + } + playBtn.setIcon(paused ? PLAY_FADED : PLAY); + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + boolean paused = true; + TaskData currentTask = slayerPlugin.getCurrentTask(); + if (currentTask != null) + { + paused = currentTask.isPaused(); + } + playBtn.setIcon(paused ? PLAY_HOVER : PLAY); + } + }); + + pauseBtn.setIcon(PAUSE); + pauseBtn.setToolTipText("Pause the current slayer task"); + pauseBtn.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + slayerPlugin.setPaused(true); + changePauseState(true); + } + @Override + public void mouseExited(MouseEvent mouseEvent) + { + boolean paused = true; + TaskData currentTask = slayerPlugin.getCurrentTask(); + if (currentTask != null) + { + paused = currentTask.isPaused(); + } + pauseBtn.setIcon(paused ? PAUSE : PAUSE_FADED); + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + boolean paused = true; + TaskData currentTask = slayerPlugin.getCurrentTask(); + if (currentTask != null) + { + paused = currentTask.isPaused(); + } + pauseBtn.setIcon(paused ? PAUSE : PAUSE_HOVER); + } + }); + + controlsPanel.add(playBtn); + controlsPanel.add(pauseBtn); + + actionsContainer.add(controlsPanel, BorderLayout.EAST); + changePauseState(true); + if (slayerPlugin.getCurrentTask() != null) + { + changePauseState(slayerPlugin.getCurrentTask().isPaused()); + } + + // Create panel that will contain overall data + overallPanel.setBorder(new EmptyBorder(8, 10, 8, 10)); + overallPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + overallPanel.setLayout(new BorderLayout()); + overallPanel.setVisible(false); + + // Add contents + final JPanel overallInfo = new JPanel(); + overallInfo.setBackground(ColorScheme.DARKER_GRAY_COLOR); + overallInfo.setLayout(new GridLayout(2, 1)); + overallInfo.setBorder(new EmptyBorder(2, 10, 2, 0)); + overallKillsLabel.setFont(FontManager.getRunescapeSmallFont()); + overallTimeLabel.setFont(FontManager.getRunescapeSmallFont()); + overallInfo.add(overallKillsLabel); + overallInfo.add(overallTimeLabel); + overallPanel.add(overallIcon, BorderLayout.WEST); + overallPanel.add(overallInfo, BorderLayout.CENTER); + + tasksContainer.setLayout(new BoxLayout(tasksContainer, BoxLayout.Y_AXIS)); + layoutPanel.add(actionsContainer); + layoutPanel.add(Box.createRigidArea(new Dimension(0, 5))); + layoutPanel.add(overallPanel); + layoutPanel.add(tasksContainer); + + // Add error pane + errorPanel.setContent("Task trackers", "You have not received any slayer tasks yet."); + add(errorPanel); + } + + void loadHeaderIcon(BufferedImage img) + { + overallIcon.setIcon(new ImageIcon(img)); + } + + private void changePauseState(boolean paused) + { + playBtn.setIcon(paused ? PLAY_FADED : PLAY); + pauseBtn.setIcon(paused ? PAUSE : PAUSE_FADED); + } + + private void updateOverall() + { + int overallKills = 0; + long overallTime = 0; + for (TaskBox box : tasks) + { + overallKills += box.getTaskData().getElapsedKills(); + overallTime += box.getTaskData().getElapsedTime(); + } + + overallKillsLabel.setText(htmlLabel("Total kills: ", overallKills)); + overallTimeLabel.setText(htmlLabel("Total time: ", overallTime)); + } + + private static boolean isEmptyTask(TaskData taskData) + { + return (taskData.getTaskName() == null || taskData.getTaskName().equals("")) && taskData.getAmount() == 0 && taskData.getInitialAmount() == 0; + } + + private void showMainView() + { + remove(errorPanel); + actionsContainer.setVisible(true); + overallPanel.setVisible(true); + } + + private TaskBox buildBox(SlayerPlugin plugin, JPanel container, TaskData data) + { + TaskBox newBox = new TaskBox(plugin, container, data.toBuilder().build()); + tasks.add(0, newBox); + showMainView(); + return newBox; + } + + private boolean stringsEqualIncludeNull(String str0, String str1) + { + if (str0 == null && str1 == null) + { + return true; // both are null + } + else if (str0 == null || str1 == null) + { + return false; // only 1 is null + } + else + { + // none are null so equals check is safe + return str0.equals(str1); + } + } + + void updateCurrentTask(boolean updated, boolean paused, TaskData newData, boolean isNewAssignment) + { + // important case for if the current task is completed so the update will show the empty task + if (isEmptyTask(newData)) + { + if (tasks.isEmpty()) // if there is no current task an empty task doesn't do anything + { + return; + } + TaskBox current = tasks.get(0); + // current task has ended even though it should still have 1 amount remaining b/c the ending chat message + // pops before the slayer xp drop so we need to force the remaining kc to zero and add the last kc to + // the elapsed kc + if (current.getTaskData().getAmount() != 0) // must check not equal zero b/c otherwise this would constantly tick + { + int amountDelta = current.getTaskData().getAmount(); + current.getTaskData().setAmount(0); + current.getTaskData().setElapsedKills(current.getTaskData().getElapsedKills() + amountDelta); + // current task has ended so it should be paused + current.update(true, true, current.getTaskData()); + updateOverall(); + } + return; + } + + if (tasks.isEmpty() || isNewAssignment) + { + // new task so append it to the front of the list + SwingUtilities.invokeLater(() -> { + TaskBox newBox = buildBox(slayerPlugin, tasksContainer, newData); + newBox.update(true, newData.isPaused(), newData); + }); + return; + } + else + { + // if here there is a current task so check if the current task matches + // the update being sent + TaskBox current = tasks.get(0); + if (!stringsEqualIncludeNull(current.getTaskData().getTaskName(), newData.getTaskName()) || + !stringsEqualIncludeNull(current.getTaskData().getTaskLocation(), newData.getTaskLocation()) || + current.getTaskData().getInitialAmount() != newData.getInitialAmount()) + { + // current task does not match the update being sent so the current task + // must have been outdated - this is necessarily true because if a true + // new task was sent it would have set the isNewAssignment flag + + // so this previous task is invalid so delete it then add in the new actually + // correct task + SwingUtilities.invokeLater(() -> { + tasksContainer.remove(tasks.get(0)); + tasks.remove(0); + TaskBox newBox = buildBox(slayerPlugin, tasksContainer, newData); + newBox.update(true, newData.isPaused(), newData); + }); + return; + } + } + + // not an empty assignment or a new assignment so just update the current assignment + TaskBox current = tasks.get(0); + current.update(updated, paused, newData); + + // update the overall stats once this task stats are updated + updateOverall(); + changePauseState(paused); + } + + static String htmlLabel(String key, long timeMillis) + { + if (timeMillis == Long.MAX_VALUE) + { + String valueStr = "N/A"; + return String.format(HTML_LABEL_TEMPLATE, ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), + key, valueStr); + } + else + { + long seconds = timeMillis / MILLIS_PER_SECOND; + long minutes = seconds / SECONDS_PER_MINUTE; + seconds %= 60; + long hours = minutes / MINUTES_PER_HOUR; + minutes %= 60; + return String.format(HTML_TIME_LABEL_TEMPLATE, ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), + key, (int) hours, (int) minutes, (int) seconds); + } + } + + static String htmlLabel(String key, int value) + { + String valueStr = StackFormatter.quantityToRSDecimalStack(value); + return String.format(HTML_LABEL_TEMPLATE, ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), + key, valueStr); + } + +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerXpDropLookup.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerXpDropLookup.java new file mode 100644 index 0000000000..8973f92bef --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerXpDropLookup.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2018, Davis Cook + * 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.slayer; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; + +public class SlayerXpDropLookup +{ + private Map> xpMap; + + // floating point math equality + private static final double EPSILON = 1e-6; + + void loadXpJson() + { + final InputStream xpFile = getClass().getResourceAsStream("/slayer_xp.json"); + Gson gson = new Gson(); + xpMap = gson.fromJson(new InputStreamReader(xpFile), new TypeToken>>() + { + + }.getType()); + } + + /** + * Finds the xp for a given npc using the xp + combat level data provided + * from the JSON - since scrapping from the wiki isn't perfectly accurate + * we make some estimations + * + * precondition is that xpCombatLevel array is non-null - if it is null + * we can simply return -1 to indicate no slayer xp because this npc + * has no associated xpCombatLevel array + * + * 1. first check to see if anywhere in the xp + combat level data this + * creature name give slayer xp - if it doesn't just return -1 and + * be done with this - if it does give slayer xp then continue + * 2. now check to see if we can find the xp for this combat level where + * that xp is greater than 0. note that we don't just find the xp for + * this combat level - this is because for some monsters the wiki + * only has slayer xp data for some combat levels and has it unknown + * for the other combat levels. this way we only return the combat level + * related xp data for a monster if it is know + * 3. finally if the slayer xp data for the monster was unknown for the given + * level we estimate the slayer xp by using one of the slayer xps for a level + * that does have xp given + * 4. note that if a monster gives no slayer xp for any level it will return + * -1 so we don't accidentally misscount non-slayer targets dying as giving + * slayer xp + * + * @param npc the npc we are estimating slayer xp for + * @return our best guess for the slayer xp for this npc + */ + public double findXpForNpc(NPCPresence npc) + { + List xpCombatLevel = xpMap.get(npc.getName()); + if (xpCombatLevel == null) + { + return -1; + } + boolean givesSlayerXp = false; + for (int i = 0; i < xpCombatLevel.size() - 1; i += 2) + { + if (xpCombatLevel.get(i) > 0) + { + givesSlayerXp = true; + } + } + if (!givesSlayerXp) + { + return -1; + } + boolean foundCombatLevel = false; + for (int i = 0; i < xpCombatLevel.size() - 1; i += 2) + { + if (Math.abs(xpCombatLevel.get(i + 1) - npc.getCombatLevel()) < EPSILON + && xpCombatLevel.get(i) > 0) + { + foundCombatLevel = true; + } + } + if (foundCombatLevel) + { + for (int i = 0; i < xpCombatLevel.size() - 1; i += 2) + { + if (Math.abs(xpCombatLevel.get(i + 1) - npc.getCombatLevel()) < EPSILON) + { + return xpCombatLevel.get(i); + } + } + } + else + { + for (int i = 0; i < xpCombatLevel.size() - 1; i += 2) + { + if (xpCombatLevel.get(i) > 0) + { + return xpCombatLevel.get(i); + } + } + } + return -1; + } + + public SlayerXpDropLookup() + { + loadXpJson(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetClickboxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetClickboxOverlay.java index 598798ac97..004c33c52f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetClickboxOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetClickboxOverlay.java @@ -33,21 +33,37 @@ import java.awt.Graphics2D; import java.awt.Polygon; import java.util.List; 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.graphics.ModelOutlineRenderer; 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; +import net.runelite.client.util.Text; public class TargetClickboxOverlay extends Overlay { + private static final Color TRANSPARENT = new Color(0, 0, 0, 0); + + private final Client client; private final SlayerConfig config; private final SlayerPlugin plugin; + private final ModelOutlineRenderer modelOutliner; @Inject - TargetClickboxOverlay(SlayerConfig config, SlayerPlugin plugin) + TargetClickboxOverlay(Client client, SlayerConfig config, SlayerPlugin plugin, + ModelOutlineRenderer modelOutlineRenderer) { + this.client = client; this.config = config; this.plugin = plugin; + this.modelOutliner = modelOutlineRenderer; setPosition(OverlayPosition.DYNAMIC); setLayer(OverlayLayer.ABOVE_SCENE); } @@ -55,36 +71,104 @@ public class TargetClickboxOverlay extends Overlay @Override public Dimension render(Graphics2D graphics) { - if (!config.highlightTargets()) + if (config.highlightTargets()) { - return null; - } - - List targets = plugin.getHighlightedTargets(); - for (NPC target : targets) - { - Color coloration = config.getTargetColor(); - if (plugin.isSuperior(target.getName())) + List targets = plugin.getHighlightedTargets(); + for (NPC target : targets) { - coloration = config.getSuperiorColor(); - } + Color coloration = config.getTargetColor(); + if (plugin.isSuperior(target.getName())) + { + coloration = config.getSuperiorColor(); + } - renderTargetOverlay(graphics, target, coloration); + renderNpcOverlay(graphics, target, coloration); + } } return null; } - private void renderTargetOverlay(Graphics2D graphics, NPC actor, Color color) + private void renderNpcOverlay(Graphics2D graphics, NPC actor, Color color) { - Polygon objectClickbox = actor.getConvexHull(); - if (objectClickbox != null) + switch (config.renderStyle()) + { + case SOUTH_WEST_TILE: + LocalPoint lp1 = LocalPoint.fromWorld(client, actor.getWorldLocation()); + Polygon tilePoly1 = Perspective.getCanvasTilePoly(client, lp1); + + renderPoly(graphics, color, tilePoly1); + break; + + case TILE: + int size = 1; + NPCComposition composition = actor.getTransformedComposition(); + if (composition != null) + { + size = composition.getSize(); + } + LocalPoint lp = actor.getLocalLocation(); + Polygon tilePoly = Perspective.getCanvasTileAreaPoly(client, lp, size); + + renderPoly(graphics, color, tilePoly); + break; + + case HULL: + Polygon objectClickbox = actor.getConvexHull(); + + renderPoly(graphics, color, objectClickbox); + break; + case THIN_OUTLINE: + modelOutliner.drawOutline(actor, 1, color); + break; + + case OUTLINE: + modelOutliner.drawOutline(actor, 2, color); + break; + + case THIN_GLOW: + modelOutliner.drawOutline(actor, 4, color, TRANSPARENT); + break; + + case GLOW: + modelOutliner.drawOutline(actor, 8, color, TRANSPARENT); + break; + case TRUE_LOCATIONS: + size = 1; + composition = actor.getTransformedComposition(); + if (composition != null) + { + size = composition.getSize(); + } + WorldPoint wp = actor.getWorldLocation(); + lp = LocalPoint.fromWorld(client, wp); + tilePoly = Perspective.getCanvasTileAreaPoly(client, lp, size); + + renderPoly(graphics, color, tilePoly); + break; + } + + if (config.drawNames()) + { + String npcName = Text.removeTags(actor.getName()); + Point textLocation = actor.getCanvasTextLocation(graphics, npcName, actor.getLogicalHeight() + 40); + + if (textLocation != null) + { + OverlayUtil.renderTextLocation(graphics, textLocation, npcName, color); + } + } + } + + private static void renderPoly(Graphics2D graphics, Color color, Polygon polygon) + { + if (polygon != null) { graphics.setColor(color); graphics.setStroke(new BasicStroke(2)); - graphics.draw(objectClickbox); + graphics.draw(polygon); graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20)); - graphics.fill(objectClickbox); + graphics.fill(polygon); } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetMinimapOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetMinimapOverlay.java index ea12fde38a..8100ff70c7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetMinimapOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetMinimapOverlay.java @@ -31,6 +31,7 @@ import java.awt.Dimension; import java.awt.Graphics2D; import java.util.List; import javax.inject.Inject; +import net.runelite.api.Client; import net.runelite.api.NPC; import net.runelite.api.Point; import net.runelite.client.ui.overlay.Overlay; @@ -40,12 +41,15 @@ import net.runelite.client.ui.overlay.OverlayUtil; public class TargetMinimapOverlay extends Overlay { + + private final Client client; private final SlayerConfig config; private final SlayerPlugin plugin; @Inject - TargetMinimapOverlay(SlayerConfig config, SlayerPlugin plugin) + TargetMinimapOverlay(Client client, SlayerConfig config, SlayerPlugin plugin) { + this.client = client; this.config = config; this.plugin = plugin; setPosition(OverlayPosition.DYNAMIC); @@ -63,18 +67,29 @@ public class TargetMinimapOverlay extends Overlay List targets = plugin.getHighlightedTargets(); for (NPC target : targets) { - renderTargetOverlay(graphics, target, config.getTargetColor()); + Color coloration = config.getTargetColor(); + if (plugin.isSuperior(target.getName())) + { + coloration = config.getSuperiorColor(); + } + + renderTargetOverlay(graphics, target, target.getName(), coloration); } return null; } - private void renderTargetOverlay(Graphics2D graphics, NPC actor, Color color) + private void renderTargetOverlay(Graphics2D graphics, NPC actor, String name, Color color) { Point minimapLocation = actor.getMinimapLocation(); if (minimapLocation != null) { OverlayUtil.renderMinimapLocation(graphics, minimapLocation, color); + + if (config.drawMinimapNames()) + { + OverlayUtil.renderTextLocation(graphics, minimapLocation, name, color); + } } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetWeaknessOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetWeaknessOverlay.java index ada3f3a727..84fa4d8773 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetWeaknessOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TargetWeaknessOverlay.java @@ -27,7 +27,6 @@ package net.runelite.client.plugins.slayer; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.image.BufferedImage; -import java.util.List; import javax.inject.Inject; import net.runelite.api.Client; import net.runelite.api.NPC; @@ -65,35 +64,26 @@ class TargetWeaknessOverlay extends Overlay @Override public Dimension render(Graphics2D graphics) { - final List targets = plugin.getHighlightedTargets(); - - if (targets.isEmpty() || !config.weaknessPrompt()) + if (!config.weaknessPrompt()) { return null; } - final Task curTask = Task.getTask(plugin.getTaskName()); - if (curTask == null || curTask.getWeaknessThreshold() < 0 || curTask.getWeaknessItem() < 0) + final Task npcTask = plugin.getWeaknessTask(); + + if (npcTask == null) { return null; } - final int threshold = curTask.getWeaknessThreshold(); - final BufferedImage image = itemManager.getImage(curTask.getWeaknessItem()); + final NPC npc = (NPC) client.getLocalPlayer().getInteracting(); + final int threshold = npcTask.getWeaknessThreshold(); + final BufferedImage image = itemManager.getImage(npcTask.getWeaknessItem()); + final int currentHealth = calculateHealth(npc); - if (image == null) + if (currentHealth >= 0 && currentHealth <= threshold) { - return null; - } - - for (NPC target : targets) - { - final int currentHealth = calculateHealth(target); - - if (currentHealth >= 0 && currentHealth <= threshold) - { - renderTargetItem(graphics, target, image); - } + renderTargetItem(graphics, npc, image); } return null; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java index 69725f6d1b..1c6d5029d0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java @@ -26,131 +26,191 @@ package net.runelite.client.plugins.slayer; import com.google.common.base.Preconditions; +import java.util.ArrayList; +import static java.util.Arrays.asList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.Getter; import net.runelite.api.ItemID; +import net.runelite.api.NpcID; @Getter enum Task { + + /* + * format for enum is that the name of the task is first + * second is the item id image we use to represent the task graphically + * third is the list of names of monsters that can be killed on task (note that he task name is already handled + * so that is why for a task like ankou there is no need to include this list) + * fourth is the list of ids of monsters that can be killed on task (main reason for this is weird cases like baby dragons and the elf mourner) + */ + // - ABERRANT_SPECTRES("Aberrant spectres", ItemID.ABERRANT_SPECTRE, "Spectre"), - ABYSSAL_DEMONS("Abyssal demons", ItemID.ABYSSAL_DEMON), + ABERRANT_SPECTRES("Aberrant spectres", ItemID.ABERRANT_SPECTRE, + asList("Abhorrent spectre", "Deviant spectre", "Repugnant spectre"), Collections.emptyList()), + ABYSSAL_DEMONS("Abyssal demons", ItemID.ABYSSAL_DEMON, + asList("Ayssal Sire"), Collections.emptyList()), ABYSSAL_SIRE("Abyssal Sire", ItemID.ABYSSAL_ORPHAN), ADAMANT_DRAGONS("Adamant dragons", ItemID.ADAMANT_DRAGON_MASK), ALCHEMICAL_HYDRA("Alchemical Hydra", ItemID.IKKLE_HYDRA), ANKOU("Ankou", ItemID.ANKOU_MASK), - AVIANSIES("Aviansies", ItemID.ENSOULED_AVIANSIE_HEAD), + AVIANSIES("Aviansies", ItemID.ENSOULED_AVIANSIE_HEAD, + asList("Kree'arra"), Collections.emptyList()), BANSHEES("Banshees", ItemID.BANSHEE), - BARROWS_BROTHERS("Barrows Brothers", ItemID.KARILS_COIF), + BARROWS_BROTHERS("Barrows Brothers", ItemID.KARILS_COIF, + asList("Ahrim the blighted", "Dharok the wretched", "Guthan the infested", "Karil the tainted", "Torag the corrupted", "Verac the defiled"), Collections.emptyList()), BASILISKS("Basilisks", ItemID.BASILISK), BATS("Bats", ItemID.GIRAL_BAT_2), - BEARS("Bears", ItemID.ENSOULED_BEAR_HEAD), - ENTS("Ents", ItemID.NICE_TREE, "Ent"), - LAVA_DRAGONS("Lava Dragons", ItemID.LAVA_SCALE, "Lava dragon"), - BIRDS("Birds", ItemID.FEATHER, "Chicken", "Rooster", "Terrorbird", "Seagull"), - BLACK_DEMONS("Black demons", ItemID.BLACK_DEMON_MASK), - BLACK_DRAGONS("Black dragons", ItemID.BLACK_DRAGON_MASK), + BEARS("Bears", ItemID.ENSOULED_BEAR_HEAD, + asList("Callisto"), Collections.emptyList()), + ENTS("Ents", ItemID.ENTS_ROOTS), + LAVA_DRAGONS("Lava Dragons", ItemID.LAVA_SCALE), + BANDITS("Bandits", ItemID.BANDIT), + BIRDS("Birds", ItemID.FEATHER, + asList("Chicken", "Rooster", "Terrorbird", "Seagull", "Chompy bird", "Jubbly bird", "Oomlie bird", "Vulture"), Collections.emptyList()), + BLACK_DEMONS("Black demons", ItemID.BLACK_DEMON_MASK, + asList("Demonic gorilla", "Balfrug kreeyath", "Skotizo"), Collections.emptyList()), + BLACK_DRAGONS("Black dragons", ItemID.BLACK_DRAGON_MASK, + Collections.emptyList(), asList(NpcID.BABY_DRAGON_1871, NpcID.BABY_DRAGON_1872, NpcID.BABY_DRAGON_7955)), BLOODVELD("Bloodveld", ItemID.BLOODVELD), - BLUE_DRAGONS("Blue dragons", ItemID.BLUE_DRAGON_MASK), + BLUE_DRAGONS("Blue dragons", ItemID.BLUE_DRAGON_MASK, + asList("Vorkath"), asList(NpcID.BABY_DRAGON, NpcID.BABY_DRAGON_242, NpcID.BABY_DRAGON_243)), BRINE_RATS("Brine rats", ItemID.BRINE_RAT), BRONZE_DRAGONS("Bronze dragons", ItemID.BRONZE_DRAGON_MASK), CALLISTO("Callisto", ItemID.CALLISTO_CUB), CATABLEPON("Catablepon", ItemID.LEFT_SKULL_HALF), CAVE_BUGS("Cave bugs", ItemID.SWAMP_CAVE_BUG), - CAVE_CRAWLERS("Cave crawlers", ItemID.CAVE_CRAWLER, "Chasm crawler"), - CAVE_HORRORS("Cave horrors", ItemID.CAVE_HORROR, "Cave abomination"), - CAVE_KRAKEN("Cave kraken", ItemID.CAVE_KRAKEN), + CAVE_CRAWLERS("Cave crawlers", ItemID.CAVE_CRAWLER, + asList("Chasm crawler"), Collections.emptyList()), + CAVE_HORRORS("Cave horrors", ItemID.CAVE_HORROR, + asList("Cave abomination"), Collections.emptyList()), + CAVE_KRAKEN("Cave kraken", ItemID.CAVE_KRAKEN, + asList("Kraken"), Collections.emptyList()), CAVE_SLIMES("Cave slimes", ItemID.SWAMP_CAVE_SLIME), CERBERUS("Cerberus", ItemID.HELLPUPPY), + CHAOS_DRUIDS("Chaos druids", ItemID.ELDER_CHAOS_HOOD), CHAOS_ELEMENTAL("Chaos Elemental", ItemID.PET_CHAOS_ELEMENTAL), CHAOS_FANATIC("Chaos Fanatic", ItemID.ANCIENT_STAFF), - COCKATRICE("Cockatrice", ItemID.COCKATRICE, "Cockathrice"), + COCKATRICE("Cockatrice", ItemID.COCKATRICE, + asList("Cockathrice"), Collections.emptyList()), COWS("Cows", ItemID.COW_MASK), - CRAWLING_HANDS("Crawling hands", ItemID.CRAWLING_HAND, "Crushing hand"), - CRAZY_ARCHAEOLOGIST("Crazy Archaeologists", ItemID.FEDORA, "Crazy Archaeologist"), + CRAWLING_HANDS("Crawling hands", ItemID.CRAWLING_HAND, + asList("Crushing hand"), Collections.emptyList()), + CRAZY_ARCHAEOLOGIST("Crazy Archaeologist", ItemID.FEDORA), CROCODILES("Crocodiles", ItemID.SWAMP_LIZARD), - DAGANNOTH("Dagannoth", ItemID.DAGANNOTH), - DAGANNOTH_KINGS("Dagannoth Kings", ItemID.PET_DAGANNOTH_PRIME), - DARK_BEASTS("Dark beasts", ItemID.DARK_BEAST, "Night beast"), - DARK_WARRIORS("Dark warriors", ItemID.BLACK_MED_HELM, "Dark warrior"), + DAGANNOTH("Dagannoth", ItemID.DAGANNOTH, + asList("Dagannoth Rex", "Dagannoth Prime", "Dagannoth Supreme"), Collections.emptyList()), + DAGANNOTH_KINGS("Dagannoth Kings", ItemID.PET_DAGANNOTH_PRIME, + asList("Dagannoth Rex", "Dagannoth Prime", "Dagannoth Supreme"), Collections.emptyList()), + DARK_BEASTS("Dark beasts", ItemID.DARK_BEAST, + asList("Night beast"), Collections.emptyList()), + DARK_WARRIORS("Dark warriors", ItemID.BLACK_MED_HELM), DERANGED_ARCHAEOLOGIST("Deranged Archaeologist", ItemID.ARCHAEOLOGISTS_DIARY), - DESERT_LIZARDS("Desert lizards", ItemID.DESERT_LIZARD, 4, ItemID.ICE_COOLER, "Small lizard", "Lizard"), - DOGS("Dogs", ItemID.GUARD_DOG, "Jackal"), + DESERT_LIZARDS("Desert lizards", ItemID.DESERT_LIZARD, + asList("Small lizard", "Lizard"), Collections.emptyList(), 4, ItemID.ICE_COOLER), + DOGS("Dogs", ItemID.GUARD_DOG, asList("Jackal"), Collections.emptyList()), DRAKES("Drakes", ItemID.DRAKE), - DUST_DEVILS("Dust devils", ItemID.DUST_DEVIL, "Choke devil"), - DWARVES("Dwarves", ItemID.DWARVEN_HELMET, "Dwarf"), + DUST_DEVILS("Dust devils", ItemID.DUST_DEVIL, + asList("Choke devil"), Collections.emptyList()), + DWARVES("Dwarves", ItemID.DWARVEN_HELMET, + asList("Dwarf", "Black guard"), Collections.emptyList()), EARTH_WARRIORS("Earth warriors", ItemID.BRONZE_FULL_HELM_T), - ELVES("Elves", ItemID.ELF, "Elf"), + ELVES("Elves", ItemID.ELF, + asList("Elf"), asList(NpcID.MOURNER_5311)), FEVER_SPIDERS("Fever spiders", ItemID.FEVER_SPIDER), FIRE_GIANTS("Fire giants", ItemID.FIRE_BATTLESTAFF), - REVENANTS("Revenants", ItemID.BRACELET_OF_ETHEREUM, "Revenant imp", "Revenant goblin", "Revenant pyrefiend", "Revenant hobgoblin", "Revenant cyclops", "Revenant hellhound", "Revenant demon", "Revenant ork", "Revenant dark beast", "Revenant knight", "Revenant dragon"), - FLESH_CRAWLERS("Fleshcrawlers", ItemID.ENSOULED_SCORPION_HEAD, "Flesh crawler"), - FOSSIL_ISLAND_WYVERNS("Fossil island wyverns", ItemID.FOSSIL_ISLAND_WYVERN, "Ancient wyvern", "Long-tailed wyvern", "Spitting wyvern", "Taloned wyvern"), - GARGOYLES("Gargoyles", ItemID.GARGOYLE, 9, ItemID.ROCK_HAMMER), + REVENANTS("Revenants", ItemID.REVENANT_ETHER, + asList("Revenant imp", "Revenant goblin", "Revenant pyrefiend", "Revenant hobgoblin", "Revenant cyclops", "Revenant hellhound", "Revenant demon", "Revenant ork", "Revenant dark beast", "Revenant knight", "Revenant dragon"), Collections.emptyList()), + FLESH_CRAWLERS("Flesh crawlers", ItemID.ENSOULED_SCORPION_HEAD), + FOSSIL_ISLAND_WYVERNS("Fossil island wyverns", ItemID.FOSSIL_ISLAND_WYVERN, + asList("Ancient wyvern", "Long-tailed wyvern", "Spitting wyvern", "Taloned wyvern"), Collections.emptyList()), + GARGOYLES("Gargoyles", ItemID.GARGOYLE, + asList("Dusk", "Dawn"), Collections.emptyList(), 9, ItemID.ROCK_HAMMER), GENERAL_GRAARDOR("General Graardor", ItemID.PET_GENERAL_GRAARDOR), - GHOSTS("Ghosts", ItemID.GHOSTSPEAK_AMULET, "Tortured soul"), + GHOSTS("Ghosts", ItemID.GHOSTSPEAK_AMULET, + asList("Tortured soul"), Collections.emptyList()), GIANT_MOLE("Giant Mole", ItemID.BABY_MOLE), GHOULS("Ghouls", ItemID.ZOMBIE_HEAD), GOBLINS("Goblins", ItemID.ENSOULED_GOBLIN_HEAD), - GREATER_DEMONS("Greater demons", ItemID.GREATER_DEMON_MASK), - GREEN_DRAGONS("Green dragons", ItemID.GREEN_DRAGON_MASK), - GROTESQUE_GUARDIANS("Grotesque Guardians", ItemID.MIDNIGHT, 0, ItemID.ROCK_HAMMER, "Dusk", "Dawn"), + GREATER_DEMONS("Greater demons", ItemID.GREATER_DEMON_MASK, + asList("K'ril Tsutsaroth", "Tstanon Karlak", "Skotizo"), Collections.emptyList()), + GREEN_DRAGONS("Green dragons", ItemID.GREEN_DRAGON_MASK, + Collections.emptyList(), asList(NpcID.BABY_DRAGON_5194, NpcID.BABY_DRAGON_5872, NpcID.BABY_DRAGON_5873)), + GROTESQUE_GUARDIANS("Grotesque Guardians", ItemID.MIDNIGHT, + asList("Dusk", "Dawn"), Collections.emptyList(), 0, ItemID.ROCK_HAMMER), HARPIE_BUG_SWARMS("Harpie bug swarms", ItemID.SWARM), - HELLHOUNDS("Hellhounds", ItemID.HELLHOUND), - HILL_GIANTS("Hill giants", ItemID.ENSOULED_GIANT_HEAD), + HELLHOUNDS("Hellhounds", ItemID.HELLHOUND, + asList("Cerberus"), Collections.emptyList()), + HILL_GIANTS("Hill giants", ItemID.ENSOULED_GIANT_HEAD, + asList("Cyclops"), Collections.emptyList()), HOBGOBLINS("Hobgoblins", ItemID.HOBGOBLIN_GUARD), HYDRAS("Hydras", ItemID.HYDRA), + ICEFIENDS("Icefiends", ItemID.ICE_DIAMOND), ICE_GIANTS("Ice giants", ItemID.ICE_DIAMOND), ICE_WARRIORS("Ice warriors", ItemID.MITHRIL_FULL_HELM_T), - ICEFIENDS("Icefiends", ItemID.ICE_DIAMOND), - INFERNAL_MAGES("Infernal mages", ItemID.INFERNAL_MAGE, "Malevolent mage"), + INFERNAL_MAGES("Infernal mages", ItemID.INFERNAL_MAGE, + asList("Malevolent mage"), Collections.emptyList()), IRON_DRAGONS("Iron dragons", ItemID.IRON_DRAGON_MASK), JAD("TzTok-Jad", ItemID.TZREKJAD), - JELLIES("Jellies", ItemID.JELLY, "Jelly"), + JELLIES("Jellies", ItemID.JELLY, + asList("Jelly"), Collections.emptyList()), JUNGLE_HORROR("Jungle horrors", ItemID.ENSOULED_HORROR_HEAD), KALPHITE("Kalphite", ItemID.KALPHITE_SOLDIER), - MAMMOTHS("Mammoths", ItemID.ATTACKER_HORN, "Mammoth"), KALPHITE_QUEEN("Kalphite Queen", ItemID.KALPHITE_PRINCESS), KILLERWATTS("Killerwatts", ItemID.KILLERWATT), KING_BLACK_DRAGON("King Black Dragon", ItemID.PRINCE_BLACK_DRAGON), - KRAKEN("Cave Kraken Boss", ItemID.PET_KRAKEN, "Kraken"), + KRAKEN("Cave Kraken Boss", ItemID.PET_KRAKEN, + asList("Kraken"), Collections.emptyList()), KREEARRA("Kree'arra", ItemID.PET_KREEARRA), KRIL_TSUTSAROTH("K'ril Tsutsaroth", ItemID.PET_KRIL_TSUTSAROTH), KURASK("Kurask", ItemID.KURASK), - ROGUES("Rogues", ItemID.ROGUE_MASK, "Rogue"), + ROGUES("Rogues", ItemID.ROGUE_MASK, asList("Rogue"), Collections.emptyList()), LESSER_DEMONS("Lesser demons", ItemID.LESSER_DEMON_MASK), - LIZARDMEN("Lizardmen", ItemID.LIZARDMAN_FANG, "Lizardman"), - MINIONS_OF_SCABARAS("Minions of scabaras", ItemID.GOLDEN_SCARAB, "Scarab swarm", "Locust rider", "Scarab mage"), + LIZARDMEN("Lizardmen", ItemID.LIZARDMAN_FANG, + asList("Lizardman"), Collections.emptyList()), + MAGIC_AXES("Magic axes", ItemID.IRON_BATTLEAXE), + MAMMOTHS("Mammoths", ItemID.ATTACKER_HORN, + asList("Mammoth"), Collections.emptyList()), + MINIONS_OF_SCABARAS("Minions of scabaras", ItemID.GOLDEN_SCARAB, + asList("Scarab swarm", "Locust rider", "Scarab mage"), Collections.emptyList()), MINOTAURS("Minotaurs", ItemID.ENSOULED_MINOTAUR_HEAD), MITHRIL_DRAGONS("Mithril dragons", ItemID.MITHRIL_DRAGON_MASK), MOGRES("Mogres", ItemID.MOGRE), MOLANISKS("Molanisks", ItemID.MOLANISK), MONKEYS("Monkeys", ItemID.ENSOULED_MONKEY_HEAD), - MOSS_GIANTS("Moss giants", ItemID.HILL_GIANT_CLUB), - MUTATED_ZYGOMITES("Mutated zygomites", ItemID.MUTATED_ZYGOMITE, 7, ItemID.FUNGICIDE_SPRAY_0, "Zygomite", "Fungi"), - NECHRYAEL("Nechryael", ItemID.NECHRYAEL, "Nechryarch"), - OGRES("Ogres", ItemID.ENSOULED_OGRE_HEAD), + MOSS_GIANTS("Moss giants", ItemID.HILL_GIANT_CLUB, + asList("Bryophyta"), Collections.emptyList()), + MUTATED_ZYGOMITES("Mutated zygomites", ItemID.MUTATED_ZYGOMITE, + asList("Zygomite"), Collections.emptyList(), 7, ItemID.FUNGICIDE_SPRAY_0), + NECHRYAEL("Nechryael", ItemID.NECHRYAEL, + asList("Nechryarch"), Collections.emptyList()), + OGRES("Ogres", ItemID.ENSOULED_OGRE_HEAD, + asList("Enclave guard"), Collections.emptyList()), OTHERWORLDLY_BEING("Otherworldly beings", ItemID.GHOSTLY_HOOD), - PYREFIENDS("Pyrefiends", ItemID.PYREFIEND, "Flaming pyrelord"), + PYREFIENDS("Pyrefiends", ItemID.PYREFIEND, + asList("Flaming pyrelord"), Collections.emptyList()), RATS("Rats", ItemID.RATS_TAIL), - RED_DRAGONS("Red dragons", ItemID.BABY_RED_DRAGON), + RED_DRAGONS("Red dragons", ItemID.BABY_RED_DRAGON, + Collections.emptyList(), asList(NpcID.BABY_DRAGON_244, NpcID.BABY_DRAGON_245, NpcID.BABY_DRAGON_246)), ROCKSLUGS("Rockslugs", ItemID.ROCKSLUG, 4, ItemID.BAG_OF_SALT), RUNE_DRAGONS("Rune dragons", ItemID.RUNE_DRAGON_MASK), SCORPIA("Scorpia", ItemID.SCORPIAS_OFFSPRING), - CHAOS_DRUIDS("Chaos druids", ItemID.ELDER_CHAOS_HOOD, "Elder Chaos druid", "Chaos druid"), - BANDITS("Bandits", ItemID.BANDIT, "Bandit"), - MAGIC_AXES("Magic axes", ItemID.IRON_BATTLEAXE, "Magic axe"), - SCORPIONS("Scorpions", ItemID.ENSOULED_SCORPION_HEAD), + SCORPIONS("Scorpions", ItemID.ENSOULED_SCORPION_HEAD, + asList("Scorpia"), Collections.emptyList()), SEA_SNAKES("Sea snakes", ItemID.SNAKE_CORPSE), - SHADES("Shades", ItemID.SHADE_ROBE_TOP, "Loar Shadow", "Loar Shade", "Phrin Shadow", "Phrin Shade", "Riyl Shadow", "Riyl Shade", "Asyn Shadow", "Asyn Shade", "Fiyr Shadow", "Fiyr Shade"), + SHADES("Shades", ItemID.SHADE_ROBE_TOP, + asList("Loar Shadow", "Loar Shade", "Phrin Shadow", "Phrin Shade", "Riyl Shadow", "Riyl Shade", "Asyn Shadow", "Asyn Shade", "Fiyr Shadow", "Fiyr Shade"), Collections.emptyList()), SHADOW_WARRIORS("Shadow warriors", ItemID.BLACK_FULL_HELM), SKELETAL_WYVERNS("Skeletal wyverns", ItemID.SKELETAL_WYVERN), SKELETONS("Skeletons", ItemID.SKELETON_GUARD), SMOKE_DEVILS("Smoke devils", ItemID.SMOKE_DEVIL), - SPIDERS("Spiders", ItemID.HUGE_SPIDER), - SPIRITUAL_CREATURES("Spiritual creatures", ItemID.DRAGON_BOOTS, "Spiritual ranger", "Spiritual mage", "Spiritual warrior"), + SPIDERS("Spiders", ItemID.HUGE_SPIDER, + asList("Venenatis"), Collections.emptyList()), + SPIRITUAL_CREATURES("Spiritual creatures", ItemID.DRAGON_BOOTS, + asList("Spiritual ranger", "Spiritual mage", "Spiritual warrior"), Collections.emptyList()), STEEL_DRAGONS("Steel dragons", ItemID.STEEL_DRAGON), SULPHUR_LIZARDS("Sulphur Lizards", ItemID.SULPHUR_LIZARD), SUQAHS("Suqahs", ItemID.SUQAH_TOOTH), @@ -158,18 +218,23 @@ enum Task THERMONUCLEAR_SMOKE_DEVIL("Thermonuclear Smoke Devil", ItemID.PET_SMOKE_DEVIL), TROLLS("Trolls", ItemID.TROLL_GUARD), TUROTH("Turoth", ItemID.TUROTH), - TZHAAR("Tzhaar", ItemID.ENSOULED_TZHAAR_HEAD), - VAMPIRES("Vampires", ItemID.STAKE), + TZHAAR("Tzhaar", ItemID.ENSOULED_TZHAAR_HEAD, + asList("Tz-"), Collections.emptyList(), false), + VAMPIRES("Vampires", ItemID.STAKE, + asList("Vampyre"), Collections.emptyList()), VENENATIS("Venenatis", ItemID.VENENATIS_SPIDERLING), VETION("Vet'ion", ItemID.VETION_JR), VORKATH("Vorkath", ItemID.VORKI), WALL_BEASTS("Wall beasts", ItemID.SWAMP_WALLBEAST), WATERFIENDS("Waterfiends", ItemID.WATER_ORB), - WEREWOLVES("Werewolves", ItemID.WOLFBANE, "Werewolf"), - WOLVES("Wolves", ItemID.GREY_WOLF_FUR, "Wolf"), + WEREWOLVES("Werewolves", ItemID.WOLFBANE, + asList("Werewolf"), Collections.emptyList()), + WOLVES("Wolves", ItemID.GREY_WOLF_FUR, + asList("Wolf"), Collections.emptyList()), WYRMS("Wyrms", ItemID.WYRM), - ZILYANA("Commander Zilyana", ItemID.PET_ZILYANA), - ZOMBIES("Zombies", ItemID.ZOMBIE_HEAD, "Undead"), + ZILYANA("Zilyana", ItemID.PET_ZILYANA), + ZOMBIES("Zombies", ItemID.ZOMBIE_HEAD, + asList("Undead"), Collections.emptyList()), ZULRAH("Zulrah", ItemID.PET_SNAKELING), ZUK("TzKal-Zuk", ItemID.TZREKZUK); // @@ -178,7 +243,10 @@ enum Task private final String name; private final int itemSpriteId; - private final String[] targetNames; + + private final List targetNames; + private final List npcIds; + private final boolean checkAsTokens; private final int weaknessThreshold; private final int weaknessItem; @@ -190,7 +258,43 @@ enum Task } } - Task(String name, int itemSpriteId, String... targetNames) + Task(String name, int itemSpriteId) + { + Preconditions.checkArgument(itemSpriteId >= 0); + this.name = name; + this.itemSpriteId = itemSpriteId; + this.weaknessThreshold = -1; + this.weaknessItem = -1; + this.targetNames = new ArrayList<>(); + this.npcIds = new ArrayList<>(); + this.checkAsTokens = true; + } + + Task(String name, int itemSpriteId, int weaknessThreshold, int weaknessItem) + { + Preconditions.checkArgument(itemSpriteId >= 0); + this.name = name; + this.itemSpriteId = itemSpriteId; + this.weaknessThreshold = weaknessThreshold; + this.weaknessItem = weaknessItem; + this.targetNames = new ArrayList<>(); + this.npcIds = new ArrayList<>(); + this.checkAsTokens = true; + } + + Task(String name, int itemSpriteId, boolean checkAsTokens) + { + Preconditions.checkArgument(itemSpriteId >= 0); + this.name = name; + this.itemSpriteId = itemSpriteId; + this.weaknessThreshold = -1; + this.weaknessItem = -1; + this.targetNames = new ArrayList<>(); + this.npcIds = new ArrayList<>(); + this.checkAsTokens = checkAsTokens; + } + + Task(String name, int itemSpriteId, List targetNames, List npcIds) { Preconditions.checkArgument(itemSpriteId >= 0); this.name = name; @@ -198,9 +302,11 @@ enum Task this.weaknessThreshold = -1; this.weaknessItem = -1; this.targetNames = targetNames; + this.npcIds = npcIds; + this.checkAsTokens = true; } - Task(String name, int itemSpriteId, int weaknessThreshold, int weaknessItem, String... targetNames) + Task(String name, int itemSpriteId, List targetNames, List npcIds, int weaknessThreshold, int weaknessItem) { Preconditions.checkArgument(itemSpriteId >= 0); this.name = name; @@ -208,6 +314,20 @@ enum Task this.weaknessThreshold = weaknessThreshold; this.weaknessItem = weaknessItem; this.targetNames = targetNames; + this.npcIds = npcIds; + this.checkAsTokens = true; + } + + Task(String name, int itemSpriteId, List targetNames, List npcIds, boolean checkAsTokens) + { + Preconditions.checkArgument(itemSpriteId >= 0); + this.name = name; + this.itemSpriteId = itemSpriteId; + this.weaknessThreshold = -1; + this.weaknessItem = -1; + this.targetNames = targetNames; + this.npcIds = npcIds; + this.checkAsTokens = checkAsTokens; } static Task getTask(String taskName) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskBox.java new file mode 100644 index 0000000000..c50d359de7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskBox.java @@ -0,0 +1,304 @@ +package net.runelite.client.plugins.slayer; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import java.text.DecimalFormat; +import java.util.Collections; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.ProgressBar; +import net.runelite.client.util.StackFormatter; + +class TaskBox extends JPanel +{ + private static final long MILLIS_PER_SECOND = 1000; + private static final long SECONDS_PER_MINUTE = 60; + private static final long MINUTES_PER_HOUR = 60; + private static final long MILLIS_PER_HOUR = MILLIS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + private static final DecimalFormat TWO_DECIMAL_FORMAT = new DecimalFormat("0.00"); + + + // Templates + private static final String HTML_TOOL_TIP_TEMPLATE = + "%.1f Kills/hr
" + + "%02d:%02d:%02d per kill"; + private static final String HTML_LABEL_TEMPLATE = + "%s"; + private static final String HTML_TIME_LABEL_TEMPLATE = + "%02d:%02d:%02d"; + + // Instance members + private final JPanel panel; + + @Getter(AccessLevel.PACKAGE) + private final TaskData taskData; + + /* Contains the task icon and the stats panel */ + private final JPanel headerPanel = new JPanel(); + + /* Contains the overall stats of the slayer task */ + private final JPanel statsPanel = new JPanel(); + + private final ProgressBar progressBar = new ProgressBar(); + + private final JLabel elapsed = new JLabel("Elapsed:"); + private final JLabel remaining = new JLabel("Remaining:"); + private final JLabel duration = new JLabel("Time:"); + private final JLabel currentDuration = new JLabel(); + private final JLabel remainingDuration = new JLabel(); + private final JLabel kills = new JLabel("Kills:"); + private final JLabel currentKills = new JLabel(); + private final JLabel remainingKills = new JLabel(); + private final JLabel xp = new JLabel("XP:"); + private final JLabel currentXp = new JLabel(); + private final JLabel remainingXp = new JLabel(); + + private boolean paused = false; + + TaskBox(SlayerPlugin slayerPlugin, JPanel panel, TaskData taskData) + { + this.panel = panel; + this.taskData = taskData; + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(5, 0, 0, 0)); + + /* This task's wrapping container */ + JPanel container = new JPanel(); + container.setLayout(new BorderLayout()); + container.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + SwingUtilities.invokeLater(() -> { + BufferedImage taskImg = slayerPlugin.getImageForTask(Task.getTask(taskData.getTaskName())); + JLabel taskIcon = new JLabel(new ImageIcon(taskImg)); + taskIcon.setHorizontalAlignment(SwingConstants.CENTER); + taskIcon.setVerticalAlignment(SwingConstants.CENTER); + taskIcon.setPreferredSize(new Dimension(35, 35)); + + statsPanel.setLayout(new DynamicGridLayout(4, 3)); + statsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + statsPanel.setBorder(new EmptyBorder(9, 2, 9, 2)); + + elapsed.setFont(FontManager.getRunescapeSmallFont()); + remaining.setFont(FontManager.getRunescapeSmallFont()); + duration.setFont(FontManager.getRunescapeSmallFont()); + currentDuration.setFont(FontManager.getRunescapeSmallFont()); + remainingDuration.setFont(FontManager.getRunescapeSmallFont()); + kills.setFont(FontManager.getRunescapeSmallFont()); + currentKills.setFont(FontManager.getRunescapeSmallFont()); + remainingKills.setFont(FontManager.getRunescapeSmallFont()); + xp.setFont(FontManager.getRunescapeSmallFont()); + currentXp.setFont(FontManager.getRunescapeSmallFont()); + remainingXp.setFont(FontManager.getRunescapeSmallFont()); + + statsPanel.add(new JLabel()); + statsPanel.add(elapsed); + statsPanel.add(remaining); + statsPanel.add(duration); + statsPanel.add(currentDuration); + statsPanel.add(remainingDuration); + statsPanel.add(kills); + statsPanel.add(currentKills); + statsPanel.add(remainingKills); + statsPanel.add(xp); + statsPanel.add(currentXp); + statsPanel.add(remainingXp); + + headerPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + headerPanel.setLayout(new BorderLayout()); + + headerPanel.add(statsPanel, BorderLayout.CENTER); + headerPanel.add(taskIcon, BorderLayout.WEST); + }); + + JPanel progressWrapper = new JPanel(); + progressWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR); + progressWrapper.setLayout(new BorderLayout()); + progressWrapper.setBorder(new EmptyBorder(0, 7, 7, 7)); + + progressBar.setMaximumValue(100); + progressBar.setBackground(new Color(61, 56, 49)); + + progressWrapper.add(progressBar, BorderLayout.NORTH); + + final JPanel logTitle = new JPanel(new BorderLayout(5, 0)); + logTitle.setBorder(new EmptyBorder(7, 7, 7, 7)); + logTitle.setBackground(ColorScheme.DARKER_GRAY_COLOR.darker()); + + String taskName = taskData.getTaskName(); + taskName = taskName.substring(0, 1).toUpperCase() + taskName.substring(1); + final JLabel titleLabel = new JLabel(taskName); + titleLabel.setFont(FontManager.getRunescapeSmallFont()); + titleLabel.setForeground(Color.WHITE); + + logTitle.add(titleLabel, BorderLayout.WEST); + + final JLabel subTitleLabel = new JLabel("x " + taskData.getInitialAmount()); + subTitleLabel.setFont(FontManager.getRunescapeSmallFont()); + subTitleLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + + logTitle.add(subTitleLabel, BorderLayout.CENTER); + + if (taskData.getTaskLocation() != null && !taskData.getTaskLocation().equals("")) + { + final JLabel locationLabel = new JLabel(taskData.getTaskLocation()); + locationLabel.setFont(FontManager.getRunescapeSmallFont()); + locationLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + + logTitle.add(locationLabel, BorderLayout.EAST); + } + + container.add(logTitle, BorderLayout.NORTH); + container.add(headerPanel, BorderLayout.CENTER); + container.add(progressWrapper, BorderLayout.SOUTH); + + add(container, BorderLayout.NORTH); + } + + void update(boolean updated, boolean paused, TaskData newData) + { + SwingUtilities.invokeLater(() -> rebuildAsync(updated, paused, newData)); + } + + private void rebuildAsync(boolean updated, boolean taskPaused, TaskData newData) + { + if (updated) + { + if (getParent() != panel) + { + panel.add(this, 0); + panel.revalidate(); + } + + // Update data + taskData.setElapsedKills(newData.getElapsedKills()); + taskData.setAmount(newData.getAmount()); + taskData.setElapsedXp(newData.getElapsedXp()); + + // Update information labels + int kills = taskData.getInitialAmount() - taskData.getAmount(); + currentKills.setText(htmlLabel(taskData.getElapsedKills())); + remainingKills.setText(htmlLabel(taskData.getAmount())); + + currentXp.setText(htmlLabel(taskData.getElapsedXp())); + double xpPerKill = ((double) taskData.getElapsedXp()) / ((double) taskData.getElapsedKills()); + double xpLeft = xpPerKill * taskData.getAmount(); + remainingXp.setText(htmlLabel((int) xpLeft)); + + // Update progress bar + double percentComplete = ((double) kills) / ((double) taskData.getInitialAmount()); + progressBar.setForeground(new Color(98, 70, 70)); + progressBar.setValue((int) (percentComplete * 100)); + progressBar.setCenterLabel(TWO_DECIMAL_FORMAT.format(percentComplete * 100) + "%"); + progressBar.setLeftLabel("0 Kc"); + progressBar.setRightLabel(taskData.getInitialAmount() + " Kc"); + progressBar.setPositions(Collections.emptyList()); + + double killsPerMillis = ((double) taskData.getElapsedKills() - 1) / ((double) taskData.getElapsedTime()); + if (killsPerMillis > 0) + { + double killsPerHour = killsPerMillis * MILLIS_PER_HOUR; + double millisPerKill = 1.0 / killsPerMillis; + long seconds = ((long) millisPerKill) / MILLIS_PER_SECOND; + long minutes = seconds / SECONDS_PER_MINUTE; + seconds %= 60; + long hours = minutes / MINUTES_PER_HOUR; + minutes %= 60; + progressBar.setToolTipText(String.format( + HTML_TOOL_TIP_TEMPLATE, + killsPerHour, + hours, + minutes, + seconds + )); + } + + if (taskData.getAmount() == 0 && taskData.getElapsedXp() == 0) + { + progressBar.setDimmedText("Skipped"); + progressBar.setForeground(new Color(128, 0, 0)); + } + else if ((taskData.getAmount() == 0 && taskData.getElapsedXp() > 0) || (int) (percentComplete * 100) >= 100) + { + progressBar.setDimmedText("Finished"); + progressBar.setForeground(new Color(0, 128, 0)); + } + else + { + progressBar.setDimmedText("Paused"); + } + + progressBar.setDimmed(taskPaused); + progressBar.repaint(); + } + else if (!paused && taskPaused) + { + progressBar.setDimmedText("Paused"); + progressBar.setDimmed(true); + progressBar.repaint(); + paused = true; + } + else if (paused && !taskPaused) + { + progressBar.setDimmed(false); + progressBar.repaint(); + paused = false; + } + + // Update duration separately, every time (not only when there's an update) + taskData.setElapsedTime(newData.getElapsedTime()); + currentDuration.setText(htmlLabel(taskData.getElapsedTime())); + remainingDuration.setText(htmlLabel(estimateRemainingTime(taskData))); + + repaint(); + } + + private static long estimateRemainingTime(TaskData taskData) + { + int kills = taskData.getElapsedKills(); + int killsInElapsedTime = kills - 1; // b/c time only elapses after 1st slayer drop + if (killsInElapsedTime < 1) + { + return Long.MAX_VALUE; + } + double timePerKill = ((double) taskData.getElapsedTime()) / ((double) killsInElapsedTime); + double remainingTime = timePerKill * taskData.getAmount(); + return (long) remainingTime; + } + + private static String htmlLabel(long timeMillis) + { + if (timeMillis == Long.MAX_VALUE) + { + String valueStr = "N/A"; + return String.format(HTML_LABEL_TEMPLATE, valueStr); + } + else + { + long seconds = timeMillis / MILLIS_PER_SECOND; + long minutes = seconds / SECONDS_PER_MINUTE; + seconds %= 60; + long hours = minutes / MINUTES_PER_HOUR; + minutes %= 60; + return String.format(HTML_TIME_LABEL_TEMPLATE, (int) hours, (int) minutes, (int) seconds); + } + } + + private static String htmlLabel(int value) + { + String valueStr = StackFormatter.quantityToRSDecimalStack(value); + return String.format(HTML_LABEL_TEMPLATE, valueStr); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskCounter.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskCounter.java index 0d4274c063..01f9b03890 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskCounter.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskCounter.java @@ -24,11 +24,10 @@ */ package net.runelite.client.plugins.slayer; +import java.awt.image.BufferedImage; import net.runelite.client.plugins.Plugin; import net.runelite.client.ui.overlay.infobox.Counter; -import java.awt.image.BufferedImage; - class TaskCounter extends Counter { TaskCounter(BufferedImage img, Plugin plugin, int amount) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskData.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskData.java new file mode 100644 index 0000000000..3e2cec384a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/TaskData.java @@ -0,0 +1,27 @@ +package net.runelite.client.plugins.slayer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +@Builder(toBuilder=true) +public class TaskData +{ + private long elapsedTime; + private int elapsedKills; + private int elapsedXp; + private int amount, initialAmount, lastCertainAmount; + private String taskLocation; + private String taskName; + private boolean paused; + + public void tick(long delta) + { + if (!paused) + { + elapsedTime += delta; + } + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/task/Scheduler.java b/runelite-client/src/main/java/net/runelite/client/task/Scheduler.java index 4533549de3..3a4926c7cb 100644 --- a/runelite-client/src/main/java/net/runelite/client/task/Scheduler.java +++ b/runelite-client/src/main/java/net/runelite/client/task/Scheduler.java @@ -60,6 +60,33 @@ public class Scheduler return Collections.unmodifiableList(scheduledMethods); } + public void registerObject(Object obj) + { + for (Method method : obj.getClass().getMethods()) + { + Schedule schedule = method.getAnnotation(Schedule.class); + if (schedule == null) + { + continue; + } + + ScheduledMethod scheduledMethod = new ScheduledMethod(schedule, method, obj); + addScheduledMethod(scheduledMethod); + } + } + + public void unregisterObject(Object obj) + { + for (ScheduledMethod sm : scheduledMethods) + { + if (sm.getObject() == obj) + { + removeScheduledMethod(sm); + break; + } + } + } + public void tick() { Instant now = Instant.now(); diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/panel_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/panel_icon.png new file mode 100644 index 0000000000..cd454c3956 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/panel_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/pause_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/pause_icon.png new file mode 100644 index 0000000000..57baef8bd3 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/pause_icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/play_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/play_icon.png new file mode 100644 index 0000000000..1a5b02e867 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/slayer/play_icon.png differ diff --git a/runelite-client/src/main/resources/slayer_xp.json b/runelite-client/src/main/resources/slayer_xp.json new file mode 100644 index 0000000000..75aa32dd17 --- /dev/null +++ b/runelite-client/src/main/resources/slayer_xp.json @@ -0,0 +1,1970 @@ +{ + "Wyrm": [ + 133.2, + 99 + ], + "Drake": [ + 268.7, + 192 + ], + "Hydra": [ + 322.5, + 194 + ], + "Alchemical Hydra": [ + 1320, + 426 + ], + "Rat": [ + 2.0, + 1.0, + 2.0, + 3.0, + 2.0, + 1.0, + 2.0, + 1.0 + ], + "Giant crypt rat": [ + 70.0, + 76.0 + ], + "Skeleton fremennik": [ + 25.0, + 40.0, + 35.0, + 50.0, + 40.0, + 60.0 + ], + "Revenant imp": [ + 10.0, + 7.0 + ], + "Gorak": [ + 112.0, + 145.0, + 112.0, + 149.0 + ], + "Moss giant": [ + 60.0, + 42.0, + 85.0, + 48.0 + ], + "Greater demon": [ + 87.0, + 92.0, + 115.0, + 100.0, + 120.0, + 101.0, + 120.0, + 104.0, + 130.0, + 113.0 + ], + "Malevolent Mage": [ + 1750.0, + 162.0 + ], + "Adamant dragon": [ + 325.0, + 338.0 + ], + "Skeleton Mage": [ + 17.0, + 16.0, + 80.0, + 83.0 + ], + "Ogre": [ + 60.0, + 53.0, + -1.0, + 58.0, + 60.0, + 63.0 + ], + "Reanimated dagannoth": [ + 35.0, + -1.0 + ], + "Feral Vampyre": [ + 40.0, + 61.0, + -1.0, + 64.0, + -1.0, + 70.0, + 50.0, + 72.0, + 60.0, + 77.0, + -1.0, + 100.0, + -1.0, + 130.0 + ], + "Thrower Troll": [ + 95.0, + 67.0, + 95.0, + 68.0 + ], + "Gorad": [ + 80.0, + 68.0 + ], + "Ancient Wyvern": [ + 315.0, + 210.0 + ], + "Mountain troll": [ + 90.0, + 69.0, + 90.0, + 71.0 + ], + "Infernal Mage": [ + 60.0, + 66.0 + ], + "Mourner": [ + -1.0, + 11.0, + -1.0, + 12.0, + -1.0, + 13.0, + -1.0, + 18.0, + -1.0, + 24.0, + -1.0, + 24.0, + 107.5, + 108.0 + ], + "Ancient Fungi": [ + 154.0, + 109.0 + ], + "Crawling Hand": [ + 16.0, + 8.0, + 19.0, + 12.0 + ], + "Jal-Nib": [ + 10.0, + 32.0 + ], + "Poison Scorpion": [ + 23.0, + 20.0 + ], + "Skeleton heavy": [ + 124.0, + 132.0 + ], + "Zygomite": [ + 65.0, + 74.0, + 75.0, + 86.0 + ], + "Jal-MejRah": [ + 25.0, + 85.0 + ], + "Cockathrice": [ + 950.0, + 89.0 + ], + "Jal-MejJak": [ + 80.0, + 250.0 + ], + "Jal-AkRek-Ket": [ + 15.0, + 70.0 + ], + "Yt-MejKot": [ + 80.0, + 180.0 + ], + "Revenant hellhound": [ + 86.0, + 90.0 + ], + "Chaos dwarf": [ + 61.0, + 48.0 + ], + "Undead cow": [ + 8.0, + 2.0 + ], + "Speedy Keith": [ + 37.0, + 34.0 + ], + "Ket-Zek": [ + 160.0, + 360.0 + ], + "Reanimated horror": [ + 35.0, + -1.0 + ], + "Karil the Tainted": [ + 100.0, + 98.0 + ], + "Flight Kilisa": [ + 132.5, + 159.0 + ], + "Zombie pirate": [ + 52.0, + 57.0 + ], + "Small Lizard": [ + 15.0, + 12.0 + ], + "Cave abomination": [ + 1300.0, + 206.0 + ], + "Dungeon rat": [ + 2.0, + 12.0 + ], + "Kraken": [ + 255.0, + 291.0 + ], + "Baby dragon": [ + 50.0, + 48.0, + 50.0, + 48.0, + 50.0, + 48.0, + 80.0, + 83.0 + ], + "Dharok the Wretched": [ + 115.0, + 115.0 + ], + "Chicken": [ + 3.0, + 1.0 + ], + "TzHaar-Mej": [ + 100.0, + 103.0 + ], + "Skeleton mage": [ + 17.0, + 16.0, + 80.0, + 83.0 + ], + "Undead Lumberjack": [ + 12.0, + 40.0, + 13.0, + 45.0, + 14.0, + 50.0, + 16.0, + 60.0 + ], + "Zulrah": [ + 500.0, + 725.0, + 500.0, + 725.0, + 500.0, + 725.0 + ], + "Vyrewatch": [ + -1.0, + 87.0, + 90.0, + 105.0, + 90.0, + 110.0, + 105.0, + 120.0, + 110.0, + 125.0 + ], + "Duckling": [ + 5.0, + 1.0 + ], + "Baby Roc": [ + 50.0, + 75.0 + ], + "Brutal black dragon": [ + 346.5, + 318.0 + ], + "Lava dragon": [ + 248.0, + 252.0 + ], + "Vulture": [ + 10.0, + 31.0 + ], + "Vampyre Juvinate": [ + -1.0, + 50.0, + -1.0, + 54.0, + -1.0, + 54.0, + -1.0, + 75.0, + 50.0, + 59.0, + 100.0, + 90.0, + -1.0, + 119.0 + ], + "Monkey": [ + 6.0, + 3.0 + ], + "Tstanon Karlak": [ + 142.0, + 145.0 + ], + "Zogre": [ + 71.0, + 44.0 + ], + "Aviansie": [ + 70.0, + 69.0, + 63.0, + 71.0, + 67.0, + 73.0, + 83.0, + 79.0, + 77.0, + 79.0, + 86.0, + 83.0, + 86.0, + 84.0, + 69.0, + 89.0, + 95.0, + 92.0, + 75.0, + 94.0, + 98.0, + 97.0, + 79.0, + 97.0, + 115.0, + 131.0, + 124.0, + 137.0, + 139.0, + 148.0 + ], + "Cave goblin": [ + 10.0, + 3.0 + ], + "Kalphite Worker": [ + 40.0, + 28.0 + ], + "Smoke Devil": [ + 185.0, + 160.0 + ], + "Arrg": [ + 150.0, + 113.0, + 299.5, + 210.0 + ], + "Crazy archaeologist": [ + 275.0, + 204.0 + ], + "Wolf": [ + 10.0, + 11.0, + 15.0, + 14.0, + 34.0, + 25.0, + 69.0, + 64.0 + ], + "Torag the Corrupted": [ + 112.5, + 115.0 + ], + "Suqah": [ + 107.5, + 111.0 + ], + "Night beast": [ + 6462.0, + 374.0 + ], + "Skeleton Hellhound": [ + 55.0, + 97.0, + -1.0, + 198.0 + ], + "Giant rockslug": [ + 770.0, + 86.0 + ], + "Rune dragon": [ + 363.0, + 380.0 + ], + "Dagannoth Prime": [ + 331.5, + 303.0 + ], + "Monkey Zombie": [ + 60.0, + 82.0, + 90.0, + 98.0, + 90.0, + 129.0 + ], + "Ogress Warrior": [ + 82.0, + 82.0 + ], + "Turoth": [ + 76.0, + 83.0, + 77.0, + 85.0, + 79.0, + 87.0, + 81.0, + 89.0 + ], + "Skotizo": [ + 618.5, + 321.0 + ], + "Iron dragon": [ + 173.2, + 189.0, + 204.5, + 215.0 + ], + "Revenant dragon": [ + 186.0, + 135.0 + ], + "Ice wolf": [ + 70.0, + 96.0, + 70.0, + 132.0 + ], + "Dark beast": [ + 225.4, + 182.0 + ], + "Ghost": [ + 25.0, + 19.0, + 20.0, + 24.0, + 75.0, + 76.0, + 80.0, + 77.0 + ], + "Dwarf gang member": [ + 40.0, + 44.0, + 25.0, + 49.0, + 25.0, + 44.0 + ], + "Revenant pyrefiend": [ + 48.0, + 52.0 + ], + "Abhorrent spectre": [ + 2500.0, + 253.0 + ], + "Cave slime": [ + 25.0, + 23.0 + ], + "Insatiable Bloodveld": [ + 2900.0, + 202.0 + ], + "Jungle spider": [ + 50.0, + 44.0 + ], + "Long-tailed Wyvern": [ + 205.0, + 152.0 + ], + "Elvarg": [ + 80.0, + 83.0, + -1.0, + 214.0 + ], + "General Graardor": [ + 338.0, + 624.0 + ], + "Scorpion": [ + 17.0, + 14.0, + 30.0, + 37.0, + 55.0, + 59.0 + ], + "Goblin": [ + 5.0, + 2.0, + 5.0, + 5.0, + 7.0, + 11.0, + 3.0, + 12.0, + 15.0, + 12.0, + 16.0, + 13.0, + 13.0, + 13.0, + 16.0, + 15.0, + 22.0, + 16.0, + 18.0, + 17.0, + 26.0, + 25.0 + ], + "Corrupt Lizardman": [ + 50.0, + 46.0, + 150.0, + 152.0 + ], + "Ranis Drakan": [ + 400.0, + 233.0, + 400.0, + 233.0 + ], + "Asyn Shade": [ + 90.0, + 100.0 + ], + "Greater Nechryael": [ + 205.0, + 200.0 + ], + "Undead chicken": [ + 3.0, + 1.0 + ], + "Basilisk": [ + 75.0, + 61.0 + ], + "Monkey Archer": [ + 50.0, + 86.0 + ], + "Cave goblin guard": [ + 26.0, + 24.0 + ], + "Ancient Zygomite": [ + 154.0, + 109.0 + ], + "Chompy bird": [ + 10.0, + 6.0 + ], + "Giant rat": [ + 5.0, + 3.0, + 10.0, + 6.0, + 25.0, + 26.0 + ], + "Blessed spider": [ + 32.0, + 39.0 + ], + "Brutal blue dragon": [ + 257.0, + 271.0 + ], + "Crypt spider": [ + 45.0, + 56.0 + ], + "Vitreous warped Jelly": [ + 2200.0, + 241.0 + ], + "Mutated Bloodveld": [ + 170.0, + 123.0 + ], + "Blue dragon": [ + 107.6, + 111.0 + ], + "Spiritual warrior": [ + 100.0, + 115.0, + 110.0, + 125.0, + 131.0, + 134.0, + 98.0, + 123.0 + ], + "Wall beast": [ + 105.0, + 49.0 + ], + "Giant bat": [ + 32.0, + 27.0 + ], + "Pee Hat": [ + 126.0, + 91.0 + ], + "Skeleton thug": [ + 124.0, + 132.0 + ], + "Dust devil": [ + 105.0, + 93.0, + 130.0, + 110.0 + ], + "Giant lobster": [ + 50.0, + 45.0 + ], + "Ice spider": [ + 65.0, + 61.0 + ], + "Scarab swarm": [ + 1.0, + 98.0 + ], + "Hill Giant": [ + 35.0, + 28.0 + ], + "Tz-Kek": [ + 20.0, + 45.0, + 10.0, + 22.0 + ], + "Twisted Banshee": [ + 100.0, + 89.0 + ], + "Sorebones": [ + 22.0, + 57.0 + ], + "Revenant hobgoblin": [ + 73.0, + 60.0 + ], + "Sergeant Grimspike": [ + 146.0, + 142.0 + ], + "Smoke devil": [ + 185.0, + 160.0 + ], + "Cyclops": [ + 75.0, + 56.0, + 100.0, + 76.0, + 110.0, + 81.0, + 150.0, + 106.0 + ], + "Jal-Zek": [ + 220.0, + 490.0 + ], + "TzHaar-Ket": [ + 140.0, + 149.0, + 200.0, + 221.0 + ], + "Flockleader Geerin": [ + 132.5, + 149.0 + ], + "Mammoth": [ + 130.0, + 80.0 + ], + "Stick": [ + 141.5, + 104.0 + ], + "TzKal-Zuk": [ + 101890.0, + 1400.0 + ], + "Waterfiend": [ + 128.0, + 115.0 + ], + "Skeleton hero": [ + 124.0, + 149.0 + ], + "Ghoul": [ + 50.0, + 42.0 + ], + "Thrower troll": [ + 95.0, + 67.0, + 95.0, + 68.0 + ], + "Enclave guard": [ + 80.0, + 83.0 + ], + "Crypt rat": [ + 35.0, + 43.0 + ], + "Fire giant": [ + 111.0, + 86.0, + 133.5, + 104.0, + 153.5, + 109.0 + ], + "Skeleton warlord": [ + 124.0, + 132.0 + ], + "Dagannoth Supreme": [ + 255.0, + 303.0 + ], + "Phrin Shadow": [ + 56.0, + 60.0 + ], + "Nechryarch": [ + 3280.0, + 300.0 + ], + "Giant scarab": [ + 137.0, + 191.0, + 280.5, + 316.0 + ], + "Kraka": [ + 126.0, + 91.0 + ], + "Bird": [ + 5.0, + 5.0, + 10.0, + 11.0 + ], + "Kolodion": [ + -1.0, + -1.0, + -1.0, + -1.0, + -1.0, + -1.0, + -1.0, + -1.0, + 107.0, + 112.0 + ], + "Ent": [ + 107.5, + 86.0, + 107.5, + 101.0 + ], + "White wolf": [ + 44.0, + 38.0 + ], + "Death wing": [ + 80.0, + 83.0 + ], + "Sergeant Strongstack": [ + 128.0, + 141.0 + ], + "Shade": [ + 5.0, + 159.0, + 115.0, + 140.0 + ], + "Mithril dragon": [ + 273.0, + 304.0 + ], + "Bronze dragon": [ + 125.0, + 131.0, + 125.0, + 143.0 + ], + "Ice troll grunt": [ + 84.0, + 100.0 + ], + "Bloodveld": [ + 120.0, + 76.0, + 120.0, + 81.0 + ], + "TzTok-Jad": [ + 25250.0, + 702.0 + ], + "Brine rat": [ + 50.0, + 70.0 + ], + "Vitreous Jelly": [ + 1900.0, + 206.0 + ], + "Taloned Wyvern": [ + 205.0, + 147.0 + ], + "Skeleton": [ + 18.0, + 13.0, + 24.0, + 21.0, + 29.0, + 22.0, + 17.0, + 25.0, + -1.0, + 42.0, + 59.0, + 45.0, + 70.0, + 60.0, + 70.0, + 68.0, + 51.0, + 77.0, + 77.0, + 85.0 + ], + "Cave horror": [ + 55.0, + 80.0 + ], + "Revenant cyclops": [ + 119.0, + 82.0 + ], + "Monkey guard": [ + 130.0, + 167.0 + ], + "Asyn Shadow": [ + 90.0, + 100.0 + ], + "Black dragon": [ + 199.5, + 227.0, + 262.0, + 247.0 + ], + "Black bear": [ + 25.0, + 19.0 + ], + "Troll general": [ + 150.5, + 113.0 + ], + "Greater Skeleton Hellhound": [ + 214.5, + 281.0 + ], + "Dusk": [ + 1350.0, + 248.0, + 1350.0, + 328.0 + ], + "Warped Jelly": [ + 140.0, + 112.0 + ], + "Black Heather": [ + 37.0, + 34.0 + ], + "Dark warrior": [ + 17.0, + 8.0, + 182.0, + 145.0 + ], + "K'ril Tsutsaroth": [ + 350.5, + 650.0 + ], + "Ice troll male": [ + 84.0, + 82.0 + ], + "Jal-Ak": [ + 40.0, + 165.0 + ], + "Molanisk": [ + 52.0, + 51.0 + ], + "Cave kraken": [ + 125.0, + 127.0 + ], + "Yt-HurKot": [ + 60.0, + 108.0, + -1.0, + 141.0 + ], + "Repugnant spectre": [ + 4085.0, + 335.0 + ], + "Abyssal Sire": [ + 450.0, + 350.0 + ], + "Gunthor the brave": [ + 35.0, + 29.0 + ], + "Earth warrior": [ + 54.0, + 51.0 + ], + "Terror dog": [ + 82.0, + 100.0, + 87.0, + 110.0 + ], + "Agrith Naar": [ + 100.0, + 100.0, + 100.0, + 196.0 + ], + "Marble gargoyle": [ + 2768.0, + 349.0 + ], + "Minotaur": [ + 10.0, + 12.0, + 22.0, + 27.0 + ], + "Reanimated abyssal": [ + 35.0, + -1.0 + ], + "Spider": [ + 2.0, + 1.0, + 2.0, + 1.0, + 2.0, + 1.0, + 22.0, + 24.0 + ], + "Red dragon": [ + 143.0, + 152.0 + ], + "Duck": [ + 6.0, + -1.0 + ], + "Guthan the Infested": [ + 115.0, + 115.0 + ], + "Cave goblin miner": [ + 10.0, + 11.0 + ], + "Rooster": [ + 7.0, + 2.0 + ], + "Wingman Skree": [ + 124.0, + 143.0 + ], + "Shadow warrior": [ + 67.0, + 48.0 + ], + "Monstrous basilisk": [ + 1700.0, + 135.0 + ], + "Banshee": [ + 22.0, + 23.0 + ], + "Ice Troll": [ + 129.0, + 120.0, + 97.0, + 121.0, + 86.0, + 123.0, + 86.0, + 124.0 + ], + "Zakl'n Gritch": [ + 150.0, + 142.0 + ], + "Harpie Bug Swarm": [ + 25.0, + 46.0 + ], + "Lizard": [ + 40.0, + 42.0 + ], + "Ice troll female": [ + 84.0, + 82.0 + ], + "Spiritual ranger": [ + 120.0, + 118.0, + 106.0, + 122.0, + 131.0, + 115.0, + 120.0, + 127.0 + ], + "Gargoyle": [ + 105.0, + 111.0 + ], + "Reanimated bloodveld": [ + 35.0, + -1.0 + ], + "Rogue": [ + 17.0, + 15.0, + 125.0, + 135.0 + ], + "Black demon": [ + 157.0, + 172.0, + 160.0, + 178.0, + 170.0, + 184.0, + 200.0, + 188.0, + 255.0, + 292.0 + ], + "Kree'arra": [ + 357.0, + 580.0 + ], + "Grave scorpion": [ + 7.0, + 12.0 + ], + "Brutal green dragon": [ + 183.0, + 227.0 + ], + "Chasm Crawler": [ + 600.0, + 68.0 + ], + "Elder Chaos druid": [ + 150.0, + 129.0 + ], + "Vampyre Juvenile": [ + 60.0, + 45.0, + 60.0, + 45.0 + ], + "Guard dog": [ + 49.0, + 44.0 + ], + "Seagull": [ + 5.0, + 2.0, + 5.0, + 3.0 + ], + "Skeleton brute": [ + 124.0, + 132.0 + ], + "Otherworldly being": [ + 66.0, + 64.0 + ], + "Lizardman shaman": [ + 157.5, + 150.0 + ], + "Tz-Kih": [ + 10.0, + 22.0 + ], + "Skeletal miner": [ + 39.0, + 42.0 + ], + "Blessed giant rat": [ + 30.0, + 9.0 + ], + "Flaming pyrelord": [ + 1250.0, + 97.0 + ], + "Insatiable mutated Bloodveld": [ + 4100.0, + 278.0 + ], + "Berry": [ + 90.0, + 71.0 + ], + "Cow calf": [ + 6.0, + 2.0 + ], + "Porazdir": [ + 376.0, + 235.0 + ], + "Lesser demon": [ + 79.0, + 82.0, + 85.0, + 87.0, + 98.0, + 94.0, + 110.0, + 94.0 + ], + "JalTok-Jad": [ + 350.0, + 900.0 + ], + "Terrorbird": [ + 34.0, + 28.0 + ], + "King Black Dragon": [ + 258.0, + 276.0 + ], + "Kalphite Guardian": [ + 170.0, + 141.0 + ], + "Fiyr Shadow": [ + 110.0, + 120.0, + 110.0, + 140.0 + ], + "Obor": [ + 129.0, + 106.0 + ], + "Revenant demon": [ + 86.0, + 98.0 + ], + "Ice giant": [ + 70.0, + 53.0, + 100.0, + 67.0 + ], + "Baby red dragon": [ + 50.0, + 48.0 + ], + "Temple guardian": [ + 45.0, + 30.0 + ], + "Hellhound": [ + 116.0, + 122.0, + 116.0, + 127.0, + 150.0, + 136.0 + ], + "Dwarf": [ + 16.0, + 10.0 + ], + "Skogre": [ + 71.0, + 44.0 + ], + "Tortured soul": [ + 51.0, + 59.0 + ], + "Steel dragon": [ + 220.5, + 246.0, + 262.5, + 274.0 + ], + "King Scorpion": [ + 30.0, + 32.0 + ], + "Callisto": [ + 312.0, + 470.0 + ], + "Spiritual mage": [ + 75.0, + 121.0, + 85.0, + 120.0, + 106.0, + 121.0, + 75.0, + 123.0 + ], + "Revenant knight": [ + 168.0, + 126.0 + ], + "Monkey Guard": [ + 130.0, + 167.0 + ], + "Kalphite Queen": [ + 535.5, + 333.0, + 537.5, + 333.0 + ], + "Revenant goblin": [ + 14.0, + 15.0 + ], + "Crushing hand": [ + 605.0, + 45.0 + ], + "Reanimated kalphite": [ + 35.0, + -1.0 + ], + "Green dragon": [ + 75.0, + 79.0, + 100.0, + 88.0 + ], + "Ice warrior": [ + 59.0, + 57.0 + ], + "Reanimated aviansie": [ + 35.0, + -1.0 + ], + "Nuclear smoke devil": [ + 2400.0, + 280.0 + ], + "Zombie swab": [ + 50.0, + 55.0 + ], + "Dark Ankou": [ + 60.0, + 95.0 + ], + "Poison spider": [ + 64.0, + 31.0, + 64.0, + 64.0 + ], + "Rockslug": [ + 27.0, + 29.0 + ], + "Sea Snake Young": [ + 85.0, + 90.0 + ], + "Fiyr Shade": [ + 110.0, + 120.0, + 110.0, + 140.0 + ], + "Venenatis": [ + 388.8, + 464.0 + ], + "Tortured gorilla": [ + 409.0, + 141.0, + -1.0, + 142.0 + ], + "Kamil": [ + 146.0, + 154.0, + -1.0, + 273.0 + ], + "Reanimated elf": [ + 35.0, + -1.0 + ], + "Jal-AkRek-Mej": [ + 15.0, + 70.0 + ], + "Grizzly bear cub": [ + 35.0, + 33.0 + ], + "Giant crypt spider": [ + 80.0, + 79.0 + ], + "Ogre chieftain": [ + 60.0, + 81.0 + ], + "Ice troll": [ + 129.0, + 120.0, + 97.0, + 121.0, + 86.0, + 123.0, + 86.0, + 124.0 + ], + "Dagannoth Rex": [ + 331.5, + 303.0 + ], + "Thermonuclear smoke devil": [ + 240.0, + 301.0 + ], + "Black Guard Berserker": [ + 50.0, + 66.0, + 50.0, + 66.0, + 50.0, + 66.0 + ], + "Chaos Elemental": [ + 250.0, + 305.0 + ], + "TzHaar-Hur": [ + 80.0, + 74.0 + ], + "Revenant ork": [ + 115.0, + 105.0 + ], + "Ahrim the Blighted": [ + 102.5, + 98.0 + ], + "Pit Scorpion": [ + 32.0, + 28.0 + ], + "Jungle horror": [ + 45.0, + 70.0 + ], + "Ice Troll King": [ + 161.0, + 122.0, + 287.0, + 213.0 + ], + "Dad": [ + 126.0, + 101.0, + 264.0, + 201.0 + ], + "Screaming banshee": [ + 610.0, + 70.0 + ], + "Jal-AkRek-Xil": [ + 15.0, + 70.0 + ], + "Bryophyta": [ + 115.0, + 128.0 + ], + "Cow": [ + 8.0, + 2.0, + -1.0, + 170.0 + ], + "Aberrant spectre": [ + 90.0, + 96.0 + ], + "Dagannoth spawn": [ + 35.0, + 42.0 + ], + "Fever spider": [ + 40.0, + 49.0 + ], + "Locust rider": [ + 92.5, + 98.0, + 92.5, + 106.0 + ], + "City guard": [ + 80.0, + 83.0 + ], + "Spitting Wyvern": [ + 205.0, + 139.0 + ], + "Skeletal Wyvern": [ + 210.0, + 140.0 + ], + "Shadow spider": [ + 55.0, + 52.0 + ], + "Giant spider": [ + 5.0, + 2.0, + 33.0, + 27.0, + 50.0, + 50.0 + ], + "Killerwatt": [ + 51.0, + 55.0 + ], + "Demonic gorilla": [ + 408.5, + 275.0 + ], + "Bear Cub": [ + 20.0, + 15.0 + ], + "Scarab mage": [ + 51.2, + -1.0 + ], + "Riyl Shadow": [ + 76.0, + 76.0 + ], + "Revenant dark beast": [ + 157.0, + 120.0 + ], + "Lizardman": [ + 60.0, + 53.0, + 60.0, + 62.0 + ], + "Penguin": [ + 4.0, + 2.0 + ], + "Scorpia's offspring": [ + 2.0, + 15.0 + ], + "Deadly red spider": [ + 35.0, + 34.0 + ], + "Loar Shadow": [ + 38.0, + 40.0 + ], + "Deviant spectre": [ + 194.5, + 169.0 + ], + "Entrana firebird": [ + 5.0, + 2.0 + ], + "Desert Lizard": [ + 15.0, + 12.0, + 25.0, + 24.0, + 40.0, + 42.0 + ], + "Riyl Shade": [ + 76.0, + 76.0 + ], + "Nechryael": [ + 105.0, + 115.0 + ], + "Big Wolf": [ + 74.0, + 73.0 + ], + "Grizzly bear": [ + 27.0, + -1.0 + ], + "King kurask": [ + 2767.0, + 295.0 + ], + "Vet'ion": [ + 312.0, + 454.0, + 322.0, + 454.0 + ], + "Hobgoblin": [ + 29.0, + 28.0, + 29.0, + 28.0, + 49.0, + 42.0, + -1.0, + 47.0 + ], + "Bouncer": [ + 116.0, + 137.0, + -1.0, + 244.0 + ], + "Troll spectator": [ + 90.0, + 71.0 + ], + "Zombie": [ + 22.0, + 13.0, + 24.0, + 18.0, + -1.0, + 23.0, + 30.0, + 24.0 + ], + "Wormbrain": [ + 5.0, + 2.0 + ], + "Jackal": [ + 27.0, + 21.0 + ], + "Cave bug": [ + 5.0, + 6.0, + 93.0, + 96.0 + ], + "Scorpia's guardian": [ + 70.0, + 47.0 + ], + "Choke devil": [ + 3000.0, + 264.0 + ], + "Mogre": [ + 48.0, + 60.0 + ], + "Mounted terrorbird gnome": [ + 36.0, + -1.0 + ], + "Ogress Shaman": [ + 82.0, + 82.0 + ], + "Loar Shade": [ + 38.0, + 40.0 + ], + "Dire Wolf": [ + 85.0, + 88.0 + ], + "Tok-Xil": [ + 40.0, + 90.0 + ], + "Elf warrior": [ + 107.5, + 108.0, + 105.0, + 90.0 + ], + "Ice troll runt": [ + 63.0, + 74.0 + ], + "Scorpia": [ + 260.0, + 225.0 + ], + "Abyssal demon": [ + 150.0, + 124.0, + 150.0, + 124.0 + ], + "Kalphite Soldier": [ + 90.0, + 85.0 + ], + "Icelord": [ + 60.0, + 51.0 + ], + "Jal-Xil": [ + 130.0, + 370.0 + ], + "Vorkath": [ + 750.0, + 732.0, + 460.0, + 392.0 + ], + "Chaos druid": [ + 20.0, + 13.0 + ], + "Jubbly bird": [ + 8.0, + 9.0 + ], + "Phrin Shade": [ + 56.0, + 60.0 + ], + "River troll": [ + 5.0, + 14.0, + -1.0, + 29.0, + -1.0, + 49.0, + -1.0, + 79.0, + 3.0, + 120.0, + -1.0, + 159.0 + ], + "Magic axe": [ + 44.0, + 42.0 + ], + "Guard Bandit": [ + 29.0, + 22.0 + ], + "Jungle Wolf": [ + 70.0, + 64.0 + ], + "Sea Snake Hatchling": [ + 50.0, + 62.0 + ], + "Balfrug Kreeyath": [ + 161.0, + 151.0 + ], + "Icefiend": [ + 15.0, + 13.0, + 20.0, + 18.0 + ], + "Sergeant Steelwill": [ + 127.0, + 142.0 + ], + "Desert Wolf": [ + 34.0, + 27.0 + ], + "Chaos Fanatic": [ + 253.0, + 202.0 + ], + "Cockatrice": [ + 37.0, + 37.0 + ], + "Verac the Defiled": [ + 112.5, + 115.0 + ], + "Brutal red dragon": [ + 306.2, + 289.0 + ], + "Wild dog": [ + 62.0, + 63.0 + ], + "Crocodile": [ + 62.0, + 63.0 + ], + "Werewolf": [ + -1.0, + 24.0, + 100.0, + 88.0, + -1.0, + 93.0 + ], + "Cerberus": [ + 690.0, + 318.0 + ], + "Giant Mole": [ + 215.0, + 230.0 + ], + "Flesh Crawler": [ + 25.0, + 28.0, + 25.0, + 35.0, + 25.0, + 41.0 + ], + "Greater abyssal demon": [ + 4200.0, + 342.0 + ], + "Pyrefiend": [ + 45.0, + 43.0 + ], + "Ankou": [ + 60.0, + 75.0, + 65.0, + 82.0, + 70.0, + 86.0, + 60.0, + 95.0, + 100.0, + 98.0 + ], + "Cave crawler": [ + 22.0, + 23.0 + ], + "Lizardman brute": [ + 60.0, + 73.0 + ], + "Jal-ImKot": [ + 81.0, + 240.0 + ], + "Treus Dayth": [ + 100.0, + 95.0, + 100.0, + 194.0 + ], + "Reanimated giant": [ + 35.0, + -1.0 + ], + "Reanimated troll": [ + 35.0, + -1.0 + ], + "Screaming twisted banshee": [ + 2200.0, + 144.0 + ], + "Black Guard": [ + 30.0, + 25.0, + 40.0, + 48.0 + ], + "Kurask": [ + 97.0, + 106.0 + ], + "Commander Zilyana": [ + 350.0, + 596.0 + ], + "Bat": [ + 8.0, + 6.0 + ], + "TzHaar-Xil": [ + 120.0, + 133.0 + ], + "Oomlie bird": [ + 90.0, + 46.0 + ], + "Catablepon": [ + 40.0, + 49.0, + 70.0, + 64.0, + 50.0, + 68.0 + ], + "Deranged archaeologist": [ + 200.0, + 276.0 + ], + "Shadow Hound": [ + 58.0, + 63.0 + ], + "Jelly": [ + 75.0, + 78.0 + ] +} diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java index cf0f1d5bdb..c8234a7dc6 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java @@ -35,9 +35,11 @@ import static net.runelite.api.ChatMessageType.GAMEMESSAGE; import net.runelite.api.Client; import net.runelite.api.MessageNode; import net.runelite.api.Player; +import net.runelite.api.Varbits; import net.runelite.api.coords.LocalPoint; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameTick; +import net.runelite.api.events.VarbitChanged; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.Notifier; @@ -73,7 +75,6 @@ public class SlayerPluginTest private static final String TASK_CHECKSLAYERGEM = "You're assigned to kill Suqahs; only 211 more to go."; private static final String TASK_CHECKSLAYERGEM_WILDERNESS = "You're assigned to kill Suqahs in the Wilderness; only 211 more to go."; private static final String TASK_CHECKSLAYERGEM_KONAR = "You're assigned to kill Blue dragons in the Ogre Enclave; only 122 more to go."; - private static final String TASK_UPDATE_COMBAT_BRACELET = "You still need to kill 30 monsters to complete your current Slayer assignment"; private static final String TASK_BOSS_NEW = "Excellent. You're now assigned to kill Vet'ion 3 times.
Your reward point tally is 914."; private static final String TASK_BOSS_NEW_THE = "Excellent. You're now assigned to kill the Chaos
Elemental 3 times. Your reward point tally is 914."; @@ -92,24 +93,6 @@ public class SlayerPluginTest private static final String SUPERIOR_MESSAGE = "A superior foe has appeared..."; - private static final String BRACLET_SLAUGHTER = "Your bracelet of slaughter prevents your slayer count decreasing. It has 9 charges left."; - private static final String BRACLET_EXPEDITIOUS = "Your expeditious bracelet helps you progress your slayer task faster. It has 9 charges left."; - - private static final String BRACLET_SLAUGHTER_V2 = "Your bracelet of slaughter prevents your slayer count decreasing. It has 1 charge left."; - private static final String BRACLET_EXPEDITIOUS_V2 = "Your expeditious bracelet helps you progress your slayer faster. It has 1 charge left."; - - private static final String BRACLET_SLAUGHTER_V3 = "Your bracelet of slaughter prevents your slayer count decreasing. It then crumbles to dust."; - private static final String BRACLET_EXPEDITIOUS_V3 = "Your expeditious bracelet helps you progress your slayer faster. It then crumbles to dust."; - - private static final String CHAT_BRACELET_SLAUGHTER_CHARGE = "Your bracelet of slaughter has 12 charges left."; - private static final String CHAT_BRACELET_EXPEDITIOUS_CHARGE = "Your expeditious bracelet has 12 charges left."; - - private static final String CHAT_BRACELET_SLAUGHTER_CHARGE_ONE = "Your bracelet of slaughter has 1 charge left."; - private static final String CHAT_BRACELET_EXPEDITIOUS_CHARGE_ONE = "Your expeditious bracelet has 1 charge left."; - - private static final String BREAK_SLAUGHTER = "The bracelet shatters. Your next bracelet of slaughter
will start afresh from 30 charges."; - private static final String BREAK_EXPEDITIOUS = "The bracelet shatters. Your next expeditious bracelet
will start afresh from 30 charges."; - @Mock @Bind Client client; @@ -157,6 +140,10 @@ public class SlayerPluginTest @Inject SlayerPlugin slayerPlugin; + @Mock + @Bind + SlayerTaskPanel panel; + @Before public void before() { @@ -171,8 +158,8 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("Suqahs", slayerPlugin.getTaskName()); - assertEquals(231, slayerPlugin.getAmount()); + assertEquals("Suqahs", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(231, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -183,9 +170,9 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("Wyrms", slayerPlugin.getTaskName()); - assertEquals(147, slayerPlugin.getAmount()); - assertEquals("Karuulm Slayer Dungeon", slayerPlugin.getTaskLocation()); + assertEquals("Wyrms", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(147, slayerPlugin.getCurrentTask().getAmount()); + assertEquals("Karuulm Slayer Dungeon", slayerPlugin.getCurrentTask().getTaskLocation()); } @Test @@ -196,9 +183,9 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("Hellhounds", slayerPlugin.getTaskName()); - assertEquals(142, slayerPlugin.getAmount()); - assertEquals("Witchhaven Dungeon", slayerPlugin.getTaskLocation()); + assertEquals("Hellhounds", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(142, slayerPlugin.getCurrentTask().getAmount()); + assertEquals("Witchhaven Dungeon", slayerPlugin.getCurrentTask().getTaskLocation()); } @Test @@ -209,8 +196,8 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("goblins", slayerPlugin.getTaskName()); - assertEquals(17, slayerPlugin.getAmount()); + assertEquals("goblins", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(17, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -221,8 +208,8 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("Suqahs", slayerPlugin.getTaskName()); - assertEquals(211, slayerPlugin.getAmount()); + assertEquals("Suqahs", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(211, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -233,8 +220,8 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("Vet'ion", slayerPlugin.getTaskName()); - assertEquals(3, slayerPlugin.getAmount()); + assertEquals("Vet'ion", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(3, slayerPlugin.getCurrentTask().getAmount()); assertEquals(914, slayerPlugin.getPoints()); } @@ -246,8 +233,8 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("Chaos Elemental", slayerPlugin.getTaskName()); - assertEquals(3, slayerPlugin.getAmount()); + assertEquals("Chaos Elemental", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(3, slayerPlugin.getCurrentTask().getAmount()); assertEquals(914, slayerPlugin.getPoints()); } @@ -257,8 +244,8 @@ public class SlayerPluginTest ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", TASK_NEW_FROM_PARTNER, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); - assertEquals("Dust Devils", slayerPlugin.getTaskName()); - assertEquals(377, slayerPlugin.getAmount()); + assertEquals("Dust Devils", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(377, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -266,8 +253,8 @@ public class SlayerPluginTest { ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", TASK_CHECKSLAYERGEM, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); - assertEquals("Suqahs", slayerPlugin.getTaskName()); - assertEquals(211, slayerPlugin.getAmount()); + assertEquals("Suqahs", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(211, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -275,9 +262,9 @@ public class SlayerPluginTest { ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", TASK_CHECKSLAYERGEM_WILDERNESS, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); - assertEquals("Suqahs", slayerPlugin.getTaskName()); - assertEquals(211, slayerPlugin.getAmount()); - assertEquals("Wilderness", slayerPlugin.getTaskLocation()); + assertEquals("Suqahs", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(211, slayerPlugin.getCurrentTask().getAmount()); + assertEquals("Wilderness", slayerPlugin.getCurrentTask().getTaskLocation()); } @Test @@ -286,9 +273,9 @@ public class SlayerPluginTest ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", TASK_CHECKSLAYERGEM_KONAR, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); - assertEquals("Blue dragons", slayerPlugin.getTaskName()); - assertEquals(122, slayerPlugin.getAmount()); - assertEquals("Ogre Enclave", slayerPlugin.getTaskLocation()); + assertEquals("Blue dragons", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(122, slayerPlugin.getCurrentTask().getAmount()); + assertEquals("Ogre Enclave", slayerPlugin.getCurrentTask().getTaskLocation()); } @Test @@ -299,23 +286,8 @@ public class SlayerPluginTest when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog); slayerPlugin.onGameTick(new GameTick()); - assertEquals("suqahs", slayerPlugin.getTaskName()); - assertEquals(222, slayerPlugin.getAmount()); - } - - @Test - public void testRewardPointsWidget() - { - Widget rewardBar = mock(Widget.class); - Widget rewardBarText = mock(Widget.class); - Widget[] rewardBarChildren = new Widget[]{rewardBarText}; - - when(rewardBar.getDynamicChildren()).thenReturn(rewardBarChildren); - when(rewardBarText.getText()).thenReturn(REWARD_POINTS); - when(client.getWidget(WidgetInfo.SLAYER_REWARDS_TOPBAR)).thenReturn(rewardBar); - slayerPlugin.onGameTick(new GameTick()); - - assertEquals(17566, slayerPlugin.getPoints()); + assertEquals("suqahs", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(222, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -325,8 +297,8 @@ public class SlayerPluginTest slayerPlugin.onChatMessage(chatMessageEvent); assertEquals(1, slayerPlugin.getStreak()); - assertEquals("", slayerPlugin.getTaskName()); - assertEquals(0, slayerPlugin.getAmount()); + assertEquals("", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(0, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -336,58 +308,66 @@ public class SlayerPluginTest slayerPlugin.onChatMessage(chatMessageEvent); assertEquals(3, slayerPlugin.getStreak()); - assertEquals("", slayerPlugin.getTaskName()); - assertEquals(0, slayerPlugin.getAmount()); + assertEquals("", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(0, slayerPlugin.getCurrentTask().getAmount()); } @Test public void testPoints() { + when(client.getVar(Varbits.SLAYER_REWARD_POINTS)).thenReturn(18_000); + ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "Perterter", TASK_POINTS, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); + VarbitChanged varbitChanged = new VarbitChanged(); + slayerPlugin.onVarbitChanged(varbitChanged); assertEquals(9, slayerPlugin.getStreak()); - assertEquals("", slayerPlugin.getTaskName()); - assertEquals(0, slayerPlugin.getAmount()); + assertEquals("", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(0, slayerPlugin.getCurrentTask().getAmount()); assertEquals(18_000, slayerPlugin.getPoints()); } @Test public void testLargeStreak() { + when(client.getVar(Varbits.SLAYER_REWARD_POINTS)).thenReturn(17_566_000); + ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "Perterter", TASK_LARGE_STREAK, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); + VarbitChanged varbitChanged = new VarbitChanged(); + slayerPlugin.onVarbitChanged(varbitChanged); assertEquals(2465, slayerPlugin.getStreak()); - assertEquals("", slayerPlugin.getTaskName()); - assertEquals(0, slayerPlugin.getAmount()); + assertEquals("", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(0, slayerPlugin.getCurrentTask().getAmount()); assertEquals(17_566_000, slayerPlugin.getPoints()); } @Test public void testComplete() { - slayerPlugin.setTaskName("cows"); - slayerPlugin.setAmount(42); + slayerPlugin.getCurrentTask().setTaskName("cows"); + slayerPlugin.getCurrentTask().setAmount(42); ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "Perterter", TASK_COMPLETE, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); - assertEquals("", slayerPlugin.getTaskName()); - assertEquals(0, slayerPlugin.getAmount()); + assertEquals("", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(0, slayerPlugin.getCurrentTask().getAmount()); } @Test public void testCancelled() { - slayerPlugin.setTaskName("cows"); - slayerPlugin.setAmount(42); + slayerPlugin.getCurrentTask().setTaskName("cows"); + slayerPlugin.getCurrentTask().setAmount(42); ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "Perterter", TASK_CANCELED, null, 0); slayerPlugin.onChatMessage(chatMessageEvent); - assertEquals("", slayerPlugin.getTaskName()); - assertEquals(0, slayerPlugin.getAmount()); + assertEquals("", slayerPlugin.getCurrentTask().getTaskName()); + assertEquals(0, slayerPlugin.getCurrentTask().getAmount()); } @Test @@ -404,120 +384,6 @@ public class SlayerPluginTest verifyNoMoreInteractions(notifier); } - @Test - public void testBraceletSlaughter() - { - ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", BRACLET_SLAUGHTER, null, 0); - - slayerPlugin.setAmount(42); - slayerPlugin.setSlaughterChargeCount(10); - - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(9, slayerPlugin.getSlaughterChargeCount()); - assertEquals(43, slayerPlugin.getAmount()); - - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", CHAT_BRACELET_SLAUGHTER_CHARGE, null, 0); - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(12, slayerPlugin.getSlaughterChargeCount()); - - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", CHAT_BRACELET_SLAUGHTER_CHARGE_ONE, null, 0); - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(1, slayerPlugin.getSlaughterChargeCount()); - - slayerPlugin.setSlaughterChargeCount(1); - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", BRACLET_SLAUGHTER_V3, null, 0); - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(30, slayerPlugin.getSlaughterChargeCount()); - - Widget braceletBreakWidget = mock(Widget.class); - when(braceletBreakWidget.getText()).thenReturn(BREAK_SLAUGHTER); - when(client.getWidget(WidgetInfo.DIALOG_SPRITE_TEXT)).thenReturn(braceletBreakWidget); - - slayerPlugin.setSlaughterChargeCount(-1); - slayerPlugin.onGameTick(new GameTick()); - assertEquals(30, slayerPlugin.getSlaughterChargeCount()); - - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", BRACLET_SLAUGHTER_V2, null, 0); - - slayerPlugin.setAmount(42); - slayerPlugin.setSlaughterChargeCount(2); - - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(1, slayerPlugin.getSlaughterChargeCount()); - assertEquals(43, slayerPlugin.getAmount()); - } - - @Test - public void testBraceletExpeditious() - { - ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", BRACLET_EXPEDITIOUS, null, 0); - - slayerPlugin.setAmount(42); - slayerPlugin.setExpeditiousChargeCount(10); - - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(41, slayerPlugin.getAmount()); - assertEquals(9, slayerPlugin.getExpeditiousChargeCount()); - - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", CHAT_BRACELET_EXPEDITIOUS_CHARGE, null, 0); - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(12, slayerPlugin.getExpeditiousChargeCount()); - - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", CHAT_BRACELET_EXPEDITIOUS_CHARGE_ONE, null, 0); - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(1, slayerPlugin.getExpeditiousChargeCount()); - - slayerPlugin.setExpeditiousChargeCount(1); - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", BRACLET_EXPEDITIOUS_V3, null, 0); - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(30, slayerPlugin.getExpeditiousChargeCount()); - - Widget braceletBreakWidget = mock(Widget.class); - when(braceletBreakWidget.getText()).thenReturn(BREAK_EXPEDITIOUS); - when(client.getWidget(WidgetInfo.DIALOG_SPRITE_TEXT)).thenReturn(braceletBreakWidget); - - slayerPlugin.setExpeditiousChargeCount(-1); - slayerPlugin.onGameTick(new GameTick()); - assertEquals(30, slayerPlugin.getExpeditiousChargeCount()); - - chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", BRACLET_EXPEDITIOUS_V2, null, 0); - - slayerPlugin.setAmount(42); - slayerPlugin.setExpeditiousChargeCount(2); - - slayerPlugin.onChatMessage(chatMessageEvent); - - assertEquals(41, slayerPlugin.getAmount()); - assertEquals(1, slayerPlugin.getExpeditiousChargeCount()); - } - - @Test - public void testCombatBraceletUpdate() - { - final Player player = mock(Player.class); - when(player.getLocalLocation()).thenReturn(new LocalPoint(0, 0)); - when(client.getLocalPlayer()).thenReturn(player); - - slayerPlugin.setTaskName("Suqahs"); - slayerPlugin.setAmount(231); - - ChatMessage chatMessage = new ChatMessage(null, GAMEMESSAGE, "", TASK_UPDATE_COMBAT_BRACELET, null, 0); - slayerPlugin.onChatMessage(chatMessage); - - assertEquals("Suqahs", slayerPlugin.getTaskName()); - slayerPlugin.killedOne(); - assertEquals(30, slayerPlugin.getAmount()); - } - @Test public void testTaskLookup() throws IOException {