Add NPC unaggression timer

This commit is contained in:
WooxSolo
2019-03-19 15:38:31 -04:00
committed by Adam
parent 5008ecefaa
commit c74b3b9ab5
7 changed files with 1300 additions and 2 deletions

View File

@@ -114,8 +114,7 @@ public class WorldArea
return Integer.MAX_VALUE;
}
Point distances = getAxisDistances(other);
return Math.max(distances.getX(), distances.getY());
return distanceTo2D(other);
}
/**
@@ -129,6 +128,29 @@ public class WorldArea
return distanceTo(new WorldArea(other, 1, 1));
}
/**
* Computes the shortest distance to another area while ignoring the plane.
*
* @param other the passed area
* @return the distance
*/
public int distanceTo2D(WorldArea other)
{
Point distances = getAxisDistances(other);
return Math.max(distances.getX(), distances.getY());
}
/**
* Computes the shortest distance to a world coordinate.
*
* @param other the passed coordinate
* @return the distance
*/
public int distanceTo2D(WorldPoint other)
{
return distanceTo2D(new WorldArea(other, 1, 1));
}
/**
* Checks whether this area is within melee distance of another.
* <p>

View File

@@ -0,0 +1,454 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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.api.geometry;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.LinkedList;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
public class Geometry
{
/**
* Find the point where two lines intersect.
*
* @param x1 X coordinate of the first endpoint of the first line.
* @param y1 Y coordinate of the first endpoint of the first line.
* @param x2 X coordinate of the second endpoint of the first line.
* @param y2 Y coordinate of the second endpoint of the first line.
* @param x3 X coordinate of the first endpoint of the second line.
* @param y3 Y coordinate of the first endpoint of the second line.
* @param x4 X coordinate of the second endpoint of the second line.
* @param y4 Y coordinate of the second endpoint of the second line.
* @return The intersection point of the lines, or null if the lines don't intersect.
*/
public static Point2D.Float lineIntersectionPoint(
float x1, float y1, float x2, float y2,
float x3, float y3, float x4, float y4)
{
// https://stackoverflow.com/a/1968345
float p1x = x2 - x1;
float p1y = y2 - y1;
float p2x = x4 - x3;
float p2y = y4 - y3;
float s = (-p1y * (x1 - x3) + p1x * (y1 - y3)) / (-p2x * p1y + p1x * p2y);
float t = ( p2x * (y1 - y3) - p2y * (x1 - x3)) / (-p2x * p1y + p1x * p2y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1)
{
float x = x1 + (t * p1x);
float y = y1 + (t * p1y);
return new Point2D.Float(x, y);
}
// No intersection
return null;
}
/**
* Find the intersection points between a Shape and a line.
*
* @param shape The shape.
* @param x1 X coordinate of the first endpoint of the line.
* @param y1 Y coordinate of the first endpoint of the line.
* @param x2 X coordinate of the second endpoint of the line.
* @param y2 Y coordinate of the second endpoint of the line.
* @return A list with the intersection points.
*/
public static List<Point2D.Float> intersectionPoints(Shape shape, float x1, float y1, float x2, float y2)
{
List<Point2D.Float> intersections = new LinkedList<>();
PathIterator it = shape.getPathIterator(new AffineTransform());
float[] coords = new float[2];
float[] prevCoords = new float[2];
float[] start = new float[2];
while (!it.isDone())
{
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO)
{
start[0] = coords[0];
start[1] = coords[1];
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
else if (type == PathIterator.SEG_LINETO)
{
Point2D.Float intersection = lineIntersectionPoint(
prevCoords[0], prevCoords[1], coords[0], coords[1], x1, y1, x2, y2);
if (intersection != null)
{
intersections.add(intersection);
}
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
else if (type == PathIterator.SEG_CLOSE)
{
Point2D.Float intersection = lineIntersectionPoint(
coords[0], coords[1], start[0], start[1], x1, y1, x2, y2);
if (intersection != null)
{
intersections.add(intersection);
}
}
it.next();
}
return intersections;
}
/**
* Transforms the points in a path according to a method.
*
* @param it The iterator of the path to change the points on.
* @param method The method to use to transform the points. Takes a float[2] array with x and y coordinates as parameter.
* @return The transformed path.
*/
public static GeneralPath transformPath(PathIterator it, Consumer<float[]> method)
{
GeneralPath path = new GeneralPath();
float[] coords = new float[2];
while (!it.isDone())
{
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO)
{
method.accept(coords);
path.moveTo(coords[0], coords[1]);
}
else if (type == PathIterator.SEG_LINETO)
{
method.accept(coords);
path.lineTo(coords[0], coords[1]);
}
else if (type == PathIterator.SEG_CLOSE)
{
path.closePath();
}
it.next();
}
return path;
}
/**
* Transforms the points in a path according to a method.
*
* @param path The path to change the points on.
* @param method The method to use to transform the points. Takes a float[2] array with x and y coordinates as parameter.
* @return The transformed path.
*/
public static GeneralPath transformPath(GeneralPath path, Consumer<float[]> method)
{
return transformPath(path.getPathIterator(new AffineTransform()), method);
}
/**
* Splits a line into smaller segments and appends the segments to a path.
*
* @param path The path to append lines to.
* @param segmentLength The desired length to use for the segmented lines.
* @param x1 X coordinate of the first endpoint of the line.
* @param y1 Y coordinate of the first endpoint of the line.
* @param x2 X coordinate of the second endpoint of the line.
* @param y2 Y coordinate of the second endpoint of the line.
*/
private static void appendSegmentLines(GeneralPath path, float segmentLength,
float x1, float y1, float x2, float y2)
{
float x = x1;
float y = y1;
float angle = (float)Math.atan2(y2 - y1, x2 - x1);
float dx = (float)Math.cos(angle) * segmentLength;
float dy = (float)Math.sin(angle) * segmentLength;
float length = (float)Math.hypot(x2 - x1, y2 - y1);
int steps = (int)((length - 1e-4) / segmentLength);
for (int i = 0; i < steps; i++)
{
x += dx;
y += dy;
path.lineTo(x, y);
}
}
/**
* Splits a path into smaller segments.
* For example, calling this on a path with a line of length 6, with desired
* segment length of 2, would split the path into 3 consecutive lines of length 2.
*
* @param it The iterator of the path to modify.
* @param segmentLength The desired length to use for the segments.
* @return The modified path.
*/
public static GeneralPath splitIntoSegments(PathIterator it, float segmentLength)
{
GeneralPath newPath = new GeneralPath();
float[] prevCoords = new float[2];
float[] coords = new float[2];
float[] startCoords = new float[2];
while (!it.isDone())
{
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO)
{
startCoords[0] = coords[0];
startCoords[1] = coords[1];
newPath.moveTo(coords[0], coords[1]);
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
else if (type == PathIterator.SEG_LINETO)
{
appendSegmentLines(newPath, segmentLength, prevCoords[0], prevCoords[1], coords[0], coords[1]);
newPath.lineTo(coords[0], coords[1]);
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
else if (type == PathIterator.SEG_CLOSE)
{
appendSegmentLines(newPath, segmentLength, coords[0], coords[1], startCoords[0], startCoords[1]);
newPath.closePath();
}
it.next();
}
return newPath;
}
/**
* Splits a path into smaller segments.
* For example, calling this on a path with a line of length 6, with desired
* segment length of 2, would split the path into 3 consecutive lines of length 2.
*
* @param path The path to modify.
* @param segmentLength The desired length to use for the segments.
* @return The modified path.
*/
public static GeneralPath splitIntoSegments(GeneralPath path, float segmentLength)
{
return splitIntoSegments(path.getPathIterator(new AffineTransform()), segmentLength);
}
/**
* Removes lines from a path according to a method.
*
* @param it The iterator of the path to filter.
* @param method The method to use to decide which lines to remove. Takes two float[2] arrays with x and y coordinates of the endpoints of the line. Lines for which the predicate returns false are removed.
* @return The filtered path.
*/
public static GeneralPath filterPath(PathIterator it, BiPredicate<float[], float[]> method)
{
GeneralPath newPath = new GeneralPath();
float[] prevCoords = new float[2];
float[] coords = new float[2];
float[] start = new float[2];
boolean shouldMoveNext = false;
while (!it.isDone())
{
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO)
{
start[0] = coords[0];
start[1] = coords[1];
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
shouldMoveNext = true;
}
else if (type == PathIterator.SEG_LINETO)
{
if (method.test(prevCoords, coords))
{
if (shouldMoveNext)
{
newPath.moveTo(prevCoords[0], prevCoords[1]);
shouldMoveNext = false;
}
newPath.lineTo(coords[0], coords[1]);
}
else
{
shouldMoveNext = true;
}
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
else if (type == PathIterator.SEG_CLOSE)
{
if (shouldMoveNext)
{
newPath.moveTo(prevCoords[0], prevCoords[1]);
}
if (method.test(prevCoords, start))
{
newPath.lineTo(start[0], start[1]);
}
shouldMoveNext = false;
}
it.next();
}
return newPath;
}
/**
* Removes lines from a path according to a method.
*
* @param path The path to filter.
* @param method The method to use to decide which lines to remove. Takes two float[2] arrays with x and y coordinates of the endpoints of the line. Lines for which the predicate returns false are removed.
* @return The filtered path.
*/
public static GeneralPath filterPath(GeneralPath path, BiPredicate<float[], float[]> method)
{
return filterPath(path.getPathIterator(new AffineTransform()), method);
}
/**
* Removes lines from a path that lie outside the clipping area and cuts
* lines intersecting with the clipping area so the resulting lines
* lie within the clipping area.
*
* @param it The iterator of the path to clip.
* @param shape The clipping area to clip with.
* @return The clipped path.
*/
public static GeneralPath clipPath(PathIterator it, Shape shape)
{
GeneralPath newPath = new GeneralPath();
float[] prevCoords = new float[2];
float[] coords = new float[2];
float[] start = new float[2];
float[] nextMove = new float[2];
boolean shouldMove = false;
boolean wasInside = false;
while (!it.isDone())
{
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO)
{
start[0] = coords[0];
start[1] = coords[1];
wasInside = shape.contains(coords[0], coords[1]);
if (wasInside)
{
nextMove[0] = coords[0];
nextMove[1] = coords[1];
shouldMove = true;
}
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
else if (type == PathIterator.SEG_LINETO || type == PathIterator.SEG_CLOSE)
{
if (type == PathIterator.SEG_CLOSE)
{
coords[0] = start[0];
coords[1] = start[1];
}
List<Point2D.Float> intersections = intersectionPoints(shape, prevCoords[0], prevCoords[1], coords[0], coords[1]);
intersections.sort((a, b) ->
{
double diff = a.distance(prevCoords[0], prevCoords[1]) - b.distance(prevCoords[0], prevCoords[1]);
if (diff < 0)
{
return -1;
}
if (diff > 0)
{
return 1;
}
return 0;
});
for (Point2D.Float intersection : intersections)
{
if (wasInside)
{
if (shouldMove)
{
newPath.moveTo(nextMove[0], nextMove[1]);
shouldMove = false;
}
newPath.lineTo(intersection.getX(), intersection.getY());
}
else
{
nextMove[0] = intersection.x;
nextMove[1] = intersection.y;
shouldMove = true;
}
wasInside = !wasInside;
prevCoords[0] = intersection.x;
prevCoords[1] = intersection.y;
}
wasInside = shape.contains(coords[0], coords[1]);
if (wasInside)
{
if (shouldMove)
{
newPath.moveTo(nextMove[0], nextMove[1]);
shouldMove = false;
}
newPath.lineTo(coords[0], coords[1]);
}
else
{
nextMove[0] = coords[0];
nextMove[1] = coords[1];
shouldMove = true;
}
prevCoords[0] = coords[0];
prevCoords[1] = coords[1];
}
it.next();
}
return newPath;
}
/**
* Removes lines from a path that lie outside the clipping area and cuts
* lines intersecting with the clipping area so the resulting lines
* lie within the clipping area.
*
* @param path The path to clip.
* @param shape The clipping area to clip with.
* @return The clipped path.
*/
public static GeneralPath clipPath(GeneralPath path, Shape shape)
{
return clipPath(path.getPathIterator(new AffineTransform()), shape);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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.npcunaggroarea;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import lombok.Getter;
import lombok.Setter;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.ui.overlay.infobox.Timer;
class AggressionTimer extends Timer
{
@Getter
@Setter
private boolean visible;
AggressionTimer(Duration duration, BufferedImage image, Plugin plugin, boolean visible)
{
super(duration.toMillis(), ChronoUnit.MILLIS, image, plugin);
setTooltip("Time until NPCs become unaggressive");
this.visible = visible;
}
@Override
public Color getTextColor()
{
Duration timeLeft = Duration.between(Instant.now(), getEndTime());
if (timeLeft.getSeconds() < 60)
{
return Color.RED.brighter();
}
return Color.WHITE;
}
@Override
public boolean render()
{
return visible && super.render();
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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.npcunaggroarea;
import java.awt.Color;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("npcUnaggroArea")
public interface NpcAggroAreaConfig extends Config
{
String CONFIG_GROUP = "npcUnaggroArea";
String CONFIG_CENTER1 = "center1";
String CONFIG_CENTER2 = "center2";
String CONFIG_LOCATION = "location";
String CONFIG_DURATION = "duration";
@ConfigItem(
keyName = "npcUnaggroAlwaysActive",
name = "Always active",
description = "Always show this plugins overlays<br>Otherwise, they will only be shown when any NPC name matches the list",
position = 1
)
default boolean alwaysActive()
{
return false;
}
@ConfigItem(
keyName = "npcUnaggroNames",
name = "NPC names",
description = "Enter names of NPCs where you wish to use this plugin",
position = 2
)
default String npcNamePatterns()
{
return "";
}
@ConfigItem(
keyName = "npcUnaggroShowTimer",
name = "Show timer",
description = "Display a timer until NPCs become unaggressive",
position = 3
)
default boolean showTimer()
{
return true;
}
@ConfigItem(
keyName = "npcUnaggroShowAreaLines",
name = "Show area lines",
description = "Display lines, when walked past, the unaggressive timer resets",
position = 4
)
default boolean showAreaLines()
{
return false;
}
@ConfigItem(
keyName = "npcUnaggroAreaColor",
name = "Area lines colour",
description = "Choose colour to use for marking NPC unaggressive area",
position = 5
)
default Color aggroAreaColor()
{
return Color.YELLOW;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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.npcunaggroarea;
import com.google.inject.Inject;
import java.awt.Dimension;
import java.awt.Graphics2D;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayPriority;
import net.runelite.client.ui.overlay.components.LineComponent;
import net.runelite.client.ui.overlay.components.PanelComponent;
class NpcAggroAreaNotWorkingOverlay extends Overlay
{
private final NpcAggroAreaPlugin plugin;
private final PanelComponent panelComponent;
@Inject
private NpcAggroAreaNotWorkingOverlay(NpcAggroAreaPlugin plugin)
{
this.plugin = plugin;
panelComponent = new PanelComponent();
panelComponent.setPreferredSize(new Dimension(150, 0));
panelComponent.getChildren().add(LineComponent.builder()
.left("Unaggressive NPC timers will start working when you teleport far away or enter a dungeon.")
.build());
setPriority(OverlayPriority.LOW);
setPosition(OverlayPosition.TOP_LEFT);
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!plugin.isActive() || plugin.getSafeCenters()[1] != null)
{
return null;
}
return panelComponent.render(graphics);
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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.npcunaggroarea;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.GeneralPath;
import java.time.Instant;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.Perspective;
import net.runelite.api.Point;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.geometry.Geometry;
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.OverlayPriority;
class NpcAggroAreaOverlay extends Overlay
{
private static final int MAX_LOCAL_DRAW_LENGTH = 20 * Perspective.LOCAL_TILE_SIZE;
private final Client client;
private final NpcAggroAreaConfig config;
private final NpcAggroAreaPlugin plugin;
@Inject
private NpcAggroAreaOverlay(Client client, NpcAggroAreaConfig config, NpcAggroAreaPlugin plugin)
{
this.client = client;
this.config = config;
this.plugin = plugin;
setLayer(OverlayLayer.ABOVE_SCENE);
setPriority(OverlayPriority.LOW);
setPosition(OverlayPosition.DYNAMIC);
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!plugin.isActive() || plugin.getSafeCenters()[1] == null)
{
return null;
}
GeneralPath lines = plugin.getLinesToDisplay()[client.getPlane()];
if (lines == null)
{
return null;
}
Color outlineColor = config.aggroAreaColor();
AggressionTimer timer = plugin.getCurrentTimer();
if (timer == null || Instant.now().compareTo(timer.getEndTime()) < 0)
{
outlineColor = new Color(
outlineColor.getRed(),
outlineColor.getGreen(),
outlineColor.getBlue(),
100);
}
renderPath(graphics, lines, outlineColor);
return null;
}
private void renderPath(Graphics2D graphics, GeneralPath path, Color color)
{
LocalPoint playerLp = client.getLocalPlayer().getLocalLocation();
Rectangle viewArea = new Rectangle(
playerLp.getX() - MAX_LOCAL_DRAW_LENGTH,
playerLp.getY() - MAX_LOCAL_DRAW_LENGTH,
MAX_LOCAL_DRAW_LENGTH * 2,
MAX_LOCAL_DRAW_LENGTH * 2);
graphics.setColor(color);
graphics.setStroke(new BasicStroke(1));
path = Geometry.clipPath(path, viewArea);
path = Geometry.filterPath(path, (p1, p2) ->
Perspective.localToCanvas(client, new LocalPoint((int)p1[0], (int)p1[1]), client.getPlane()) != null &&
Perspective.localToCanvas(client, new LocalPoint((int)p2[0], (int)p2[1]), client.getPlane()) != null);
path = Geometry.transformPath(path, coords ->
{
Point point = Perspective.localToCanvas(client, new LocalPoint((int)coords[0], (int)coords[1]), client.getPlane());
coords[0] = point.getX();
coords[1] = point.getY();
});
graphics.draw(path);
}
}

View File

@@ -0,0 +1,474 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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.npcunaggroarea;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.inject.Provides;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.Constants;
import net.runelite.api.ItemID;
import net.runelite.api.NPC;
import net.runelite.api.NPCComposition;
import net.runelite.api.Perspective;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldArea;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ConfigChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.NpcSpawned;
import net.runelite.api.geometry.Geometry;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
import net.runelite.client.util.WildcardMatcher;
@Slf4j
@PluginDescriptor(
name = "Unaggressive NPC timer",
description = "Highlights the unaggressive area of NPCs nearby and timer until it becomes active",
tags = {"highlight", "lines", "unaggro", "aggro", "aggressive", "npcs", "area", "timer", "slayer"},
enabledByDefault = false
)
public class NpcAggroAreaPlugin extends Plugin
{
/*
How it works: The game remembers 2 tiles. When the player goes >10 steps
away from both tiles, the oldest one is moved to under the player and the
NPC aggression timer resets.
So to first figure out where the 2 tiles are, we wait until the player teleports
a long enough distance. At that point it's very likely that the player
moved out of the radius of both tiles, which resets one of them. The other
should reset shortly after as the player starts moving around.
*/
private static final int SAFE_AREA_RADIUS = 10;
private static final int UNKNOWN_AREA_RADIUS = SAFE_AREA_RADIUS * 2;
private static final int AGGRESSIVE_TIME_SECONDS = 600;
private static final Splitter NAME_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
private static final WorldArea WILDERNESS_ABOVE_GROUND = new WorldArea(2944, 3523, 448, 448, 0);
private static final WorldArea WILDERNESS_UNDERGROUND = new WorldArea(2944, 9918, 320, 442, 0);
@Inject
private Client client;
@Inject
private NpcAggroAreaConfig config;
@Inject
private NpcAggroAreaOverlay overlay;
@Inject
private NpcAggroAreaNotWorkingOverlay notWorkingOverlay;
@Inject
private OverlayManager overlayManager;
@Inject
private ItemManager itemManager;
@Inject
private InfoBoxManager infoBoxManager;
@Inject
private ConfigManager configManager;
@Getter
private final WorldPoint[] safeCenters = new WorldPoint[2];
@Getter
private final GeneralPath[] linesToDisplay = new GeneralPath[Constants.MAX_Z];
@Getter
private boolean active;
@Getter
private AggressionTimer currentTimer;
private WorldPoint lastPlayerLocation;
private WorldPoint previousUnknownCenter;
private boolean loggingIn;
private List<String> npcNamePatterns;
@Provides
NpcAggroAreaConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(NpcAggroAreaConfig.class);
}
@Override
protected void startUp() throws Exception
{
overlayManager.add(overlay);
overlayManager.add(notWorkingOverlay);
npcNamePatterns = NAME_SPLITTER.splitToList(config.npcNamePatterns());
}
@Override
protected void shutDown() throws Exception
{
removeTimer();
overlayManager.remove(overlay);
overlayManager.remove(notWorkingOverlay);
Arrays.fill(safeCenters, null);
lastPlayerLocation = null;
currentTimer = null;
loggingIn = false;
npcNamePatterns = null;
active = false;
Arrays.fill(linesToDisplay, null);
}
private Area generateSafeArea()
{
final Area area = new Area();
for (WorldPoint wp : safeCenters)
{
if (wp == null)
{
continue;
}
Polygon poly = new Polygon();
poly.addPoint(wp.getX() - SAFE_AREA_RADIUS, wp.getY() - SAFE_AREA_RADIUS);
poly.addPoint(wp.getX() - SAFE_AREA_RADIUS, wp.getY() + SAFE_AREA_RADIUS + 1);
poly.addPoint(wp.getX() + SAFE_AREA_RADIUS + 1, wp.getY() + SAFE_AREA_RADIUS + 1);
poly.addPoint(wp.getX() + SAFE_AREA_RADIUS + 1, wp.getY() - SAFE_AREA_RADIUS);
area.add(new Area(poly));
}
return area;
}
private void transformWorldToLocal(float[] coords)
{
final LocalPoint lp = LocalPoint.fromWorld(client, (int)coords[0], (int)coords[1]);
coords[0] = lp.getX() - Perspective.LOCAL_TILE_SIZE / 2f;
coords[1] = lp.getY() - Perspective.LOCAL_TILE_SIZE / 2f;
}
private void reevaluateActive()
{
if (currentTimer != null)
{
currentTimer.setVisible(active && config.showTimer());
}
calculateLinesToDisplay();
}
private void calculateLinesToDisplay()
{
if (!active || !config.showAreaLines())
{
Arrays.fill(linesToDisplay, null);
return;
}
Rectangle sceneRect = new Rectangle(
client.getBaseX() + 1, client.getBaseY() + 1,
Constants.SCENE_SIZE - 2, Constants.SCENE_SIZE - 2);
for (int i = 0; i < linesToDisplay.length; i++)
{
GeneralPath lines = new GeneralPath(generateSafeArea());
lines = Geometry.clipPath(lines, sceneRect);
lines = Geometry.splitIntoSegments(lines, 1);
lines = Geometry.transformPath(lines, this::transformWorldToLocal);
linesToDisplay[i] = lines;
}
}
private void removeTimer()
{
infoBoxManager.removeInfoBox(currentTimer);
currentTimer = null;
}
private void createTimer(Duration duration)
{
removeTimer();
BufferedImage image = itemManager.getImage(ItemID.ENSOULED_DEMON_HEAD);
currentTimer = new AggressionTimer(duration, image, this, active && config.showTimer());
infoBoxManager.addInfoBox(currentTimer);
}
private void resetTimer()
{
createTimer(Duration.ofSeconds(AGGRESSIVE_TIME_SECONDS));
}
private static boolean isInWilderness(WorldPoint location)
{
return WILDERNESS_ABOVE_GROUND.distanceTo2D(location) == 0 || WILDERNESS_UNDERGROUND.distanceTo2D(location) == 0;
}
private boolean isNpcMatch(NPC npc)
{
NPCComposition composition = npc.getTransformedComposition();
if (composition == null)
{
return false;
}
if (Strings.isNullOrEmpty(composition.getName()))
{
return false;
}
// Most NPCs stop aggroing when the player has more than double
// its combat level.
int playerLvl = client.getLocalPlayer().getCombatLevel();
int npcLvl = composition.getCombatLevel();
String npcName = composition.getName().toLowerCase();
if (npcLvl > 0 && playerLvl > npcLvl * 2 && !isInWilderness(npc.getWorldLocation()))
{
return false;
}
for (String pattern : npcNamePatterns)
{
if (WildcardMatcher.matches(pattern, npcName))
{
return true;
}
}
return false;
}
private void checkAreaNpcs(final NPC... npcs)
{
for (NPC npc : npcs)
{
if (npc == null)
{
continue;
}
if (isNpcMatch(npc))
{
active = true;
break;
}
}
reevaluateActive();
}
private void recheckActive()
{
active = config.alwaysActive();
checkAreaNpcs(client.getCachedNPCs());
}
@Subscribe
public void onNpcSpawned(NpcSpawned event)
{
if (config.alwaysActive())
{
return;
}
checkAreaNpcs(event.getNpc());
}
@Subscribe
public void onGameTick(GameTick event)
{
WorldPoint newLocation = client.getLocalPlayer().getWorldLocation();
if (lastPlayerLocation != null)
{
if (safeCenters[1] == null && newLocation.distanceTo2D(lastPlayerLocation) > SAFE_AREA_RADIUS * 4)
{
safeCenters[0] = null;
safeCenters[1] = newLocation;
resetTimer();
calculateLinesToDisplay();
// We don't know where the previous area was, so if the player e.g.
// entered a dungeon and then goes back out, he/she may enter the previous
// area which is unknown and would make the plugin inaccurate
previousUnknownCenter = lastPlayerLocation;
}
}
if (safeCenters[0] == null && previousUnknownCenter != null &&
previousUnknownCenter.distanceTo2D(newLocation) <= UNKNOWN_AREA_RADIUS)
{
// Player went back to their previous unknown area before the 2nd
// center point was found, which means we don't know where it is again.
safeCenters[1] = null;
removeTimer();
calculateLinesToDisplay();
}
if (safeCenters[1] != null)
{
if (Arrays.stream(safeCenters).noneMatch(
x -> x != null && x.distanceTo2D(newLocation) <= SAFE_AREA_RADIUS))
{
safeCenters[0] = safeCenters[1];
safeCenters[1] = newLocation;
resetTimer();
calculateLinesToDisplay();
previousUnknownCenter = null;
}
}
lastPlayerLocation = newLocation;
}
@Subscribe
public void onConfigChanged(ConfigChanged event)
{
String key = event.getKey();
switch (key)
{
case "npcUnaggroAlwaysActive":
recheckActive();
break;
case "npcUnaggroShowTimer":
if (currentTimer != null)
{
currentTimer.setVisible(active && config.showTimer());
}
break;
case "npcUnaggroCollisionDetection":
case "npcUnaggroShowAreaLines":
calculateLinesToDisplay();
break;
case "npcUnaggroNames":
npcNamePatterns = NAME_SPLITTER.splitToList(config.npcNamePatterns());
recheckActive();
break;
}
}
private void loadConfig()
{
safeCenters[0] = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1, WorldPoint.class);
safeCenters[1] = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2, WorldPoint.class);
lastPlayerLocation = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION, WorldPoint.class);
Duration timeLeft = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION, Duration.class);
if (timeLeft != null)
{
createTimer(timeLeft);
}
}
private void resetConfig()
{
configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1);
configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2);
configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION);
configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION);
}
private void saveConfig()
{
if (safeCenters[0] == null || safeCenters[1] == null || lastPlayerLocation == null || currentTimer == null)
{
resetConfig();
}
else
{
configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1, safeCenters[0]);
configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2, safeCenters[1]);
configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION, lastPlayerLocation);
configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION, Duration.between(Instant.now(), currentTimer.getEndTime()));
}
}
private void onLogin()
{
loadConfig();
resetConfig();
WorldPoint newLocation = client.getLocalPlayer().getWorldLocation();
assert newLocation != null;
// If the player isn't at the location he/she logged out at,
// the safe unaggro area probably changed, and should be disposed.
if (lastPlayerLocation == null || newLocation.distanceTo(lastPlayerLocation) != 0)
{
safeCenters[0] = null;
safeCenters[1] = null;
lastPlayerLocation = newLocation;
}
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
switch (event.getGameState())
{
case LOGGED_IN:
if (loggingIn)
{
loggingIn = false;
onLogin();
}
recheckActive();
break;
case LOGGING_IN:
loggingIn = true;
break;
case LOGIN_SCREEN:
if (lastPlayerLocation != null)
{
saveConfig();
}
safeCenters[0] = null;
safeCenters[1] = null;
lastPlayerLocation = null;
break;
}
}
}