From 049ac9b22a7dfae79bbdb7a72d2c7e7e53532b19 Mon Sep 17 00:00:00 2001 From: GeChallengeM <52377234+GeChallengeM@users.noreply.github.com> Date: Mon, 1 Jul 2019 07:08:59 +0200 Subject: [PATCH] Add a new function to the WorldArea api. Add npcstatus plugin. (#833) * Add a new function to the WorldArea api. Add npcstatus plugin. * Ganom adds his mentioned changes (thanks!), and a fix for splash flinching is added. --- .../net/runelite/api/coords/WorldArea.java | 19 ++ .../plugins/npcstatus/MemorizedNPC.java | 84 +++++++ .../plugins/npcstatus/NpcStatusConfig.java | 43 ++++ .../plugins/npcstatus/NpcStatusOverlay.java | 89 +++++++ .../plugins/npcstatus/NpcStatusPlugin.java | 231 ++++++++++++++++++ 5 files changed, 466 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/MemorizedNPC.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusPlugin.java diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java index 105bf9fa89..7156045eb8 100644 --- a/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java +++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java @@ -184,6 +184,25 @@ public class WorldArea return isInMeleeDistance(new WorldArea(other, 1, 1)); } + /** + * Checks whether this area is within melee distance of another without blocking in-between. + * + * @param client the client to test in + * @param other the other area + * @return true if in melee distance without blocking, false otherwise + */ + public boolean canMelee(Client client, WorldArea other) + { + if (isInMeleeDistance(other)) + { + Point p1 = this.getComparisonPoint(other); + Point p2 = other.getComparisonPoint(this); + WorldArea w1 = new WorldArea(p1.getX(), p1.getY() , 1, 1, this.getPlane()); + return (w1.canTravelInDirection(client, p2.getX() - p1.getX(), p2.getY() - p1.getY())); + } + return false; + } + /** * Checks whether this area intersects with another. * diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/MemorizedNPC.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/MemorizedNPC.java new file mode 100644 index 0000000000..4a746e63db --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/MemorizedNPC.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019, GeChallengeM + * 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.npcstatus; + +import java.awt.Color; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.NPC; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.Actor; + +@Getter +class MemorizedNPC +{ + private NPC npc; + private int npcIndex; + private String npcName; + private int attackSpeed; + @Setter + private int combatTimerEnd; + @Setter + private int timeLeft; + @Setter + private int flinchTimerEnd; + @Setter + private Status status; + @Setter + private WorldArea lastnpcarea; + @Setter + private Actor lastinteracted; + @Setter + private int lastspotanimation; + + MemorizedNPC(NPC npc, int attackSpeed, WorldArea worldArea) + { + this.npc = npc; + this.npcIndex = npc.getIndex(); + this.npcName = npc.getName(); + this.attackSpeed = attackSpeed; + this.combatTimerEnd = -1; + this.flinchTimerEnd = -1; + this.timeLeft = 0; + this.status = Status.OUT_OF_COMBAT; + this.lastnpcarea = worldArea; + this.lastinteracted = null; + this.lastspotanimation = -1; + } + + @Getter + @AllArgsConstructor + enum Status + { + FLINCHING("Flinching", Color.GREEN), + IN_COMBAT_DELAY("In Combat Delay", Color.ORANGE), + IN_COMBAT("In Combat", Color.RED), + OUT_OF_COMBAT("Out of Combat", Color.BLUE); + + private String name; + private Color color; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusConfig.java new file mode 100644 index 0000000000..bf0495fae5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusConfig.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, GeChallengeM + * 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.npcstatus; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("npcstatus") +public interface NpcStatusConfig extends Config +{ + @ConfigItem( + keyName = "AttackRange", + name = "NPC Attack range", + description = "The attack range of the NPC" + ) + default int getRange() + { + return 1; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusOverlay.java new file mode 100644 index 0000000000..7b948f24c0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusOverlay.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018, GeChallengeM + * 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.npcstatus; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Point; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; + +public class NpcStatusOverlay extends Overlay +{ + private final Client client; + private final NpcStatusPlugin plugin; + + @Inject + NpcStatusOverlay(Client client, NpcStatusPlugin plugin) + { + this.client = client; + this.plugin = plugin; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + } + + @Override + public Dimension render(Graphics2D graphics) + { + for (MemorizedNPC npc : plugin.getMemorizedNPCs()) + { + if (npc.getNpc().getInteracting() == null) + { + continue; + } + if (npc.getNpc().getInteracting() == client.getLocalPlayer() || client.getLocalPlayer().getInteracting() == npc.getNpc()) + { + switch (npc.getStatus()) + { + case FLINCHING: + npc.setTimeLeft(Math.max(0, npc.getFlinchTimerEnd() - client.getTickCount())); + break; + case IN_COMBAT_DELAY: + npc.setTimeLeft(Math.max(0, npc.getCombatTimerEnd() - client.getTickCount() - 7)); + break; + case IN_COMBAT: + npc.setTimeLeft(Math.max(0, npc.getCombatTimerEnd() - client.getTickCount())); + break; + case OUT_OF_COMBAT: + default: + npc.setTimeLeft(0); + break; + } + + Point textLocation = npc.getNpc().getCanvasTextLocation(graphics, Integer.toString(npc.getTimeLeft()), npc.getNpc().getLogicalHeight() + 40); + + if (textLocation != null) + { + OverlayUtil.renderTextLocation(graphics, textLocation, Integer.toString(npc.getTimeLeft()), npc.getStatus().getColor()); + } + } + } + return null; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusPlugin.java new file mode 100644 index 0000000000..0f1a59a17a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcstatus/NpcStatusPlugin.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2019, GeChallengeM + * 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.npcstatus; + +import com.google.inject.Provides; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.GraphicID; +import net.runelite.api.Hitsplat; +import net.runelite.api.NPC; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.NPCManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; + +@Slf4j +@PluginDescriptor( + name = "NPC Status Timer", + description = "Adds a timer on NPC's for their attacks and flinching.", + tags = {"flinch", "npc"}, + enabledByDefault = false +) +public class NpcStatusPlugin extends Plugin +{ + @Getter(AccessLevel.PACKAGE) + private final Set memorizedNPCs = new HashSet<>(); + @Inject + private Client client; + @Inject + private OverlayManager overlayManager; + @Inject + private ItemManager itemManager; + @Inject + private NPCManager npcManager; + @Inject + private NpcStatusConfig config; + @Inject + private NpcStatusOverlay npcStatusOverlay; + @Getter(AccessLevel.PACKAGE) + private Instant lastTickUpdate; + private WorldArea lastPlayerLocation; + + @Provides + NpcStatusConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(NpcStatusConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(npcStatusOverlay); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(npcStatusOverlay); + memorizedNPCs.clear(); + } + + @Subscribe + public void onNpcSpawned(NpcSpawned npcSpawned) + { + final NPC npc = npcSpawned.getNpc(); + final String npcName = npc.getName(); + + if (npcName == null || !Arrays.asList(npc.getDefinition().getActions()).contains("Attack")) + { + return; + } + memorizedNPCs.add(new MemorizedNPC(npc, npcManager.getAttackSpeed(npc.getId()), npc.getWorldArea())); + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + final NPC npc = npcDespawned.getNpc(); + memorizedNPCs.removeIf(c -> c.getNpc() == npc); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOGIN_SCREEN || + event.getGameState() == GameState.HOPPING) + { + memorizedNPCs.clear(); + } + } + + @Subscribe + public void onHitsplatApplied(HitsplatApplied event) + { + if (event.getActor().getInteracting() != client.getLocalPlayer()) + { + return; + } + final Hitsplat hitsplat = event.getHitsplat(); + if (hitsplat.getHitsplatType() == Hitsplat.HitsplatType.DAMAGE || hitsplat.getHitsplatType() == Hitsplat.HitsplatType.BLOCK) + { + if (event.getActor() instanceof NPC) + { + for (MemorizedNPC mn : memorizedNPCs) + { + if (mn.getStatus() == MemorizedNPC.Status.OUT_OF_COMBAT || (mn.getStatus() == MemorizedNPC.Status.IN_COMBAT && mn.getCombatTimerEnd() - client.getTickCount() < 1) || mn.getLastinteracted() == null) + { + mn.setStatus(MemorizedNPC.Status.FLINCHING); + mn.setCombatTimerEnd(-1); + mn.setFlinchTimerEnd(client.getTickCount() + mn.getAttackSpeed() / 2 + 1); + } + } + } + } + } + + private void checkStatus() + { + for (MemorizedNPC npc : memorizedNPCs) + { + final int ATTACK_SPEED = npc.getAttackSpeed(); + final double CombatTime = npc.getCombatTimerEnd() - client.getTickCount(); + final double FlinchTime = npc.getFlinchTimerEnd() - client.getTickCount(); + if (npc.getNpc().getWorldArea() == null) + { + continue; + } + if (npc.getNpc().getInteracting() == client.getLocalPlayer()) + { + if (npc.getLastspotanimation() == GraphicID.SPLASH && npc.getNpc().getSpotAnimation() == GraphicID.SPLASH) //For splash flinching + { + npc.setLastspotanimation(-1); + if ((npc.getStatus() == MemorizedNPC.Status.OUT_OF_COMBAT ) || npc.getLastinteracted() == null) + { + npc.setStatus(MemorizedNPC.Status.FLINCHING); + npc.setCombatTimerEnd(-1); + npc.setFlinchTimerEnd(client.getTickCount() + ATTACK_SPEED / 2 + 1); + npc.setLastnpcarea(npc.getNpc().getWorldArea()); + npc.setLastinteracted(npc.getNpc().getInteracting()); + continue; + } + } + //Checks: will the NPC attack this tick? + if (((npc.getNpc().getWorldArea().canMelee(client, lastPlayerLocation) && config.getRange() == 1) //Separate mechanics for meleerange-only NPC's because they have extra collisiondata checks (fences etc.) and can't attack diagonally + || (lastPlayerLocation.hasLineOfSightTo(client, npc.getNpc().getWorldArea()) && npc.getNpc().getWorldArea().distanceTo(lastPlayerLocation) <= config.getRange() && config.getRange() > 1)) + && ((npc.getStatus() != MemorizedNPC.Status.FLINCHING && CombatTime < 9) || (npc.getStatus() == MemorizedNPC.Status.FLINCHING && FlinchTime < 2)) + && npc.getNpc().getAnimation() != -1 //Failsafe, attacking NPC's always have an animation. + && !(npc.getLastnpcarea().distanceTo(lastPlayerLocation) == 0 && npc.getLastnpcarea() != npc.getNpc().getWorldArea())) //Weird mechanic: NPC's can't attack on the tick they do a random move + { + npc.setCombatTimerEnd(client.getTickCount() + ATTACK_SPEED + 8); + npc.setStatus(MemorizedNPC.Status.IN_COMBAT_DELAY); + npc.setLastnpcarea(npc.getNpc().getWorldArea()); + npc.setLastspotanimation(npc.getNpc().getSpotAnimation()); + npc.setLastinteracted(npc.getNpc().getInteracting()); + continue; + } + } + switch (npc.getStatus()) + { + case IN_COMBAT: + if (CombatTime < 2) + { + npc.setStatus(MemorizedNPC.Status.OUT_OF_COMBAT); + } + break; + case IN_COMBAT_DELAY: + if (CombatTime < 9) + { + npc.setStatus(MemorizedNPC.Status.IN_COMBAT); + } + break; + case FLINCHING: + if (FlinchTime < 2) + { + npc.setStatus(MemorizedNPC.Status.IN_COMBAT); + npc.setCombatTimerEnd(client.getTickCount() + 8); + } + } + npc.setLastnpcarea(npc.getNpc().getWorldArea()); + npc.setLastspotanimation(npc.getNpc().getSpotAnimation()); + npc.setLastinteracted(npc.getNpc().getInteracting()); + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + lastTickUpdate = Instant.now(); + checkStatus(); + lastPlayerLocation = client.getLocalPlayer().getWorldArea(); + } +}