diff --git a/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java b/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java index 1a51afea3f..1251db7350 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java +++ b/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java @@ -24,7 +24,11 @@ */ package net.runelite.client.game; +import com.google.common.collect.ImmutableMap; +import com.google.gson.stream.JsonReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -42,12 +46,14 @@ public class NPCManager { private final OkHttpClient okHttpClient; private Map npcMap = Collections.emptyMap(); + private ImmutableMap statsMap; @Inject private NPCManager(OkHttpClient okHttpClient, ScheduledExecutorService scheduledExecutorService) { this.okHttpClient = okHttpClient; scheduledExecutorService.execute(this::loadNpcs); + loadStats(); } @Nullable @@ -74,4 +80,45 @@ public class NPCManager log.warn("error loading npc stats", e); } } + + private void loadStats() + { + try (JsonReader reader = new JsonReader(new InputStreamReader(NPCManager.class.getResourceAsStream("/npc_stats.json"), StandardCharsets.UTF_8))) + { + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(2821); + reader.beginObject(); + + while (reader.hasNext()) + { + builder.put( + Integer.parseInt(reader.nextName()), + NPCStats.NPC_STATS_TYPE_ADAPTER.read(reader) + ); + } + + reader.endObject(); + statsMap = builder.build(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * Returns the attack speed for target NPC ID. + * + * @param npcId NPC id + * @return attack speed in game ticks for NPC ID. + */ + public int getAttackSpeed(final int npcId) + { + final NPCStats s = statsMap.get(npcId); + if (s == null || s.getAttackSpeed() == -1) + { + return -1; + } + + return s.getAttackSpeed(); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java b/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java new file mode 100644 index 0000000000..49a5f11002 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2019, TheStonedTurtle + * 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.game; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(builderClassName = "Builder") +public class NPCStats +{ + private final String name; + + private final int hitpoints; + private final int combatLevel; + private final int slayerLevel; + private final int attackSpeed; + + private final int attackLevel; + private final int strengthLevel; + private final int defenceLevel; + private final int rangeLevel; + private final int magicLevel; + + private final int stab; + private final int slash; + private final int crush; + private final int range; + private final int magic; + + private final int stabDef; + private final int slashDef; + private final int crushDef; + private final int rangeDef; + private final int magicDef; + + private final int bonusAttack; + private final int bonusStrength; + private final int bonusRangeStrength; + private final int bonusMagicDamage; + + private final boolean poisonImmune; + private final boolean venomImmune; + + private final boolean dragon; + private final boolean demon; + private final boolean undead; + + /** + * Based off the formula found here: http://services.runescape.com/m=forum/c=PLuJ4cy6gtA/forums.ws?317,318,712,65587452,209,337584542#209 + * + * @return bonus XP modifier + */ + public double calculateXpModifier() + { + final double averageLevel = Math.floor((attackLevel + strengthLevel + defenceLevel + hitpoints) / 4); + final double averageDefBonus = Math.floor((stabDef + slashDef + crushDef) / 3); + + return (1 + Math.floor(averageLevel * (averageDefBonus + bonusStrength + bonusAttack) / 5120) / 40); + } + + // Because this class is here we can't add the TypeAdapter to gson (easily) + // doesn't mean we can't use one to do it a bit quicker + public static final TypeAdapter NPC_STATS_TYPE_ADAPTER = new TypeAdapter<>() + { + @Override + public void write(JsonWriter out, NPCStats value) + { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public NPCStats read(JsonReader in) throws IOException + { + in.beginObject(); + NPCStats.Builder builder = NPCStats.builder(); + + // Name is the only one that's guaranteed + in.skipValue(); + builder.name(in.nextString()); + + while (in.hasNext()) + { + switch (in.nextName()) + { + case "hitpoints": + builder.hitpoints(in.nextInt()); + break; + case "combatLevel": + builder.combatLevel(in.nextInt()); + break; + case "slayerLevel": + builder.slayerLevel(in.nextInt()); + break; + case "attackSpeed": + builder.attackSpeed(in.nextInt()); + break; + case "attackLevel": + builder.attackLevel(in.nextInt()); + break; + case "strengthLevel": + builder.strengthLevel(in.nextInt()); + break; + case "defenceLevel": + builder.defenceLevel(in.nextInt()); + break; + case "rangeLevel": + builder.rangeLevel(in.nextInt()); + break; + case "magicLevel": + builder.magicLevel(in.nextInt()); + break; + case "stab": + builder.stab(in.nextInt()); + break; + case "slash": + builder.slash(in.nextInt()); + break; + case "crush": + builder.crush(in.nextInt()); + break; + case "range": + builder.range(in.nextInt()); + break; + case "magic": + builder.magic(in.nextInt()); + break; + case "stabDef": + builder.stabDef(in.nextInt()); + break; + case "slashDef": + builder.slashDef(in.nextInt()); + break; + case "crushDef": + builder.crushDef(in.nextInt()); + break; + case "rangeDef": + builder.rangeDef(in.nextInt()); + break; + case "magicDef": + builder.magicDef(in.nextInt()); + break; + case "bonusAttack": + builder.bonusAttack(in.nextInt()); + break; + case "bonusStrength": + builder.bonusStrength(in.nextInt()); + break; + case "bonusRangeStrength": + builder.bonusRangeStrength(in.nextInt()); + break; + case "bonusMagicDamage": + builder.bonusMagicDamage(in.nextInt()); + break; + case "poisonImmune": + builder.poisonImmune(in.nextBoolean()); + break; + case "venomImmune": + builder.venomImmune(in.nextBoolean()); + break; + case "dragon": + builder.dragon(in.nextBoolean()); + break; + case "demon": + builder.demon(in.nextBoolean()); + break; + case "undead": + builder.undead(in.nextBoolean()); + break; + } + } + + in.endObject(); + return builder.build(); + } + }; +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java index 382282b92e..9564bf40f7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java @@ -165,10 +165,16 @@ class PluginListPanel extends PluginPanel setLayout(new BorderLayout()); setBackground(ColorScheme.DARK_GRAY_COLOR); + JButton externalPluginOPRSButton = new JButton("OpenOSRS Hub"); + externalPluginOPRSButton.setBorder(new EmptyBorder(5, 5, 5, 5)); + externalPluginOPRSButton.setLayout(new BorderLayout(0, BORDER_OFFSET)); + externalPluginOPRSButton.addActionListener(l -> muxer.pushState(pluginHubPanelProvider.get())); + JPanel topPanel = new JPanel(); topPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); topPanel.setLayout(new BorderLayout(0, BORDER_OFFSET)); topPanel.add(searchBar, BorderLayout.CENTER); + topPanel.add(externalPluginOPRSButton, BorderLayout.SOUTH); add(topPanel, BorderLayout.NORTH); mainPanel = new FixedWidthPanel(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/MemorizedNPC.java b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/MemorizedNPC.java new file mode 100644 index 0000000000..7c8e84c6f2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/MemorizedNPC.java @@ -0,0 +1,82 @@ +/* + * 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.nextattack; + +import java.awt.Color; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Actor; +import net.runelite.api.NPC; +import net.runelite.api.coords.WorldArea; + +@Getter(AccessLevel.PACKAGE) +class MemorizedNPC +{ + private NPC npc; + private int npcIndex; + private String npcName; + private int attackSpeed; + @Setter(AccessLevel.PACKAGE) + private int combatTimerEnd; + @Setter(AccessLevel.PACKAGE) + private int timeLeft; + @Setter(AccessLevel.PACKAGE) + private int flinchTimerEnd; + @Setter(AccessLevel.PACKAGE) + private Status status; + @Setter(AccessLevel.PACKAGE) + private WorldArea lastnpcarea; + @Setter(AccessLevel.PACKAGE) + private Actor lastinteracted; + + MemorizedNPC(final NPC npc, final int attackSpeed, final 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; + } + + @Getter(AccessLevel.PACKAGE) + @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/nextattack/NextAttackConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackConfig.java new file mode 100644 index 0000000000..f1f77d8b8d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackConfig.java @@ -0,0 +1,91 @@ +/* + * 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.nextattack; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; + +@ConfigGroup("nextattack") +public interface NextAttackConfig extends Config +{ + @ConfigSection( + name = "Attack range", + description = "", + position = 1 + ) + String rangeTitle = "Attack range"; + + @Range( + min = 1 + ) + @ConfigItem( + keyName = "AttackRange", + name = "NPC attack range", + description = "The attack range of the NPC.", + position = 2, + section = rangeTitle + ) + default int getRange() + { + return 1; + } + + @ConfigSection( + name = "Attack speed", + description = "", + position = 3 + ) + String speedTitle = "Attack speed"; + + @ConfigItem( + keyName = "CustomAttSpeedEnabled", + name = "Use custom speed", + description = "Use this if the timer is wrong.", + position = 4, + section = speedTitle + ) + default boolean isCustomAttSpeed() + { + return false; + } + + @Range( + min = 1 + ) + @ConfigItem( + keyName = "CustomAttSpeed", + name = "Custom NPC att speed", + description = "The attack speed of the NPC (amount of ticks between their attacks).", + position = 5, + section = speedTitle + ) + default int getCustomAttSpeed() + { + return 4; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackOverlay.java new file mode 100644 index 0000000000..6ad8fdf3ca --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackOverlay.java @@ -0,0 +1,87 @@ +/* + * 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.nextattack; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import javax.inject.Singleton; +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; + +@Singleton +public class NextAttackOverlay extends Overlay +{ + private final Client client; + private final NextAttackPlugin plugin; + + @Inject + NextAttackOverlay(final Client client, final NextAttackPlugin 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() == 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/nextattack/NextAttackPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackPlugin.java new file mode 100644 index 0000000000..5825892dc7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/nextattack/NextAttackPlugin.java @@ -0,0 +1,266 @@ +/* + * 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.nextattack; + +import com.google.inject.Provides; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +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.GraphicChanged; +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.NPCManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; + +@PluginDescriptor( + name = "Next Attack Timer", + enabledByDefault = false, + description = "Adds a timer on NPC's for their projected next attack", + tags = {"openosrs", "flinch", "npc"} +) +public class NextAttackPlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private OverlayManager overlayManager; + + @Inject + private NPCManager npcManager; + + @Inject + private NextAttackConfig config; + + @Inject + private NextAttackOverlay npcStatusOverlay; + + @Getter(AccessLevel.PACKAGE) + private final Set memorizedNPCs = new HashSet<>(); + + private WorldArea lastPlayerLocation; + + @Provides + NextAttackConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(NextAttackConfig.class); + } + + @Override + protected void startUp() + { + overlayManager.add(npcStatusOverlay); + } + + @Override + protected void shutDown() + { + overlayManager.remove(npcStatusOverlay); + memorizedNPCs.clear(); + } + + @Subscribe + private void onNpcSpawned(NpcSpawned npcSpawned) + { + final NPC npc = npcSpawned.getNpc(); + final String npcName = npc.getName(); + + if (npcName == null || !Arrays.asList(npc.getComposition().getActions()).contains("Attack")) + { + return; + } + int AttackSpeed = npcManager.getAttackSpeed(npc.getId()); + if (AttackSpeed == 0) + { + AttackSpeed = 4; + } + memorizedNPCs.add(new MemorizedNPC(npc, AttackSpeed, npc.getWorldArea())); + } + + @Subscribe + private void onNpcDespawned(NpcDespawned npcDespawned) + { + final NPC npc = npcDespawned.getNpc(); + memorizedNPCs.removeIf(c -> c.getNpc() == npc); + } + + @Subscribe + private void onGameStateChanged(GameStateChanged event) + { + if (event.getGameState() == GameState.LOGIN_SCREEN || + event.getGameState() == GameState.HOPPING) + { + memorizedNPCs.clear(); + } + } + + @Subscribe + private void onHitsplatApplied(HitsplatApplied event) + { + if (event.getActor().getInteracting() != client.getLocalPlayer()) + { + return; + } + final Hitsplat hitsplat = event.getHitsplat(); + if ((hitsplat.getHitsplatType() == Hitsplat.HitsplatType.DAMAGE_ME || hitsplat.getHitsplatType() == Hitsplat.HitsplatType.BLOCK_ME) && event.getActor() instanceof NPC) + { + for (MemorizedNPC mn : memorizedNPCs) + { + if (mn.getNpcIndex() != ((NPC) event.getActor()).getIndex()) + { + continue; + } + 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); + if (config.isCustomAttSpeed()) + { + mn.setFlinchTimerEnd(client.getTickCount() + config.getCustomAttSpeed() / 2 + 1); + } + else + { + mn.setFlinchTimerEnd(client.getTickCount() + mn.getAttackSpeed() / 2 + 1); + } + } + } + } + + } + + @Subscribe + private void onGraphicChanged(GraphicChanged event) + { + if ((event.getActor().getGraphic() == GraphicID.SPLASH) && event.getActor() instanceof NPC) + { + for (MemorizedNPC mn : memorizedNPCs) + { + if (mn.getNpcIndex() != ((NPC) event.getActor()).getIndex()) + { + continue; + } + if (mn.getStatus() == MemorizedNPC.Status.OUT_OF_COMBAT || (mn.getStatus() == MemorizedNPC.Status.IN_COMBAT && mn.getCombatTimerEnd() - client.getTickCount() < 2) || event.getActor().getInteracting() == null) + { + mn.setStatus(MemorizedNPC.Status.FLINCHING); + mn.setCombatTimerEnd(-1); + if (config.isCustomAttSpeed()) + { + mn.setFlinchTimerEnd(client.getTickCount() + config.getCustomAttSpeed() / 2 + 2); + } + else + { + mn.setFlinchTimerEnd(client.getTickCount() + mn.getAttackSpeed() / 2 + 2); + } + } + } + } + } + + private void checkStatus() + { + if (lastPlayerLocation == null) + { + return; + } + for (MemorizedNPC npc : memorizedNPCs) + { + 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()) + { + //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.setStatus(MemorizedNPC.Status.IN_COMBAT_DELAY); + npc.setLastnpcarea(npc.getNpc().getWorldArea()); + npc.setLastinteracted(npc.getNpc().getInteracting()); + if (config.isCustomAttSpeed()) + { + npc.setCombatTimerEnd(client.getTickCount() + config.getCustomAttSpeed() + 8); + } + else + { + npc.setCombatTimerEnd(client.getTickCount() + npc.getAttackSpeed() + 8); + } + 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.setLastinteracted(npc.getNpc().getInteracting()); + } + } + + @Subscribe + private void onGameTick(GameTick event) + { + checkStatus(); + lastPlayerLocation = client.getLocalPlayer().getWorldArea(); + } +} \ No newline at end of file