From 5ef2fef60bb78f18f821c901565dc3f09747e4ea Mon Sep 17 00:00:00 2001 From: TheStonedTurtle Date: Fri, 12 Apr 2019 06:41:27 -0700 Subject: [PATCH 1/5] Add npc stats to NPCManager & remove npc_health.json Also updates all plugins that relied on the old NPCManager method --- .../net/runelite/http/api/npc/NPCClient.java | 79 ++ .../net/runelite/http/api/npc/NPCStats.java | 75 ++ .../net/runelite/client/game/NPCManager.java | 84 +- .../opponentinfo/OpponentInfoOverlay.java | 2 +- .../plugins/slayer/TargetWeaknessOverlay.java | 4 +- .../plugins/xptracker/XpTrackerPlugin.java | 4 +- .../src/main/resources/npc_health.json | 1161 ----------------- 7 files changed, 224 insertions(+), 1185 deletions(-) create mode 100644 http-api/src/main/java/net/runelite/http/api/npc/NPCClient.java create mode 100644 http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java delete mode 100644 runelite-client/src/main/resources/npc_health.json diff --git a/http-api/src/main/java/net/runelite/http/api/npc/NPCClient.java b/http-api/src/main/java/net/runelite/http/api/npc/NPCClient.java new file mode 100644 index 0000000000..f1104bb791 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/npc/NPCClient.java @@ -0,0 +1,79 @@ +/* + * 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.http.api.npc; + +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.Map; +import net.runelite.http.api.RuneLiteAPI; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NPCClient +{ + private static final Logger logger = LoggerFactory.getLogger(NPCClient.class); + + public Map getStats() throws IOException + { + final HttpUrl.Builder urlBuilder = RuneLiteAPI.getStaticBase().newBuilder() + .addPathSegment("npc") + .addPathSegment("stats.min.json"); + + final HttpUrl url = urlBuilder.build(); + + logger.debug("Built URI: {}", url); + + final Request request = new Request.Builder() + .url(url) + .build(); + + try (final Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + logger.warn("Error looking up npc stats: {}", response); + return null; + } + + InputStream in = response.body().byteStream(); + final Type typeToken = new TypeToken>() + { + }.getType(); + + return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), typeToken); + } + catch (JsonParseException ex) + { + throw new IOException(ex); + } + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java b/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java new file mode 100644 index 0000000000..8ee5761a21 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java @@ -0,0 +1,75 @@ +/* + * 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.http.api.npc; + +import lombok.Value; + +@Value +public class NPCStats +{ + private final String name; + + private final int hitpoints; + private final int combatLevel; + private final int slayerLevel; + + 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; + + /** + * 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); + } +} 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 f34d2c13ee..9725057168 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Adam + * Copyright (c) 2019, TheStonedTurtle * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,42 +25,89 @@ */ package net.runelite.client.game; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Type; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.npc.NPCClient; +import net.runelite.http.api.npc.NPCStats; @Singleton +@Slf4j public class NPCManager { - private final Map healthMap; + private final NPCClient NPCClient = new NPCClient(); + private final ImmutableMap statsMap; @Inject private NPCManager() { - final Gson gson = new Gson(); - final Type typeToken = new TypeToken>() - { - }.getType(); + this.statsMap = getStatsMap(); + } - final InputStream healthFile = getClass().getResourceAsStream("/npc_health.json"); - healthMap = gson.fromJson(new InputStreamReader(healthFile), typeToken); + private ImmutableMap getStatsMap() + { + try + { + final Map stats = NPCClient.getStats(); + if (stats != null) + { + log.debug("Loaded {} npc stats", stats.size()); + return ImmutableMap.copyOf(stats); + } + } + catch (IOException e) + { + log.warn("error loading stats!", e); + } + + return ImmutableMap.of(); } /** - * Returns health for target NPC based on it's combat level and name - * @param name npc name - * @param combatLevel npc combat level - * @return health or null if HP is unknown + * Returns the {@link NPCStats} for target NPC id + * @param npcId NPC id + * @return the {@link NPCStats} or null if unknown */ @Nullable - public Integer getHealth(final String name, final int combatLevel) + public NPCStats getStats(final int npcId) { - return healthMap.get(name + "_" + combatLevel); + return statsMap.get(npcId); + } + + /** + * Returns health for target NPC ID + * @param npcId NPC id + * @return health or null if unknown + */ + @Nullable + public Integer getHealth(final int npcId) + { + final NPCStats s = statsMap.get(npcId); + if (s == null || s.getHitpoints() == -1) + { + return null; + } + + return s.getHitpoints(); + } + + /** + * Returns the exp modifier for target NPC ID based on its stats. + * @param npcId NPC id + * @return npcs exp modifier. Assumes default xp rate if npc stats are unknown (returns 1) + */ + public double getXpModifier(final int npcId) + { + final NPCStats s = statsMap.get(npcId); + if (s == null) + { + return 1; + } + + return s.calculateXpModifier(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java index 63432efa20..df0007f9ff 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoOverlay.java @@ -114,7 +114,7 @@ class OpponentInfoOverlay extends Overlay lastMaxHealth = null; if (opponent instanceof NPC) { - lastMaxHealth = npcManager.getHealth(opponentName, opponent.getCombatLevel()); + lastMaxHealth = npcManager.getHealth(((NPC) opponent).getId()); } else if (opponent instanceof Player) { 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..8a036eff83 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 @@ -40,7 +40,6 @@ 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; class TargetWeaknessOverlay extends Overlay { @@ -109,8 +108,7 @@ class TargetWeaknessOverlay extends Overlay final int healthScale = target.getHealth(); final int healthRatio = target.getHealthRatio(); - final String targetName = Text.removeTags(target.getName()); - final Integer maxHealth = npcManager.getHealth(targetName, target.getCombatLevel()); + final Integer maxHealth = npcManager.getHealth(target.getId()); if (healthRatio < 0 || healthScale <= 0 || maxHealth == null) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java index a8d65962a5..6d3facde0f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java @@ -296,7 +296,7 @@ public class XpTrackerPlugin extends Plugin if (interacting instanceof NPC && COMBAT.contains(skill)) { final NPC npc = (NPC) interacting; - xpState.updateNpcExperience(skill, npc, npcManager.getHealth(npc.getName(), npc.getCombatLevel())); + xpState.updateNpcExperience(skill, npc, npcManager.getHealth(npc.getId())); } final XpUpdateResult updateResult = xpState.updateSkill(skill, currentXp, startGoalXp, endGoalXp); @@ -328,7 +328,7 @@ public class XpTrackerPlugin extends Plugin for (Skill skill : COMBAT) { - final XpUpdateResult updateResult = xpState.updateNpcKills(skill, npc, npcManager.getHealth(npc.getName(), npc.getCombatLevel())); + final XpUpdateResult updateResult = xpState.updateNpcKills(skill, npc, npcManager.getHealth(npc.getId())); final boolean updated = XpUpdateResult.UPDATED.equals(updateResult); xpPanel.updateSkillExperience(updated, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); } diff --git a/runelite-client/src/main/resources/npc_health.json b/runelite-client/src/main/resources/npc_health.json deleted file mode 100644 index 4944167e6f..0000000000 --- a/runelite-client/src/main/resources/npc_health.json +++ /dev/null @@ -1,1161 +0,0 @@ -{ - "Molanisk_51": 52, - "Aberrant spectre_96": 90, - "Nechryael_115": 105, - "Death spawn_46": 60, - "Zombie_13": 22, - "Zombie_18": 24, - "Zombie_24": 30, - "Summoned Zombie_13": 22, - "Skeleton_22": 29, - "Skeleton_21": 24, - "Skeleton_25": 17, - "Skeleton_45": 59, - "Skeleton Mage_16": 17, - "Ghost_19": 25, - "Soulless_18": 24, - "Death wing_83": 80, - "Rock Crab_13": 50, - "Hellhound_122": 116, - "Wolf_64": 69, - "White wolf_25": 35, - "White wolf_38": 44, - "Big Wolf_73": 74, - "Wolf_25": 34, - "Wild dog_63": 62, - "Guard dog_44": 49, - "Hobgoblin_54": 62, - "Troll_91": 120, - "Huge spider_81": 90, - "Ogre_53": 60, - "Baby red dragon_65": 65, - "Kalphite Soldier_85": 90, - "Steel dragon_246": 210, - "Dagannoth_135": 142, - "Tok-Xil_135": 60, - "Rocnar_97": 100, - "Jungle Wolf_64": 69, - "King Black Dragon_276": 240, - "Black demon_172": 157, - "Baby dragon_48": 50, - "Red dragon_152": 140, - "Black dragon_227": 190, - "Green dragon_79": 75, - "Blue dragon_111": 105, - "Bronze dragon_131": 122, - "Iron dragon_189": 165, - "Ghoul_42": 50, - "Dwarf_10": 16, - "Chaos dwarf_48": 61, - "Dwarf_20": 16, - "Dwarf_9": 16, - "Dwarf_11": 16, - "Gunthor the brave_29": 35, - "Jailer_47": 47, - "Black Heather_34": 37, - "Donny the lad_34": 37, - "Speedy Keith_34": 37, - "Salarin the twisted_70": 70, - "Corporeal Beast_785": 2000, - "Dark energy core_75": 25, - "Pheasant_3": 5, - "Cave crawler_23": 22, - "Kurask_106": 97, - "Gargoyle_111": 105, - "Banshee_23": 22, - "Abyssal demon_124": 150, - "Basilisk_61": 75, - "Cockatrice_37": 37, - "Rockslug_29": 27, - "Dust devil_93": 105, - "Turoth_86": 76, - "Turoth_89": 81, - "Turoth_87": 79, - "Turoth_85": 77, - "Turoth_83": 76, - "Turoth_88": 76, - "Pyrefiend_43": 45, - "Jelly_78": 75, - "Infernal Mage_66": 60, - "Crawling Hand_8": 16, - "Crawling Hand_7": 16, - "Crawling Hand_12": 19, - "Crawling Hand_11": 16, - "Lizard_42": 40, - "Desert Lizard_24": 25, - "Small Lizard_12": 15, - "Harpie Bug Swarm_46": 25, - "Skeletal Wyvern_140": 210, - "Killerwatt_55": 51, - "Zygomite_74": 65, - "Zygomite_86": 75, - "Wall beast_49": 105, - "Giant frog_99": 100, - "Big frog_24": 25, - "Cave slime_23": 25, - "Cave bug_6": 5, - "Cave bug_96": 93, - "Bloodveld_76": 120, - "Cave kraken_127": 125, - "Kraken_291": 255, - "Smoke devil_160": 185, - "Thermonuclear smoke devil_301": 240, - "Sorebones_57": 52, - "Zombie pirate_57": 52, - "Barrelchest_190": 134, - "Zombie swab_55": 50, - "Evil spirit_150": 90, - "Fever spider_49": 40, - "Tyras guard_110": 110, - "Arrg_113": 140, - "Ice wolf_96": 70, - "Ice wolf_132": 70, - "Ice Troll_124": 80, - "Ice Troll_123": 80, - "Ice Troll_120": 100, - "Ice Troll_121": 80, - "Goblin_5": 12, - "Giant skeleton_80": 70, - "Damis_103": 90, - "Damis_174": 200, - "Stranger_95": 80, - "Bandit_74": 65, - "Bandit_57": 50, - "Ice troll_124": 80, - "Ice troll_123": 80, - "Ice troll_120": 100, - "Ice troll_121": 80, - "Mummy_96": 86, - "Mummy_103": 91, - "Scarabs_92": 25, - "Bandit champion_70": 50, - "Baby Roc_75": 50, - "Giant Roc_172": 250, - "Me_79": 45, - "Suqah_111": 106, - "Scarab mage_93": 50, - "Locust rider_106": 90, - "Locust rider_98": 90, - "Giant scarab_191": 130, - "Scarab mage_66": 50, - "Locust rider_68": 90, - "Wormbrain_2": 5, - "Melzar the Mad_43": 44, - "Icelord_51": 60, - "Zogre_44": 71, - "Skogre_44": 71, - "Slash Bash_111": 100, - "Moss giant_84": 120, - "Agrith Naar_100": 100, - "Skeleton_13": 18, - "Rock_111": 140, - "Stick_104": 135, - "Pee Hat_91": 120, - "Kraka_91": 120, - "Thrower Troll_67": 95, - "Mountain troll_69": 90, - "Ghast_30": 45, - "Mummy_84": 68, - "Kalphite Worker_28": 40, - "Kalphite Guardian_141": 171, - "Kalphite Queen_333": 255, - "Dagannoth_74": 70, - "Dagannoth_92": 120, - "Dagannoth mother_100": 120, - "Sigmund_50": 70, - "Angry unicorn_45": 200, - "Angry giant rat_45": 200, - "Angry goblin_45": 200, - "Fear reaper_42": 25, - "Confusion beast_43": 64, - "Hopeless creature_40": 25, - "Tolna_46": 45, - "Sea Snake Young_90": 85, - "Sea Snake Hatchling_62": 50, - "Giant Sea Snake_149": 100, - "Mourner_11": 19, - "Mourner_24": 25, - "Man_4": 7, - "Woman_3": 7, - "Barrelchest (hard)_380": 255, - "Giant scarab (hard)_316": 255, - "Dessous (hard)_217": 255, - "Kamil (hard)_273": 255, - "Woman_4": 7, - "Damis (hard)_200": 198, - "Damis (hard)_272": 255, - "Mourner_18": 13, - "Woman_12": 7, - "Woman_14": 7, - "Paladin_59": 66, - "Mourner_12": 13, - "Ogre_63": 60, - "Tree spirit_101": 85, - "Alomone_13": 25, - "Clivet_13": 25, - "Hazeel Cultist_13": 25, - "Khazard Guard_23": 25, - "General Khazard_112": 171, - "Bouncer_137": 116, - "Khazard Ogre_63": 60, - "Khazard Scorpion_44": 40, - "Arzinian Avatar of Strength_125": 100, - "Arzinian Avatar of Strength_75": 70, - "Arzinian Avatar of Ranging_125": 100, - "Arzinian Avatar of Ranging_75": 70, - "Arzinian Avatar of Magic_125": 100, - "Arzinian Avatar of Magic_75": 70, - "Ram_2": 8, - "Vulture_31": 10, - "Experiment_51": 40, - "Experiment_25": 100, - "Loar Shadow_40": 38, - "Loar Shade_40": 38, - "Phrin Shadow_60": 56, - "Phrin Shade_60": 56, - "Riyl Shade_80": 76, - "Asyn Shadow_100": 90, - "Asyn Shade_100": 90, - "Fiyr Shadow_120": 110, - "Fiyr Shade_120": 110, - "Afflicted_37": 30, - "Afflicted_34": 28, - "Afflicted_32": 26, - "Afflicted_30": 24, - "Seagull_2": 6, - "Seagull_3": 10, - "Dwarf gang member_44": 40, - "Dwarf gang member_48": 25, - "Dwarf gang member_49": 25, - "Slagilith_92": 60, - "Fire elemental_35": 30, - "Earth elemental_35": 35, - "Air elemental_34": 30, - "Water elemental_34": 30, - "The Kendal_70": 50, - "Camp dweller_31": 30, - "Camp dweller_25": 25, - "Dwarf_7": 16, - "Black Guard_25": 30, - "Foreman_23": 20, - "Jungle Demon_195": 170, - "Pirate_23": 20, - "Thief_16": 17, - "Mugger_6": 8, - "Chompy bird_6": 10, - "Kebbit_13": 50, - "Skeleton hero_149": 124, - "Skeleton brute_132": 124, - "Skeleton warlord_132": 124, - "Skeleton heavy_132": 124, - "Skeleton thug_132": 124, - "Black knight_33": 42, - "Guard_20": 22, - "Guard_21": 22, - "Fire wizard_13": 25, - "Water wizard_13": 25, - "Earth wizard_13": 25, - "Air wizard_13": 25, - "Kolodion_112": 107, - "Battle mage_54": 120, - "Ahrim the Blighted_98": 100, - "Dharok the Wretched_115": 100, - "Guthan the Infested_115": 100, - "Karil the Tainted_98": 100, - "Torag the Corrupted_115": 100, - "Verac the Defiled_115": 100, - "Bloodworm_52": 45, - "Crypt rat_43": 35, - "Giant crypt rat_76": 70, - "Crypt spider_56": 60, - "Giant crypt spider_79": 80, - "Skeleton_77": 50, - "Splatter_22": 13, - "Splatter_33": 23, - "Splatter_44": 33, - "Splatter_54": 43, - "Splatter_65": 53, - "Shifter_38": 23, - "Shifter_57": 38, - "Shifter_76": 53, - "Shifter_90": 68, - "Shifter_104": 83, - "Ravager_36": 23, - "Ravager_53": 38, - "Ravager_71": 53, - "Ravager_89": 68, - "Ravager_106": 83, - "Spinner_36": 33, - "Spinner_55": 53, - "Spinner_74": 73, - "Spinner_92": 101, - "Spinner_88": 93, - "Torcher_33": 18, - "Torcher_49": 30, - "Torcher_66": 45, - "Torcher_79": 57, - "Torcher_92": 71, - "Defiler_33": 27, - "Defiler_50": 45, - "Defiler_66": 62, - "Defiler_67": 62, - "Defiler_80": 78, - "Defiler_97": 97, - "Brawler_51": 53, - "Brawler_76": 83, - "Brawler_101": 113, - "Brawler_129": 143, - "Double agent_65": 80, - "Double agent_108": 120, - "Scarab swarm_98": 25, - "Goat_23": 21, - "Billy Goat_33": 28, - "White Knight_36": 52, - "White Knight_38": 52, - "White Knight_39": 52, - "White Knight_42": 55, - "Gorak_145": 112, - "Duck_1": 3, - "Stag_15": 19, - "Rabbit_1": 5, - "Tree spirit_14": 50, - "Tree spirit_79": 86, - "Tree spirit_120": 120, - "Tree spirit_159": 170, - "Evil Chicken_159": 120, - "Baby dragon_83": 80, - "Ice troll runt_74": 60, - "Ice troll male_82": 80, - "Ice troll female_82": 80, - "Ice troll grunt_100": 80, - "Duckling_1": 3, - "Lesser demon_82": 81, - "Greater demon_92": 89, - "Zulrah_725": 500, - "Snakeling_90": 1, - "Chaos Elemental_305": 250, - "Dark wizard_23": 24, - "Dark wizard_22": 24, - "Dark wizard_11": 24, - "Oomlie bird_46": 40, - "Terrorbird_28": 34, - "Mounted terrorbird gnome_31": 36, - "Mounted terrorbird gnome_49": 36, - "Fire giant_86": 111, - "Ice giant_53": 70, - "Moss giant_42": 60, - "Jogre_53": 60, - "Cyclops_56": 75, - "Hill Giant_28": 35, - "Cyclops_106": 150, - "Giant frog_13": 23, - "Big frog_10": 18, - "Frog_5": 8, - "TzHaar-Hur_74": 80, - "TzHaar-Xil_133": 120, - "TzHaar-Ket_149": 140, - "Tz-Kih_22": 10, - "Tz-Kek_45": 20, - "Tok-Xil_90": 40, - "Commander Zilyana_596": 255, - "Starlight_149": 160, - "Growler_139": 146, - "Bree_146": 162, - "Saradomin priest_113": 89, - "Spiritual warrior_125": 110, - "Spiritual ranger_122": 106, - "Spiritual mage_120": 85, - "Knight of Saradomin_103": 135, - "Knight of Saradomin_101": 108, - "General Graardor_624": 255, - "Sergeant Strongstack_141": 128, - "Sergeant Steelwill_142": 127, - "Sergeant Grimspike_142": 146, - "Ogre_58": 70, - "Jogre_58": 70, - "Cyclops_81": 110, - "Ork_107": 110, - "Hobgoblin_47": 52, - "Spiritual ranger_115": 131, - "Spiritual warrior_134": 131, - "Spiritual mage_121": 75, - "Goblin_17": 18, - "Goblin_12": 15, - "Goblin_15": 16, - "Goblin_13": 16, - "Dagannoth_88": 85, - "Giant Rock Crab_137": 180, - "Bardur_94": 99, - "Dagannoth fledgeling_70": 100, - "Dagannoth Supreme_303": 255, - "Dagannoth Prime_303": 255, - "Dagannoth Rex_303": 255, - "Animated Bronze Armour_11": 10, - "Animated Iron Armour_23": 20, - "Animated Steel Armour_46": 40, - "Animated Black Armour_69": 60, - "Animated Mithril Armour_92": 80, - "Animated Adamant Armour_113": 99, - "Animated Rune Armour_138": 120, - "Cyclops_76": 100, - "Catablepon_49": 40, - "Catablepon_64": 70, - "Catablepon_68": 50, - "Giant spider_50": 50, - "Spider_24": 22, - "Scorpion_59": 55, - "Scorpion_37": 37, - "Minotaur_12": 10, - "Minotaur_19": 10, - "Minotaur_27": 22, - "Goblin_11": 7, - "Goblin_16": 22, - "Goblin_25": 26, - "Wolf_14": 15, - "Wolf_11": 10, - "Rat_1": 2, - "Flesh Crawler_28": 25, - "Flesh Crawler_35": 25, - "Flesh Crawler_41": 25, - "Giant rat_26": 25, - "Ankou_75": 60, - "Ankou_82": 65, - "Ankou_86": 70, - "Skeleton_68": 70, - "Skeleton_60": 70, - "Skeleton_85": 77, - "Ghost_77": 80, - "Ghost_76": 75, - "H.A.M. Guard_12": 15, - "H.A.M. Guard_18": 20, - "H.A.M. Guard_22": 30, - "Monk_5": 15, - "Abyssal leech_41": 10, - "Abyssal guardian_59": 55, - "Abyssal walker_81": 95, - "Mogre_60": 48, - "Werewolf_88": 98, - "Boris_24": 60, - "Imre_24": 60, - "Yuri_24": 60, - "Joseph_24": 60, - "Nikolai_24": 60, - "Eduard_24": 60, - "Lev_24": 60, - "Georgy_24": 60, - "Svetlana_24": 60, - "Irina_24": 60, - "Alexis_24": 60, - "Milla_24": 60, - "Galina_24": 60, - "Sofiya_24": 60, - "Ksenia_24": 60, - "Yadviga_24": 60, - "Nikita_24": 60, - "Vera_24": 60, - "Zoja_24": 60, - "Liliya_24": 60, - "Myre Blamish Snail_9": 8, - "Blood Blamish Snail_20": 13, - "Ochre Blamish Snail_10": 10, - "Bruise Blamish Snail_20": 12, - "Bark Blamish Snail_15": 22, - "Ochre Blamish Snail_15": 20, - "Chicken_1": 3, - "Rooster_3": 5, - "Cow_2": 8, - "Cow calf_2": 6, - "Bat_6": 8, - "Troll_69": 90, - "Giant bat_27": 32, - "Unicorn_15": 19, - "Grizzly bear_21": 27, - "Black bear_19": 25, - "Earth warrior_51": 54, - "Ice warrior_57": 59, - "Otherworldly being_64": 66, - "Magic axe_42": 45, - "Snake_5": 6, - "Black unicorn_27": 29, - "Shadow warrior_48": 67, - "Giant rat_3": 5, - "Giant rat_6": 10, - "Dark wizard_20": 24, - "Invrigar the Necromancer_20": 24, - "Dark wizard_7": 12, - "Black Knight_33": 42, - "Highwayman_5": 13, - "Chaos druid_13": 20, - "Pirate_26": 23, - "Thug_10": 18, - "Rogue_15": 17, - "Monk of Zamorak_22": 20, - "Monk of Zamorak_17": 10, - "Monk of Zamorak_45": 40, - "Tribesman_32": 40, - "Dark warrior_8": 17, - "Chaos druid warrior_37": 40, - "Necromancer_26": 40, - "Guard Bandit_22": 27, - "Waterfiend_115": 130, - "Brutal green dragon_227": 175, - "Mithril dragon_304": 255, - "Confused barbarian_132": 124, - "Lost barbarian_132": 124, - "Nail beast_69": 55, - "Nail beast_98": 65, - "Nail beast_141": 75, - "Zamorak wizard_65": 73, - "Saradomin wizard_108": 120, - "Big Snake_84": 120, - "Undead cow_2": 8, - "Undead chicken_1": 3, - "Giant lobster_32": 32, - "Tortured soul_59": 51, - "Man_2": 7, - "Woman_2": 7, - "Shadow spider_52": 55, - "Giant spider_2": 5, - "Giant spider_27": 33, - "Spider_1": 2, - "Jungle spider_44": 51, - "Deadly red spider_34": 35, - "Ice spider_61": 65, - "Poison spider_64": 64, - "Scorpion_14": 17, - "Poison Scorpion_20": 23, - "Pit Scorpion_28": 32, - "King Scorpion_32": 30, - "Goblin_2": 5, - "Hobgoblin_28": 29, - "Hobgoblin_42": 49, - "Barbarian_17": 25, - "Barbarian_10": 25, - "Barbarian_15": 25, - "Barbarian_9": 18, - "Farmer_7": 12, - "Wizard_9": 14, - "Druid_33": 30, - "Warrior woman_24": 20, - "Al-Kharid warrior_9": 19, - "Paladin_62": 66, - "Hero_69": 82, - "Forester_15": 17, - "Knight of Ardougne_46": 52, - "Tz-Kek_22": 10, - "Yt-MejKot_180": 80, - "Ket-Zek_360": 160, - "TzTok-Jad_702": 250, - "Yt-HurKot_108": 60, - "K'ril Tsutsaroth_650": 255, - "Tstanon Karlak_145": 142, - "Zakl'n Gritch_142": 150, - "Balfrug Kreeyath_151": 161, - "Hellhound_127": 116, - "Imp_7": 10, - "Werewolf_93": 92, - "Feral Vampyre_77": 60, - "Bloodveld_81": 134, - "Pyrefiend_48": 45, - "Icefiend_18": 20, - "Gorak_149": 128, - "Spiritual warrior_115": 100, - "Spiritual ranger_118": 120, - "Kree'arra_580": 255, - "Wingman Skree_143": 121, - "Flockleader Geerin_149": 132, - "Flight Kilisa_159": 159, - "Spiritual warrior_123": 98, - "Spiritual ranger_127": 89, - "Spiritual mage_122": 75, - "Aviansie_69": 70, - "Aviansie_79": 83, - "Aviansie_84": 86, - "Aviansie_83": 86, - "Aviansie_92": 95, - "Aviansie_97": 98, - "Aviansie_137": 124, - "Aviansie_148": 139, - "Aviansie_71": 63, - "Aviansie_73": 67, - "Aviansie_89": 69, - "Aviansie_94": 75, - "Aviansie_131": 115, - "Dagannoth spawn_42": 35, - "Dagannoth_90": 95, - "Snake_35": 25, - "Albino bat_52": 33, - "Giant mosquito_13": 15, - "Jungle horror_70": 45, - "Cave horror_80": 55, - "Leech_52": 45, - "Feral Vampyre_72": 50, - "Feral Vampyre_61": 40, - "Watchman_33": 22, - "Soldier_28": 22, - "Shipyard worker_11": 10, - "Drunken man_3": 7, - "Gardener_4": 8, - "Gardener_3": 7, - "Cuffs_3": 7, - "Narf_2": 7, - "Rusty_2": 7, - "Jeff_2": 7, - "Hengel_2": 7, - "Anja_2": 7, - "Chicken_3": 3, - "Earth Warrior Champion_102": 108, - "Giant Champion_56": 70, - "Ghoul Champion_85": 100, - "Goblin Champion_24": 32, - "Hobgoblin Champion_56": 58, - "Imp Champion_14": 40, - "Jogre Champion_107": 120, - "Lesser Demon Champion_162": 148, - "Skeleton Champion_40": 58, - "Zombies Champion_51": 60, - "Leon d'Cour_141": 123, - "Rabbit_2": 5, - "Grizzly bear_42": 35, - "Grizzly bear cub_33": 35, - "Dire Wolf_88": 85, - "Elf warrior_90": 105, - "Elf warrior_108": 105, - "Lucien_14": 17, - "Guardian of Armadyl_45": 49, - "Guardian of Armadyl_43": 49, - "Fire Warrior of Lesarkus_84": 59, - "Shadow Hound_63": 62, - "Fareed_167": 130, - "Kamil_154": 130, - "Dessous_139": 200, - "The Inadequacy_343": 180, - "The Everlasting_223": 230, - "The Untouchable_274": 90, - "The Illusive_108": 140, - "A Doubt_78": 50, - "Count Draynor_34": 35, - "Monk of Zamorak_30": 25, - "Bouncer_160": 116, - "Renegade Knight_37": 48, - "Thrantax the Mighty_92": 80, - "Sir Mordred_39": 38, - "Desert snake_5": 6, - "Menaphite Thug_55": 60, - "Tough Guy_75": 75, - "Frogeel_103": 90, - "Unicow_25": 24, - "Spidine_42": 35, - "Swordchick_46": 35, - "Jubster_87": 60, - "Newtroost_19": 18, - "Possessed pickaxe_50": 40, - "Skeletal miner_42": 39, - "Treus Dayth_95": 100, - "Ghost_29": 27, - "Rooster_2": 5, - "Einar_1": 1, - "Alrik_1": 1, - "Thorhild_1": 1, - "Rannveig_2": 1, - "Valgerd_2": 1, - "Broddi_2": 1, - "Ragnvald_2": 1, - "Vampyre Juvenile_45": 60, - "Vampyre Juvinate_54": 65, - "Feral Vampyre_64": 80, - "Vyrewatch_105": 90, - "Vyrewatch_110": 90, - "Vyrewatch_120": 105, - "Vyrewatch_125": 110, - "Vanstrom Klause_169": 155, - "Moss giant_48": 85, - "Jake_37": 50, - "Wilson_37": 50, - "Palmer_37": 50, - "Fox_19": 30, - "Bunny_2": 5, - "Bear Cub_15": 20, - "Unicorn Foal_12": 15, - "Black unicorn Foal_22": 25, - "The Draugen_69": 60, - "Freidir_48": 50, - "Borrokar_48": 50, - "Lanzig_48": 50, - "Jennella_48": 50, - "Market Guard_48": 50, - "Ungadulu_70": 65, - "Ungadulu_169": 150, - "Nezikchened_187": 150, - "San Tojalon_106": 120, - "Irvig Senay_100": 125, - "Ranalph Devere_92": 130, - "Zombie rat_3": 5, - "Witch's experiment_19": 21, - "Witch's experiment (second form)_30": 31, - "Witch's experiment (third form)_42": 41, - "Witch's experiment (fourth form)_53": 51, - "Shadow_73": 15, - "Dark beast_182": 220, - "Black Knight Titan_120": 142, - "Soldier_48": 50, - "Ocga_5": 10, - "Penda_5": 10, - "Fareed (hard)_299": 255, - "Troll general_113": 140, - "Troll spectator_71": 90, - "Dad_101": 120, - "Twig_71": 90, - "Berry_71": 90, - "Thrower troll_68": 95, - "Mountain troll_71": 90, - "King Roald_47": 60, - "Outlaw_32": 20, - "Crocodile_63": 62, - "Jackal_21": 27, - "Locust_18": 27, - "Plague frog_11": 10, - "Possessed Priest_91": 90, - "Monk_3": 5, - "Thief_14": 17, - "Head Thief_26": 37, - "Jail guard_26": 32, - "Sea troll_79": 100, - "Sea troll_65": 80, - "Sea troll_87": 80, - "Sea troll_101": 80, - "Sea Troll Queen_170": 200, - "Skeleton Mage_83": 80, - "Sir Lancelot_127": 115, - "Sir Kay_124": 110, - "Sir Gawain_122": 110, - "Sir Lucan_120": 105, - "Sir Palomedes_118": 100, - "Sir Tristram_115": 105, - "Sir Pelleas_112": 99, - "Sir Bedivere_110": 90, - "Ogre chieftain_81": 60, - "Gorad_68": 80, - "City guard_83": 80, - "Enclave guard_83": 80, - "Ogre shaman_113": 1, - "Tower guard_28": 22, - "Colonel Radick_38": 65, - "Vampyre Juvinate_75": 110, - "Vampyre Juvinate_50": 60, - "Gadderanks_35": 20, - "Skeleton fremennik_40": 25, - "Skeleton fremennik_50": 35, - "Skeleton fremennik_60": 40, - "Ulfric_100": 60, - "Brine rat_70": 50, - "Blessed spider_39": 32, - "Blessed giant rat_9": 30, - "Sir Jerro_62": 57, - "Sir Carl_62": 57, - "Sir Harry_62": 57, - "Kalrag_89": 78, - "Othainian_91": 87, - "Doomion_91": 90, - "Holthion_91": 87, - "Disciple of Iban_13": 20, - "Rowdy slave_10": 16, - "Mercenary Captain_47": 80, - "Desert Wolf_27": 34, - "Ugthanki_42": 45, - "Bedabin Nomad Fighter_56": 50, - "Mercenary_45": 60, - "Sir Leye_20": 20, - "Angry unicorn_47": 200, - "Angry giant rat_47": 200, - "Angry goblin_47": 200, - "Angry bear_47": 200, - "Fear reaper_55": 57, - "Confusion beast_63": 64, - "Hopeless creature_71": 71, - "Hopeless beast_71": 71, - "The Shaikahan_83": 100, - "Black golem_75": 80, - "White golem_75": 80, - "Grey golem_75": 80, - "Poltenip_21": 22, - "Radat_21": 22, - "Slug Prince_62": 70, - "Icefiend_13": 15, - "Crab_23": 19, - "Mudskipper_30": 20, - "Mudskipper_31": 20, - "Crab_21": 18, - "Jubbly bird_9": 21, - "Culinaromancer_75": 150, - "Agrith-Na-Na_146": 200, - "Flambeed_149": 210, - "Karamel_136": 250, - "Dessourt_121": 130, - "Gelatinnoth Mother_130": 240, - "Grip_22": 25, - "Ice Queen_111": 105, - "Pirate Guard_19": 25, - "Entrana firebird_2": 5, - "Black Knight_32": 42, - "Khazard trooper_19": 22, - "Khazard commander_48": 22, - "Gnome troop_1": 3, - "Chronozon_170": 60, - "Imp_2": 8, - "Imp_3": 8, - "Suit of armour_19": 29, - "Skeleton Hellhound_97": 55, - "Delrith_27": 71, - "Experiment No.2_109": 95, - "Mouse_95": 70, - "Glod_138": 160, - "Sigmund_64": 70, - "H.A.M. Archer_30": 35, - "H.A.M. Mage_30": 35, - "Weaponsmaster_23": 20, - "Jonny the beard_2": 8, - "Bird_11": 10, - "Bird_5": 5, - "Jungle spider_37": 51, - "Snake_24": 6, - "Padulah_149": 130, - "Monkey Guard_149": 130, - "Monkey Archer_86": 50, - "Monkey Guard_167": 130, - "Monkey Zombie_98": 60, - "Monkey Zombie_129": 90, - "Monkey Zombie_82": 60, - "Mourner_108": 105, - "Cave goblin miner_11": 10, - "Cave goblin guard_26": 26, - "Cave goblin guard_24": 26, - "Undead one_61": 47, - "Nazastarool_91": 70, - "Nazastarool_68": 70, - "Nazastarool_93": 80, - "Goblin guard_42": 43, - "Ghost_24": 20, - "Grave scorpion_12": 7, - "Poison spider_31": 64, - "Enormous Tentacle_112": 120, - "Angry barbarian spirit_166": 190, - "Enraged barbarian spirit_166": 190, - "Berserk barbarian spirit_166": 190, - "Ferocious barbarian spirit_166": 190, - "Swamp snake_80": 120, - "Swamp snake_109": 120, - "Swamp snake_139": 85, - "Ghast_79": 45, - "Ghast_109": 135, - "Ghast_139": 160, - "Giant snail_80": 125, - "Giant snail_109": 150, - "Giant snail_139": 160, - "Vampyre Juvinate_59": 50, - "Vampyre Juvinate_90": 100, - "Vampyre Juvinate_119": 150, - "Feral Vampyre_70": 75, - "Feral Vampyre_100": 135, - "Feral Vampyre_130": 185, - "Tentacle_99": 75, - "Head_140": 150, - "Tentacle_136": 75, - "Undead Lumberjack_30": 12, - "Undead Lumberjack_35": 12, - "Undead Lumberjack_40": 12, - "Undead Lumberjack_45": 13, - "Undead Lumberjack_50": 14, - "Undead Lumberjack_55": 12, - "Undead Lumberjack_60": 12, - "Undead Lumberjack_64": 12, - "Undead Lumberjack_70": 12, - "Penance Fighter_30": 28, - "Penance Fighter_32": 29, - "Penance Fighter_37": 32, - "Penance Fighter_42": 37, - "Penance Fighter_47": 38, - "Penance Fighter_56": 49, - "Penance Fighter_61": 50, - "Penance Fighter_68": 55, - "Penance Fighter_77": 56, - "Penance Ranger_21": 20, - "Penance Ranger_25": 29, - "Penance Ranger_32": 32, - "Penance Ranger_38": 34, - "Penance Ranger_43": 41, - "Penance Ranger_51": 50, - "Penance Ranger_57": 50, - "Penance Ranger_64": 55, - "Penance Ranger_72": 58, - "Penance Queen_209": 250, - "Queen spawn_63": 45, - "Giant Mole_230": 200, - "Yak_22": 50, - "Ice Troll King_122": 150, - "Ice troll grunt_102": 80, - "Tanglefoot_111": 102, - "Baby tanglefoot_45": 40, - "Cerberus_318": 600, - "Abyssal Sire_350": 400, - "Spawn_60": 15, - "Scion_100": 50, - "Sand Crab_15": 60, - "Wallasalki_98": 120, - "Rock lobster_127": 150, - "Spinolyp_76": 100, - "Gnome troop_3": 2, - "Black Guard_48": 40, - "Black Guard Berserker_66": 50, - "Tortoise_79": 100, - "Tortoise_92": 120, - "Gnome child_1": 2, - "Gnome guard_23": 31, - "Gnome woman_1": 2, - "Gnome Archer_5": 10, - "Gnome Driver_5": 10, - "Gnome Mage_5": 10, - "Bush snake_35": 25, - "Elvarg (hard)_214": 240, - "The Inadequacy (hard)_600": 255, - "The Untouchable (hard)_440": 180, - "Large mosquito_13": 3, - "Mosquito swarm_17": 9, - "Tanglefoot (hard)_199": 204, - "Chronozon (hard)_297": 120, - "Bouncer (hard)_244": 232, - "Ice Troll King (hard)_213": 255, - "Black demon (hard)_292": 157, - "Glod (hard)_276": 255, - "Treus Dayth (hard)_194": 240, - "Black Knight Titan (hard)_210": 255, - "Dagannoth mother (hard)_201": 240, - "Evil Chicken (hard)_286": 240, - "Culinaromancer (hard)_209": 255, - "Agrith-Na-Na (hard)_235": 255, - "Flambeed (hard)_238": 255, - "Karamel (hard)_186": 255, - "Dessourt (hard)_217": 255, - "Gelatinnoth Mother (hard)_201": 240, - "Nezikchened (hard)_295": 150, - "Tree spirit (hard)_199": 187, - "Jungle Demon (hard)_327": 255, - "The Kendal (hard)_210": 150, - "Giant Roc (hard)_257": 255, - "Slagilith (hard)_202": 150, - "Moss giant (hard)_182": 240, - "Skeleton Hellhound (hard)_198": 132, - "Agrith Naar (hard)_196": 209, - "King Roald (hard)_188": 150, - "Khazard warlord (hard)_192": 255, - "Dad (hard)_201": 240, - "Arrg (hard)_210": 255, - "Count Draynor (hard)_177": 210, - "Witch's experiment (hard)_47": 63, - "Witch's experiment (second form) (hard)_77": 93, - "Witch's experiment (third form) (hard)_90": 103, - "Witch's experiment (fourth form) (hard)_103": 113, - "Nazastarool (hard)_176": 154, - "Nazastarool (hard)_153": 180, - "Nazastarool (hard)_181": 176, - "Elvarg_83": 80, - "Khazard warlord_112": 170, - "Mosquito swarm_20": 15, - "Broodoo victim_60": 100, - "Animated steel armour_53": 50, - "Animated spade_50": 40, - "Terror dog_110": 87, - "Terror dog_100": 82, - "Tarn_69": 80, - "Mutant tarn_69": 80, - "Callisto_470": 255, - "Venenatis_464": 255, - "Gnome guard_1337": 31, - "Armadylian guard_97": 132, - "Bandosian guard_125": 130, - "Lava dragon_252": 230, - "Ent_101": 105, - "Runite Golem_178": 170, - "Rogue_135": 125, - "Mammoth_80": 130, - "Dark warrior_145": 137, - "Elder Chaos druid_129": 150, - "Vet'ion_454": 255, - "Vet'ion Reborn_454": 255, - "Skeleton Hellhound_214": 55, - "Greater Skeleton Hellhound_281": 190, - "Scorpia_225": 200, - "Scorpia's guardian_47": 70, - "Crazy archaeologist_204": 225, - "Chaos Fanatic_202": 225, - "Chaotic death spawn_215": 50, - "Rock Golem_120": 120, - "Rock Golem_159": 170, - "River troll_120": 120, - "River troll_159": 170, - "Lizardman shaman_150": 150, - "Maniacal monkey_48": 65, - "Kruk_149": 210, - "Gangster_45": 40, - "Gangster_50": 50, - "Gang boss_83": 80, - "Gang boss_76": 80, - "Soldier (tier 1)_39": 50, - "Soldier (tier 2)_48": 50, - "Soldier (tier 3)_58": 55, - "Soldier (tier 4)_70": 65, - "Soldier (tier 5)_99": 90, - "Lizardman_53": 60, - "Lizardman_62": 60, - "Lizardman brute_73": 60, - "Kourend guard_21": 22, - "Kourend head guard_84": 86, - "Tortured gorilla_142": 110, - "Glough_378": 575, - "Keef_178": 180, - "Kob_185": 200, - "Maniacal monkey_140": 65, - "Maniacal Monkey Archer_132": 60, - "Demonic gorilla_275": 380, - "Tortured gorilla_141": 210, - "Ent_86": 75, - "Black demon_184": 170, - "Black demon_178": 160, - "Greater demon_101": 120, - "Greater demon_100": 115, - "Greater demon_113": 130, - "Lesser demon_87": 87, - "Lesser demon_94": 98, - "Dust devil_110": 130, - "Fire giant_109": 150, - "Fire giant_104": 130, - "Bronze dragon_143": 122, - "Iron dragon_215": 195, - "Steel dragon_274": 250, - "Ankou_95": 60, - "King Sand Crab_107": 200, - "Twisted Banshee_89": 109, - "Brutal blue dragon_271": 245, - "Brutal red dragon_289": 285, - "Brutal black dragon_318": 315, - "Mutated Bloodveld_123": 170, - "Warped Jelly_112": 140, - "Greater Nechryael_200": 205, - "Deviant spectre_169": 190, - "Skotizo_321": 450, - "Reanimated demon spawn_87": 85, - "Dark Ankou_95": 60, - "Ancient Wizard_98": 80, - "Ancient Wizard_112": 80, - "Brassican Mage_140": 150, - "Double agent_141": 160, - "Crushing hand_45": 55, - "Chasm Crawler_68": 64, - "Screaming banshee_70": 61, - "Screaming twisted banshee_144": 220, - "Giant rockslug_86": 77, - "Cockathrice_89": 95, - "Flaming pyrelord_97": 126, - "Monstrous basilisk_135": 170, - "Malevolent Mage_162": 175, - "Insatiable Bloodveld_202": 380, - "Insatiable mutated Bloodveld_278": 410, - "Vitreous Jelly_206": 190, - "Vitreous warped Jelly_241": 220, - "Cave abomination_206": 130, - "Abhorrent spectre_253": 250, - "Repugnant spectre_335": 390, - "Choke devil_264": 300, - "King kurask_295": 420, - "Nuclear smoke devil_280": 240, - "Marble gargoyle_349": 270, - "Night beast_374": 550, - "Greater abyssal demon_342": 400, - "Nechryarch_300": 320, - "Obor_106": 120, - "Zamorak warrior_84": 45, - "Zamorak warrior_85": 45, - "Zamorak ranger_81": 50, - "Zamorak ranger_82": 50, - "Cave lizard_37": 20, - "Zamorak crafter_19": 25, - "Temple guardian_30": 45, - "TzHaar-Ket_221": 200, - "Jal-Nib_32": 10, - "Jal-MejRah_85": 25, - "Jal-Ak_165": 40, - "Jal-AkRek-Mej_70": 15, - "Jal-AkRek-Xil_70": 15, - "Jal-AkRek-Ket_70": 15, - "Jal-ImKot_240": 75, - "Jal-Xil_370": 130, - "Jal-Zek_490": 220, - "JalTok-Jad_900": 350, - "Yt-HurKot_141": 90, - "TzKal-Zuk_1400": 1200, - "Jal-MejJak_250": 80, - "Long-tailed Wyvern_152": 200, - "Taloned Wyvern_147": 200, - "Spitting Wyvern_139": 200, - "Ancient Wyvern_210": 300, - "Lobstrosity_68": 50, - "Ancient Zygomite_109": 150, - "Ammonite Crab_25": 100, - "Hoop Snake_19": 25, - "Tar Monster_132": 200, - "Deranged archaeologist_276": 200, - "Dusk_248": 450, - "Dawn_228": 450, - "Justiciar Zachariah_348": 320, - "Derwen_235": 320, - "Porazdir_235": 320, - "Black dragon_247": 250, - "Ankou_98": 100, - "Green dragon_88": 100, - "Greater demon_104": 120, - "Black demon_188": 200, - "Hellhound_136": 150, - "Ice giant_67": 100, - "Revenant imp_7": 10, - "Dusk_328": 450, - "Sand Snake (hard)_154": 180, - "Sand Snake_36": 60, - "Revenant goblin_15": 14, - "Revenant pyrefiend_52": 48, - "Revenant hobgoblin_60": 72, - "Revenant cyclops_82": 110, - "Revenant hellhound_90": 80, - "Revenant demon_98": 80, - "Revenant ork_105": 105, - "Revenant dark beast_120": 140, - "Revenant knight_126": 143, - "Revenant dragon_135": 155, - "Corsair Traitor (hard)_103": 160, - "Corsair Traitor_35": 55, - "Ithoi the Navigator_35": 55, - "Ogress Warrior_82": 82, - "Ogress Shaman_82": 82, - "Corrupt Lizardman (hard)_152": 150, - "Corrupt Lizardman_46": 50, - "Rune dragon_380": 330, - "Adamant dragon_338": 295, - "Robert the Strong_194": 280, - "Vorkath_392": 460, - "Vorkath_732": 750, - "Zombified Spawn_55": 8, - "Zombified Spawn_64": 38, - "Stone Guardian_124": 62, - "Galvek_608": 1200, - "Growthling_37": 10, - "Bryophyta_128": 115, - "Ranis Drakan_233": 400, - "Vyrewatch_87": 75, - "Abomination_149": 200, - "Swamp Crab_55": 75, - "Respiratory system_0": 50, - "Sulphur Lizard_50": 50, - "Wyrm_99": 130, - "Drake_192": 250, - "Hydra_194": 300, - "Alchemical Hydra_426": 1100 -} From 7f7e3641bab1f2adfeeaee4b72700fa41c808ef5 Mon Sep 17 00:00:00 2001 From: TheStonedTurtle Date: Fri, 12 Apr 2019 20:25:00 -0700 Subject: [PATCH 2/5] Add dragon, demon, and undead flag --- .../src/main/java/net/runelite/http/api/npc/NPCStats.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java b/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java index 8ee5761a21..132473cafb 100644 --- a/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java +++ b/http-api/src/main/java/net/runelite/http/api/npc/NPCStats.java @@ -61,6 +61,10 @@ public class NPCStats 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 From 3c59f3de4c23e17a8b0065f671e3799c8b93a613 Mon Sep 17 00:00:00 2001 From: TheStonedTurtle Date: Sat, 11 May 2019 10:32:07 -0700 Subject: [PATCH 3/5] ui: Add Table Component Co-Authored-By: Jordan --- .../components/table/TableAlignment.java | 32 ++ .../components/table/TableComponent.java | 433 ++++++++++++++++++ .../components/table/TableElement.java | 38 ++ .../ui/overlay/components/table/TableRow.java | 41 ++ .../components/table/TableComponentTest.java | 85 ++++ 5 files changed, 629 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableAlignment.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableComponent.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableElement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableRow.java create mode 100644 runelite-client/src/test/java/net/runelite/client/ui/overlay/components/table/TableComponentTest.java diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableAlignment.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableAlignment.java new file mode 100644 index 0000000000..d55080a3e0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableAlignment.java @@ -0,0 +1,32 @@ +/* +* 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.ui.overlay.components.table; + +public enum TableAlignment +{ + LEFT, + CENTER, + RIGHT +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableComponent.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableComponent.java new file mode 100644 index 0000000000..5dec2a7ac6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableComponent.java @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2018, Jordan Atwood + * 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.ui.overlay.components.table; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import net.runelite.client.ui.overlay.components.ComponentConstants; +import net.runelite.client.ui.overlay.components.LayoutableRenderableEntity; +import net.runelite.client.ui.overlay.components.TextComponent; + +@Setter +public class TableComponent implements LayoutableRenderableEntity +{ + private static final TableElement EMPTY_ELEMENT = TableElement.builder().build(); + + @Getter + private final List columns = new ArrayList<>(); + @Getter + private final List rows = new ArrayList<>(); + + @Getter + private final Rectangle bounds = new Rectangle(); + + private TableAlignment defaultAlignment = TableAlignment.LEFT; + private Color defaultColor = Color.WHITE; + private Dimension gutter = new Dimension(3, 0); + private Point preferredLocation = new Point(); + private Dimension preferredSize = new Dimension(ComponentConstants.STANDARD_WIDTH, 0); + + @Override + public Dimension render(final Graphics2D graphics) + { + final FontMetrics metrics = graphics.getFontMetrics(); + final TableRow colRow = TableRow.builder().elements(this.columns).build(); + final int[] columnWidths = getColumnWidths(metrics, colRow); + + graphics.translate(preferredLocation.x, preferredLocation.y); + + // Display the columns first + int height = displayRow(graphics, colRow, 0, columnWidths, metrics); + + for (TableRow row : this.rows) + { + height = displayRow(graphics, row, height, columnWidths, metrics); + } + + graphics.translate(-preferredLocation.x, -preferredLocation.y); + + final Dimension dimension = new Dimension(preferredSize.width, height); + bounds.setLocation(preferredLocation); + bounds.setSize(dimension); + + return dimension; + } + + private int displayRow(Graphics2D graphics, TableRow row, int height, int[] columnWidths, FontMetrics metrics) + { + int x = 0; + int startingRowHeight = height; + + final List elements = row.getElements(); + for (int i = 0; i < elements.size(); i++) + { + int y = startingRowHeight; + final TableElement cell = elements.get(i); + + final String content = cell.getContent(); + if (content == null) + { + continue; + } + + final String[] lines = lineBreakText(content, columnWidths[i], metrics); + final TableAlignment alignment = getCellAlignment(row, i); + final Color color = getCellColor(row, i); + + for (String line : lines) + { + final int alignmentOffset = getAlignedPosition(line, alignment, columnWidths[i], metrics); + final TextComponent leftLineComponent = new TextComponent(); + y += metrics.getHeight(); + + leftLineComponent.setPosition(new Point(x + alignmentOffset, y)); + leftLineComponent.setText(line); + leftLineComponent.setColor(color); + leftLineComponent.render(graphics); + } + height = Math.max(height, y); + x += columnWidths[i] + gutter.width; + } + + return height + gutter.height; + } + + /** + * Returns the width that each column should take up + * Based on https://stackoverflow.com/questions/22206825/algorithm-for-calculating-variable-column-widths-for-set-table-width + * @param metrics + * @return int[] of column width + */ + private int[] getColumnWidths(final FontMetrics metrics, final TableRow columnRow) + { + int numCols = columns.size(); + for (final TableRow r : rows) + { + numCols = Math.max(r.getElements().size(), numCols); + } + + int[] maxtextw = new int[numCols]; // max text width over all rows + int[] maxwordw = new int[numCols]; // max width of longest word + boolean[] flex = new boolean[numCols]; // is column flexible? + boolean[] wrap = new boolean[numCols]; // can column be wrapped? + int[] finalcolw = new int[numCols]; // final width of columns + + final List rows = new ArrayList<>(this.rows); + rows.add(columnRow); + + for (final TableRow r : rows) + { + final List elements = r.getElements(); + for (int col = 0; col < elements.size(); col++) + { + final TableElement ele = elements.get(col); + final String cell = ele.getContent(); + if (cell == null) + { + continue; + } + + final int cellWidth = getTextWidth(metrics, cell); + + maxtextw[col] = Math.max(maxtextw[col], cellWidth); + for (String word : cell.split(" ")) + { + maxwordw[col] = Math.max(maxwordw[col], getTextWidth(metrics, word)); + } + + if (maxtextw[col] == cellWidth) + { + wrap[col] = cell.contains(" "); + } + } + } + + int left = preferredSize.width - (numCols - 1) * gutter.width; + final double avg = left / numCols; + int nflex = 0; + + // Determine whether columns should be flexible and assign width of non-flexible cells + for (int col = 0; col < numCols; col++) + { + // This limit can be adjusted as needed + final double maxNonFlexLimit = 1.5 * avg; + + flex[col] = maxtextw[col] > maxNonFlexLimit; + if (flex[col]) + { + nflex++; + } + else + { + finalcolw[col] = maxtextw[col]; + left -= finalcolw[col]; + } + } + + // If there is not enough space, make columns that could be word-wrapped flexible too + if (left < nflex * avg) + { + for (int col = 0; col < numCols; col++) + { + if (!flex[col] && wrap[col]) + { + left += finalcolw[col]; + finalcolw[col] = 0; + flex[col] = true; + nflex++; + } + } + } + + // Calculate weights for flexible columns. The max width is capped at the table width to + // treat columns that have to be wrapped more or less equal + int tot = 0; + for (int col = 0; col < numCols; col++) + { + if (flex[col]) + { + maxtextw[col] = Math.min(maxtextw[col], preferredSize.width); + tot += maxtextw[col]; + } + } + + // Now assign the actual width for flexible columns. Make sure that it is at least as long + // as the longest word length + for (int col = 0; col < numCols; col++) + { + if (flex[col]) + { + finalcolw[col] = left * maxtextw[col] / tot; + finalcolw[col] = Math.max(finalcolw[col], maxwordw[col]); + left -= finalcolw[col]; + } + } + + // When the sum of column widths is less than the total space available, distribute the + // extra space equally across all columns + final int extraPerCol = left / numCols; + for (int col = 0; col < numCols; col++) + { + finalcolw[col] += extraPerCol; + left -= extraPerCol; + } + // Add any remainder to the right-most column + finalcolw[finalcolw.length - 1] += left; + + return finalcolw; + } + + private static int getTextWidth(final FontMetrics metrics, final String cell) + { + return metrics.stringWidth(TextComponent.textWithoutColTags(cell)); + } + + private static String[] lineBreakText(final String text, final int maxWidth, final FontMetrics metrics) + { + final String[] words = text.split(" "); + + if (words.length == 0) + { + return new String[0]; + } + + final StringBuilder wrapped = new StringBuilder(words[0]); + int spaceLeft = maxWidth - getTextWidth(metrics, wrapped.toString()); + + for (int i = 1; i < words.length; i++) + { + final String word = words[i]; + final int wordLen = getTextWidth(metrics, word); + final int spaceWidth = metrics.stringWidth(" "); + + if (wordLen + spaceWidth > spaceLeft) + { + wrapped.append("\n").append(word); + spaceLeft = maxWidth - wordLen; + } + else + { + wrapped.append(" ").append(word); + spaceLeft -= spaceWidth + wordLen; + } + } + + return wrapped.toString().split("\n"); + } + + private static int getAlignedPosition(final String str, final TableAlignment alignment, final int columnWidth, final FontMetrics metrics) + { + final int stringWidth = getTextWidth(metrics, str); + int offset = 0; + + switch (alignment) + { + case LEFT: + break; + case CENTER: + offset = (columnWidth / 2) - (stringWidth / 2); + break; + case RIGHT: + offset = columnWidth - stringWidth; + break; + } + return offset; + } + + /** + * Returns the color for the specified table element. + * Priority order: cell->row->column->default + * @param row TableRow element + * @param colIndex column index + */ + private Color getCellColor(final TableRow row, final int colIndex) + { + final List rowElements = row.getElements(); + final TableElement cell = colIndex < rowElements.size() ? rowElements.get(colIndex) : EMPTY_ELEMENT; + final TableElement column = colIndex < columns.size() ? columns.get(colIndex) : EMPTY_ELEMENT; + + return firstNonNull( + cell.getColor(), + row.getRowColor(), + column.getColor(), + defaultColor); + } + + /** + * Returns the alignment for the specified table element. + * Priority order: cell->row->column->default + * @param row TableRow element + * @param colIndex column index + */ + private TableAlignment getCellAlignment(final TableRow row, final int colIndex) + { + final List rowElements = row.getElements(); + final TableElement cell = colIndex < rowElements.size() ? rowElements.get(colIndex) : EMPTY_ELEMENT; + final TableElement column = colIndex < columns.size() ? columns.get(colIndex) : EMPTY_ELEMENT; + + return firstNonNull( + cell.getAlignment(), + row.getRowAlignment(), + column.getAlignment(), + defaultAlignment); + } + + @SafeVarargs + private static T firstNonNull(@Nullable T... elements) + { + if (elements == null || elements.length == 0) + { + return null; + } + + int i = 0; + T cur = elements[0]; + while (cur == null && i < elements.length) + { + cur = elements[i]; + i++; + } + + return cur; + } + + // Helper functions for cleaner overlay code + public void addRow(@Nonnull final String... cells) + { + final List elements = new ArrayList<>(); + for (final String cell : cells) + { + elements.add(TableElement.builder().content(cell).build()); + } + + final TableRow row = TableRow.builder().build(); + row.setElements(elements); + + this.rows.add(row); + } + + public void addRows(@Nonnull final String[]... rows) + { + for (String[] row : rows) + { + addRow(row); + } + } + + public void addRows(@NonNull final TableRow... rows) + { + this.rows.addAll(Arrays.asList(rows)); + } + + public void setRows(@Nonnull final String[]... elements) + { + this.rows.clear(); + addRows(elements); + } + + public void setRows(@Nonnull final TableRow... elements) + { + this.rows.clear(); + this.rows.addAll(Arrays.asList(elements)); + } + + public void addColumn(@Nonnull final String col) + { + this.columns.add(TableElement.builder().content(col).build()); + } + + public void addColumns(@NonNull final TableElement... columns) + { + this.columns.addAll(Arrays.asList(columns)); + } + + public void setColumns(@Nonnull final TableElement... elements) + { + this.columns.clear(); + this.columns.addAll(Arrays.asList(elements)); + } + + public void setColumns(@Nonnull final String... columns) + { + this.columns.clear(); + for (String col : columns) + { + addColumn(col); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableElement.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableElement.java new file mode 100644 index 0000000000..8229f9533c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableElement.java @@ -0,0 +1,38 @@ +/* + * 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.ui.overlay.components.table; + +import java.awt.Color; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TableElement +{ + TableAlignment alignment; + Color color; + String content; +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableRow.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableRow.java new file mode 100644 index 0000000000..8879b08c5e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/table/TableRow.java @@ -0,0 +1,41 @@ +/* + * 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.ui.overlay.components.table; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TableRow +{ + Color rowColor; + TableAlignment rowAlignment; + @Builder.Default + List elements = new ArrayList<>(); +} diff --git a/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/table/TableComponentTest.java b/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/table/TableComponentTest.java new file mode 100644 index 0000000000..1c7e95d37a --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/table/TableComponentTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2018, Jordan Atwood + * 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.ui.overlay.components.table; + +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import org.mockito.Mock; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TableComponentTest +{ + @Mock + private Graphics2D graphics; + + @Before + public void before() + { + when(graphics.getFontMetrics()).thenReturn(mock(FontMetrics.class)); + } + + @Test + public void testRender() + { + TableComponent tableComponent = new TableComponent(); + tableComponent.addRow("test"); + tableComponent.setDefaultAlignment(TableAlignment.CENTER); + tableComponent.setDefaultColor(Color.RED); + tableComponent.render(graphics); + verify(graphics, times(2)).drawString(eq("test"), anyInt(), anyInt()); + verify(graphics, atLeastOnce()).setColor(Color.RED); + } + + @Test + public void testColors() + { + TableComponent tableComponent = new TableComponent(); + tableComponent.addRow("test", "test", "test", "test", "test"); + tableComponent.setColumns("", "", ""); + List elements = tableComponent.getColumns(); + elements.get(0).setColor(Color.RED); + elements.get(1).setColor(Color.GREEN); + elements.get(2).setColor(Color.BLUE); + tableComponent.render(graphics); + verify(graphics, atLeastOnce()).setColor(Color.RED); + verify(graphics, atLeastOnce()).setColor(Color.GREEN); + verify(graphics, atLeastOnce()).setColor(Color.BLUE); + verify(graphics, atLeastOnce()).setColor(Color.YELLOW); + verify(graphics, atLeastOnce()).setColor(Color.WHITE); + } +} From 8e34edd4a09b803e821a3183fb417a4e8fbddbaa Mon Sep 17 00:00:00 2001 From: TheStonedTurtle Date: Wed, 17 Apr 2019 21:52:06 -0700 Subject: [PATCH 4/5] Add performance stats plugin --- .../plugins/performancestats/Performance.java | 108 +++++ .../PerformanceStatsConfig.java | 44 ++ .../PerformanceStatsOverlay.java | 109 +++++ .../PerformanceStatsPlugin.java | 384 ++++++++++++++++++ .../src/main/scripts/FakeXPDrops.hash | 1 + .../src/main/scripts/FakeXPDrops.rs2asm | 256 ++++++++++++ 6 files changed, 902 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java create mode 100644 runelite-client/src/main/scripts/FakeXPDrops.hash create mode 100644 runelite-client/src/main/scripts/FakeXPDrops.rs2asm diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java new file mode 100644 index 0000000000..2f7967f8f1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java @@ -0,0 +1,108 @@ +/* + * 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.plugins.performancestats; + +import lombok.Getter; +import lombok.Setter; + +@Getter +class Performance +{ + private static final double TICK_LENGTH = 0.6; + + String username; + + double damageDealt = 0; + double highestHitDealt = 0; + + double damageTaken = 0; + double highestHitTaken = 0; + + int lastActivityTick = -1; + @Setter + double ticksSpent = 0; + + void addDamageDealt(double a, int currentTick) + { + damageDealt += a; + if (a > highestHitDealt) + { + highestHitDealt = a; + } + + this.lastActivityTick = currentTick; + } + + void addDamageTaken(double a, int currentTick) + { + damageTaken += a; + if (a > highestHitTaken) + { + highestHitTaken = a; + } + + this.lastActivityTick = currentTick; + } + + void incrementTicksSpent() + { + ticksSpent++; + } + + void reset() + { + damageDealt = 0; + highestHitDealt = 0; + damageTaken = 0; + highestHitTaken = 0; + lastActivityTick = -1; + ticksSpent = 0; + } + + double getSecondsSpent() + { + return Math.round(this.ticksSpent * TICK_LENGTH); + } + + double getDPS() + { + return Math.round( (this.damageDealt / this.getSecondsSpent()) * 100) / 100.00; + } + + String getHumanReadableSecondsSpent() + { + final double secondsSpent = getSecondsSpent(); + if (secondsSpent <= 60) + { + return String.format("%2.0f", secondsSpent) + "s"; + } + + final double s = secondsSpent % 3600 % 60; + final double m = Math.floor(secondsSpent % 3600 / 60); + final double h = Math.floor(secondsSpent / 3600); + + return h < 1 ? String.format("%2.0f:%02.0f", m, s) : String.format("%2.0f:%02.0f:%02.0f", h, m, s); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsConfig.java new file mode 100644 index 0000000000..1c6aea301d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsConfig.java @@ -0,0 +1,44 @@ +/* + * 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.plugins.performancestats; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("performancestats") +public interface PerformanceStatsConfig extends Config +{ + @ConfigItem( + position = 0, + keyName = "submitTimeout", + name = "Submit Timeout (seconds)", + description = "Submits after this many seconds of inactivity" + ) + default int submitTimeout() + { + return 30; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java new file mode 100644 index 0000000000..63e94e2fc0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018, 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.plugins.performancestats; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.inject.Inject; +import net.runelite.api.MenuAction; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.components.ComponentConstants; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.components.table.TableAlignment; +import net.runelite.client.ui.overlay.components.table.TableComponent; + +public class PerformanceStatsOverlay extends Overlay +{ + private static final String TARGET = "Performance Stats"; + private static final String[] COLUMNS = { + "Player", "Dealt", "Taken", "DPS", "Elapsed" + }; + + private final PerformanceStatsPlugin tracker; + private final PanelComponent panelComponent = new PanelComponent(); + private final TableComponent tableComponent = new TableComponent(); + + @Inject + PerformanceStatsOverlay(PerformanceStatsPlugin tracker) + { + super(tracker); + setPosition(OverlayPosition.TOP_RIGHT); + setPriority(OverlayPriority.LOW); + this.tracker = tracker; + + getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Pause", TARGET)); + getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Reset", TARGET)); + getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Submit", TARGET)); + + panelComponent.setPreferredSize(new Dimension(350, 0)); + panelComponent.setBackgroundColor(ComponentConstants.STANDARD_BACKGROUND_COLOR); + + tableComponent.setDefaultAlignment(TableAlignment.CENTER); + tableComponent.setColumns(COLUMNS); + + panelComponent.getChildren().add(tableComponent); + } + + @Override + public String getName() + { + return TARGET; + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!tracker.isEnabled()) + { + return null; + } + + final Performance performance = tracker.getPerformance(); + graphics.setColor(Color.WHITE); + + tableComponent.getRows().clear(); + + final String[] rowElements = createRowElements(performance); + tableComponent.addRow(rowElements); + + return panelComponent.render(graphics); + } + + private String[] createRowElements(Performance performance) + { + return new String[] + { + performance.getUsername(), + String.valueOf((int) Math.round(performance.getDamageDealt())) + " | " + String.valueOf((int) Math.round(performance.getHighestHitDealt())), + String.valueOf((int) Math.round(performance.getDamageTaken())) + " | " + String.valueOf((int) Math.round(performance.getHighestHitTaken())), + String.valueOf(performance.getDPS()), + performance.getHumanReadableSecondsSpent() + }; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java new file mode 100644 index 0000000000..37b70eb03d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2018, 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.plugins.performancestats; + +import com.google.inject.Provides; +import java.text.DecimalFormat; +import javax.inject.Inject; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Skill; +import net.runelite.api.WorldType; +import net.runelite.api.events.ExperienceChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.ScriptCallbackEvent; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.OverlayMenuClicked; +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 = "Performance Stats", + description = "Displays your current performance stats", + tags = {"performance", "stats", "dps", "damage", "combat"}, + enabledByDefault = false +) +@Slf4j +public class PerformanceStatsPlugin extends Plugin +{ + // For every damage point dealt 1.33 experience is given to the player's hitpoints (base rate) + private static final double HITPOINT_RATIO = 1.33; + private static final double DMM_MULTIPLIER_RATIO = 10; + + private static final double GAME_TICK_SECONDS = 0.6; + private static final DecimalFormat numberFormat = new DecimalFormat("#,###"); + + @Inject + private Client client; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private PerformanceStatsConfig config; + + @Inject + private PerformanceStatsOverlay performanceTrackerOverlay; + + @Inject + private OverlayManager overlayManager; + + @Inject + private NPCManager npcManager; + + @Getter + private boolean enabled = false; + @Getter + private boolean paused = false; + @Getter + private final Performance performance = new Performance(); + + // Keep track of actor last tick as sometimes getInteracting can return null when hp xp event is triggered + // as the player clicked away at the perfect time + private Actor oldTarget; + private double hpExp; + private boolean hopping; + private int pausedTicks = 0; + + @Provides + PerformanceStatsConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(PerformanceStatsConfig.class); + } + + @Override + protected void startUp() + { + overlayManager.add(performanceTrackerOverlay); + } + + @Override + protected void shutDown() + { + overlayManager.remove(performanceTrackerOverlay); + disable(); + reset(); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOGIN_SCREEN: + disable(); + break; + case HOPPING: + hopping = true; + break; + } + } + + @Subscribe + public void onHitsplatApplied(HitsplatApplied e) + { + if (isPaused()) + { + return; + } + + if (e.getActor().equals(client.getLocalPlayer())) + { + // Auto enables when hitsplat is applied to player + if (!isEnabled()) + { + enable(); + } + + performance.addDamageTaken(e.getHitsplat().getAmount(), client.getTickCount()); + } + } + + @Subscribe + public void onExperienceChanged(ExperienceChanged c) + { + if (isPaused() || hopping) + { + return; + } + + if (c.getSkill().equals(Skill.HITPOINTS)) + { + final double oldExp = hpExp; + hpExp = client.getSkillExperience(Skill.HITPOINTS); + + // Ignore initial login + if (client.getTickCount() < 2) + { + return; + } + + final double diff = hpExp - oldExp; + if (diff < 1) + { + return; + } + + // Auto enables when player receives hp exp + if (!isEnabled()) + { + enable(); + } + + final double damageDealt = calculateDamageDealt(diff); + performance.addDamageDealt(damageDealt, client.getTickCount()); + } + } + + @Subscribe + public void onScriptCallbackEvent(ScriptCallbackEvent e) + { + // Handles Fake XP drops (Ironman in PvP, DMM Cap, 200m xp, etc) + if (isPaused()) + { + return; + } + + if (!"fakeXpDrop".equals(e.getEventName())) + { + return; + } + + final int[] intStack = client.getIntStack(); + final int intStackSize = client.getIntStackSize(); + + final int skillId = intStack[intStackSize - 2]; + final Skill skill = Skill.values()[skillId]; + if (skill.equals(Skill.HITPOINTS)) + { + // Auto enables when player would have received hp exp + if (!isEnabled()) + { + enable(); + } + + final int exp = intStack[intStackSize - 1]; + performance.addDamageDealt(calculateDamageDealt(exp), client.getTickCount()); + } + } + + @Subscribe + public void onGameTick(GameTick t) + { + oldTarget = client.getLocalPlayer().getInteracting(); + + if (!isEnabled()) + { + return; + } + + if (isPaused()) + { + pausedTicks++; + return; + } + + performance.incrementTicksSpent(); + hopping = false; + + final int timeout = config.submitTimeout(); + if (timeout > 0) + { + final double tickTimeout = timeout / GAME_TICK_SECONDS; + final int activityDiff = (client.getTickCount() - pausedTicks) - performance.getLastActivityTick(); + if (activityDiff > tickTimeout) + { + // offset the tracker time to account for idle timeout + // Leave an additional tick to pad elapsed time + final double offset = tickTimeout - GAME_TICK_SECONDS; + performance.setTicksSpent(performance.getTicksSpent() - offset); + + submit(); + } + } + } + + @Subscribe + public void onOverlayMenuClicked(OverlayMenuClicked c) + { + if (!c.getOverlay().equals(performanceTrackerOverlay)) + { + return; + } + + switch (c.getEntry().getOption()) + { + case "Pause": + togglePaused(); + break; + case "Reset": + reset(); + break; + case "Submit": + submit(); + break; + } + } + + private void enable() + { + this.enabled = true; + hpExp = client.getSkillExperience(Skill.HITPOINTS); + } + + private void disable() + { + this.enabled = false; + } + + private void togglePaused() + { + this.paused = !this.paused; + } + + private void reset() + { + this.enabled = false; + this.paused = false; + + this.performance.reset(); + pausedTicks = 0; + } + + private void submit() + { + final String message = createPerformanceMessage(performance); + + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .runeLiteFormattedMessage(message) + .build()); + + reset(); + } + + /** + * Calculates damage dealt based on HP xp gained accounting for multipliers such as DMM mode + * @param diff HP xp gained + * @return damage dealt + */ + private double calculateDamageDealt(double diff) + { + double damageDealt = diff / HITPOINT_RATIO; + // DeadMan mode has an XP modifier + if (client.getWorldType().contains(WorldType.DEADMAN)) + { + damageDealt = damageDealt / DMM_MULTIPLIER_RATIO; + } + + // Some NPCs have an XP modifier, account for it here. + Actor a = client.getLocalPlayer().getInteracting(); + if (!(a instanceof NPC)) + { + // If we are interacting with nothing we may have clicked away at the perfect time fall back to last tick + if (!(oldTarget instanceof NPC)) + { + log.warn("Couldn't find current or past target for experienced gain..."); + return damageDealt; + } + + a = oldTarget; + } + + final int npcId = ((NPC) a).getId(); + return damageDealt / npcManager.getXpModifier(npcId); + } + + private String createPerformanceMessage(final Performance p) + { + // Expected result: Damage Dealt: ## (Max: ##), Damage Taken: ## (Max: ##), Time Spent: ##:## (DPS: ##.##) + return new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Damage dealt: ") + .append(ChatColorType.HIGHLIGHT) + .append(numberFormat.format(p.getDamageDealt())) + .append(ChatColorType.NORMAL) + .append(" (Max: ") + .append(ChatColorType.HIGHLIGHT) + .append(numberFormat.format(p.getHighestHitDealt())) + .append(ChatColorType.NORMAL) + .append("), Damage Taken: ") + .append(ChatColorType.HIGHLIGHT) + .append(numberFormat.format(p.getDamageTaken())) + .append(ChatColorType.NORMAL) + .append(" (Max: ") + .append(ChatColorType.HIGHLIGHT) + .append(numberFormat.format(p.getHighestHitTaken())) + .append(ChatColorType.NORMAL) + .append("), Time Spent: ") + .append(ChatColorType.HIGHLIGHT) + .append(p.getHumanReadableSecondsSpent()) + .append(ChatColorType.NORMAL) + .append(" (DPS: ") + .append(ChatColorType.HIGHLIGHT) + .append(String.valueOf(p.getDPS())) + .append(ChatColorType.NORMAL) + .append(")") + .build(); + } +} diff --git a/runelite-client/src/main/scripts/FakeXPDrops.hash b/runelite-client/src/main/scripts/FakeXPDrops.hash new file mode 100644 index 0000000000..cf5e37e931 --- /dev/null +++ b/runelite-client/src/main/scripts/FakeXPDrops.hash @@ -0,0 +1 @@ +32FBC48F8C6D8E62E02BCF09F444BA036F76133B6596396F0AB9E474687D9F3F \ No newline at end of file diff --git a/runelite-client/src/main/scripts/FakeXPDrops.rs2asm b/runelite-client/src/main/scripts/FakeXPDrops.rs2asm new file mode 100644 index 0000000000..0a61b8b13a --- /dev/null +++ b/runelite-client/src/main/scripts/FakeXPDrops.rs2asm @@ -0,0 +1,256 @@ +.id 2091 +.int_stack_count 2 +.string_stack_count 0 +.int_var_count 2 +.string_var_count 0 + iload 0 + iload 1 + sconst "fakeXpDrop" + runelite_callback ; + pop_int + pop_int + iconst 105 + iconst 83 + iconst 681 + get_varc_int 207 + coordx + enum + iload 0 + if_icmpeq LABEL9 + jump LABEL16 +LABEL9: + get_varc_int 207 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 207 + jump LABEL216 +LABEL16: + iconst 105 + iconst 83 + iconst 681 + get_varc_int 208 + coordx + enum + iload 0 + if_icmpeq LABEL25 + jump LABEL32 +LABEL25: + get_varc_int 208 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 208 + jump LABEL216 +LABEL32: + iconst 105 + iconst 83 + iconst 681 + get_varc_int 209 + coordx + enum + iload 0 + if_icmpeq LABEL41 + jump LABEL48 +LABEL41: + get_varc_int 209 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 209 + jump LABEL216 +LABEL48: + iconst 105 + iconst 83 + iconst 681 + get_varc_int 210 + coordx + enum + iload 0 + if_icmpeq LABEL57 + jump LABEL64 +LABEL57: + get_varc_int 210 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 210 + jump LABEL216 +LABEL64: + iconst 105 + iconst 83 + iconst 681 + get_varc_int 211 + coordx + enum + iload 0 + if_icmpeq LABEL73 + jump LABEL80 +LABEL73: + get_varc_int 211 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 211 + jump LABEL216 +LABEL80: + iconst 105 + iconst 83 + iconst 681 + get_varc_int 212 + coordx + enum + iload 0 + if_icmpeq LABEL89 + jump LABEL96 +LABEL89: + get_varc_int 212 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 212 + jump LABEL216 +LABEL96: + iconst 105 + iconst 83 + iconst 681 + get_varc_int 213 + coordx + enum + iload 0 + if_icmpeq LABEL105 + jump LABEL112 +LABEL105: + get_varc_int 213 + iconst 0 + iconst 0 + iload 1 + movecoord + set_varc_int 213 + jump LABEL216 +LABEL112: + get_varc_int 207 + iconst -1 + if_icmpeq LABEL116 + jump LABEL127 +LABEL116: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 207 + jump LABEL216 +LABEL127: + get_varc_int 208 + iconst -1 + if_icmpeq LABEL131 + jump LABEL142 +LABEL131: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 208 + jump LABEL216 +LABEL142: + get_varc_int 209 + iconst -1 + if_icmpeq LABEL146 + jump LABEL157 +LABEL146: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 209 + jump LABEL216 +LABEL157: + get_varc_int 210 + iconst -1 + if_icmpeq LABEL161 + jump LABEL172 +LABEL161: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 210 + jump LABEL216 +LABEL172: + get_varc_int 211 + iconst -1 + if_icmpeq LABEL176 + jump LABEL187 +LABEL176: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 211 + jump LABEL216 +LABEL187: + get_varc_int 212 + iconst -1 + if_icmpeq LABEL191 + jump LABEL202 +LABEL191: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 212 + jump LABEL216 +LABEL202: + get_varc_int 213 + iconst -1 + if_icmpeq LABEL206 + jump LABEL216 +LABEL206: + iconst 0 + iconst 83 + iconst 105 + iconst 81 + iload 0 + enum + iconst 0 + iload 1 + movecoord + set_varc_int 213 +LABEL216: + return From 3da6a2cd390b6d820a2fbf970efc88922d225c7d Mon Sep 17 00:00:00 2001 From: TheStonedTurtle Date: Wed, 17 Apr 2019 22:11:47 -0700 Subject: [PATCH 5/5] Add party support --- .../plugins/performancestats/Performance.java | 4 +- .../PerformanceStatsOverlay.java | 11 ++++ .../PerformanceStatsPlugin.java | 66 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java index 2f7967f8f1..cc81fa1bfb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/Performance.java @@ -26,12 +26,14 @@ package net.runelite.client.plugins.performancestats; import lombok.Getter; import lombok.Setter; +import net.runelite.http.api.ws.messages.party.PartyMemberMessage; @Getter -class Performance +class Performance extends PartyMemberMessage { private static final double TICK_LENGTH = 0.6; + @Setter String username; double damageDealt = 0; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java index 63e94e2fc0..c2d2961034 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsOverlay.java @@ -92,6 +92,17 @@ public class PerformanceStatsOverlay extends Overlay final String[] rowElements = createRowElements(performance); tableComponent.addRow(rowElements); + for (Performance p : tracker.getPartyDataMap().values()) + { + if (p.getMemberId().equals(performance.getMemberId())) + { + continue; + } + + final String[] eles = createRowElements(p); + tableComponent.addRow(eles); + } + return panelComponent.render(graphics); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java index 37b70eb03d..4eac3aa1ed 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/performancestats/PerformanceStatsPlugin.java @@ -26,6 +26,10 @@ package net.runelite.client.plugins.performancestats; import com.google.inject.Provides; import java.text.DecimalFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import javax.inject.Inject; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -47,10 +51,17 @@ import net.runelite.client.chat.QueuedMessage; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.OverlayMenuClicked; +import net.runelite.client.events.PartyChanged; 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; +import net.runelite.client.util.Text; +import net.runelite.client.ws.PartyMember; +import net.runelite.client.ws.PartyService; +import net.runelite.client.ws.WSClient; +import net.runelite.http.api.ws.messages.party.UserPart; +import net.runelite.http.api.ws.messages.party.UserSync; @PluginDescriptor( name = "Performance Stats", @@ -86,6 +97,12 @@ public class PerformanceStatsPlugin extends Plugin @Inject private NPCManager npcManager; + @Inject + private PartyService partyService; + + @Inject + private WSClient wsClient; + @Getter private boolean enabled = false; @Getter @@ -100,6 +117,10 @@ public class PerformanceStatsPlugin extends Plugin private boolean hopping; private int pausedTicks = 0; + // Party System + @Getter + private final Map partyDataMap = Collections.synchronizedMap(new HashMap<>()); + @Provides PerformanceStatsConfig getConfig(ConfigManager configManager) { @@ -110,12 +131,14 @@ public class PerformanceStatsPlugin extends Plugin protected void startUp() { overlayManager.add(performanceTrackerOverlay); + wsClient.registerMessage(Performance.class); } @Override protected void shutDown() { overlayManager.remove(performanceTrackerOverlay); + wsClient.unregisterMessage(Performance.class); disable(); reset(); } @@ -256,6 +279,10 @@ public class PerformanceStatsPlugin extends Plugin submit(); } } + + final String name = client.getLocalPlayer().getName(); + performance.setUsername(Text.removeTags(name)); + sendPerformance(); } @Subscribe @@ -381,4 +408,43 @@ public class PerformanceStatsPlugin extends Plugin .append(")") .build(); } + + private void sendPerformance() + { + final PartyMember me = partyService.getLocalMember(); + if (me != null && me.getMemberId() != null) + { + performance.setMemberId(me.getMemberId()); + wsClient.send(performance); + } + } + + @Subscribe + public void onPerformance(final Performance performance) + { + partyDataMap.put(performance.getMemberId(), performance); + } + + @Subscribe + public void onUserSync(final UserSync event) + { + if (isEnabled()) + { + sendPerformance(); + } + } + + @Subscribe + public void onUserPart(final UserPart event) + { + partyDataMap.remove(event.getMemberId()); + } + + @Subscribe + public void onPartyChanged(final PartyChanged event) + { + // Reset party + partyDataMap.clear(); + } + }