diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java index ec3d36114b..1d6709bc0a 100644 --- a/runelite-api/src/main/java/net/runelite/api/Varbits.java +++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java @@ -295,6 +295,15 @@ public enum Varbits THEATRE_OF_BLOOD(6440), BLOAT_DOOR(6447), + /** + * Theatre of Blood orb varbits each number stands for the player's health on a scale of 1-27 (I think), 0 hides the orb + */ + THEATRE_OF_BLOOD_ORB_1(6442), + THEATRE_OF_BLOOD_ORB_2(6443), + THEATRE_OF_BLOOD_ORB_3(6444), + THEATRE_OF_BLOOD_ORB_4(6445), + THEATRE_OF_BLOOD_ORB_5(6446), + /** * Nightmare Zone */ diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/Boss.java b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/Boss.java new file mode 100644 index 0000000000..b8ff7325cf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/Boss.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2018, Raqes + * 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.dpscounter; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Map; +import java.util.Set; +import lombok.Getter; +import lombok.ToString; +import net.runelite.api.NpcID; + +@ToString +enum Boss +{ + ABYSSAL_SIRE(1.25f, NpcID.ABYSSAL_SIRE, NpcID.ABYSSAL_SIRE_5887, NpcID.ABYSSAL_SIRE_5888, NpcID.ABYSSAL_SIRE_5889, NpcID.ABYSSAL_SIRE_5890, NpcID.ABYSSAL_SIRE_5891, NpcID.ABYSSAL_SIRE_5908), + CALLISTO(1.225f, NpcID.CALLISTO, NpcID.CALLISTO_6609), + CERBERUS(1.15f, NpcID.CERBERUS, NpcID.CERBERUS_5863, NpcID.CERBERUS_5866), + CHAOS_ELEMENTAL(1.075f, NpcID.CHAOS_ELEMENTAL, NpcID.CHAOS_ELEMENTAL_6505), + CORPOREAL_BEAST(1.55f, NpcID.CORPOREAL_BEAST), + GENERAL_GRAARDOR(1.325f, NpcID.GENERAL_GRAARDOR, NpcID.GENERAL_GRAARDOR_6494), + GIANT_MOLE(1.075f, NpcID.GIANT_MOLE, NpcID.GIANT_MOLE_6499), + KALPHITE_QUEEN(1.05f, NpcID.KALPHITE_QUEEN, NpcID.KALPHITE_QUEEN_963, NpcID.KALPHITE_QUEEN_965, NpcID.KALPHITE_QUEEN_4303, NpcID.KALPHITE_QUEEN_4304, NpcID.KALPHITE_QUEEN_6500, NpcID.KALPHITE_QUEEN_6501), + KING_BLACK_DRAGON(1.075f, NpcID.KING_BLACK_DRAGON, NpcID.KING_BLACK_DRAGON_2642, NpcID.KING_BLACK_DRAGON_6502), + KRIL_TSUROTH(1.375f, NpcID.KRIL_TSUTSAROTH, NpcID.KRIL_TSUTSAROTH_6495), + VENETENATIS(1.4f, NpcID.VENENATIS, NpcID.VENENATIS_6610), + VETION(1.225f, NpcID.VETION, NpcID.VETION_REBORN), + MAIDEN(1f, NpcID.THE_MAIDEN_OF_SUGADINTI, NpcID.THE_MAIDEN_OF_SUGADINTI_8361, NpcID.THE_MAIDEN_OF_SUGADINTI_8362, NpcID.THE_MAIDEN_OF_SUGADINTI_8363, NpcID.THE_MAIDEN_OF_SUGADINTI_8364, NpcID.THE_MAIDEN_OF_SUGADINTI_8365), + BLOAT(new float[]{1.7f, 1.775f, 1.85f}, NpcID.PESTILENT_BLOAT), + NYLOCAS_BOSS(new float[]{1.175f, 1.2f, 1.225f}, NpcID.NYLOCAS_VASILIAS, NpcID.NYLOCAS_VASILIAS_8355, NpcID.NYLOCAS_VASILIAS_8356, NpcID.NYLOCAS_VASILIAS_8357), + SOTETSEG(new float[]{1.525f, 1.6f, 1.675f}, NpcID.SOTETSEG, NpcID.SOTETSEG_8388), + XARPUS(1f, NpcID.XARPUS_8340, NpcID.XARPUS_8341), + VERZIK_P1(1.05f, NpcID.VERZIK_VITUR_8370), + VERZIK_P2(new float[]{1.35f, 1.4f, 1.425f}, NpcID.VERZIK_VITUR_8372), + VERZIK_P3(new float[]{1.675f, 1.75f, 1.85f}, NpcID.VERZIK_VITUR_8374); + + private static final Set TOB_BOSSES = ImmutableSet.of(MAIDEN, BLOAT, NYLOCAS_BOSS, SOTETSEG, XARPUS, VERZIK_P1, VERZIK_P2, VERZIK_P3); + + @Getter + private final int[] ids; + private final int[] minions; + private final float[] modifier; // Some NPCs have a modifier to the experience a player receives. + + Boss(float modifier, int... ids) + { + this.modifier = new float[]{modifier}; + this.ids = ids; + this.minions = null; + } + + Boss(float[] modifiers, int... ids) + { + this(modifiers, null, ids); + } + + Boss(float[] modifiers, int[] minions, int ... ids) + { + this.ids = ids; + this.modifier = modifiers; + this.minions = minions; + } + + float getModifier() + { + return modifier[0]; + } + + float getModifier(int partySize) + { + if (modifier.length == 1) + { + return modifier[0]; + } + + if (partySize == 5) + { + return modifier[2]; + } + else if (partySize == 4) + { + return modifier[1]; + } + else + { + return modifier[0]; + } + } + + private static final Map BOSS_MAP; + + static Boss findBoss(int id) + { + return BOSS_MAP.get(id); + } + + static boolean isTOB(Boss boss) + { + return TOB_BOSSES.contains(boss); + } + + static + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Boss boss : values()) + { + for (int id : boss.ids) + { + builder.put(id, boss); + } + } + BOSS_MAP = builder.build(); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsConfig.java new file mode 100644 index 0000000000..6845168f09 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsConfig.java @@ -0,0 +1,20 @@ +package net.runelite.client.plugins.dpscounter; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("dpscounter") +public interface DpsConfig extends Config +{ + @ConfigItem( + position = 0, + name = "Show Damage", + keyName = "showDamage", + description = "Show total damage instead of DPS" + ) + default boolean showDamage() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsCounterPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsCounterPlugin.java new file mode 100644 index 0000000000..79693838f4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsCounterPlugin.java @@ -0,0 +1,286 @@ +package net.runelite.client.plugins.dpscounter; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.Provides; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.MenuAction; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.Skill; +import net.runelite.api.Varbits; +import net.runelite.api.events.ExperienceChanged; +import net.runelite.api.events.InteractingChanged; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.OverlayMenuClicked; +import net.runelite.client.events.PartyChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ws.PartyMember; +import net.runelite.client.ws.PartyService; +import net.runelite.client.ws.WSClient; +import org.apache.commons.lang3.ArrayUtils; + +@PluginDescriptor( + name = "DPS Counter", + description = "counts dps?" +) +@Slf4j +public class DpsCounterPlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private OverlayManager overlayManager; + + @Inject + private PartyService partyService; + + @Inject + private WSClient wsClient; + + @Inject + private DpsOverlay dpsOverlay; + + static private final Set TOB_PARTY_ORBS_VARBITS = ImmutableSet.of(Varbits.THEATRE_OF_BLOOD_ORB_1, + Varbits.THEATRE_OF_BLOOD_ORB_2, Varbits.THEATRE_OF_BLOOD_ORB_3, Varbits.THEATRE_OF_BLOOD_ORB_4, + Varbits.THEATRE_OF_BLOOD_ORB_5); + + private Boss boss; + private NPC bossNpc; + private int lastHpExp = -1; + @Getter(AccessLevel.PACKAGE) + private final Map members = new ConcurrentHashMap<>(); + + @Provides + DpsConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(DpsConfig.class); + } + + @Override + protected void startUp() + { + overlayManager.add(dpsOverlay); + wsClient.registerMessage(DpsUpdate.class); + } + + @Override + protected void shutDown() + { + wsClient.unregisterMessage(DpsUpdate.class); + overlayManager.remove(dpsOverlay); + members.clear(); + boss = null; + } + + @Subscribe + public void onPartyChanged(PartyChanged partyChanged) + { + members.clear(); + } + + @Subscribe + public void onInteractingChanged(InteractingChanged interactingChanged) + { + Actor source = interactingChanged.getSource(); + Actor target = interactingChanged.getTarget(); + + if (source != client.getLocalPlayer()) + { + return; + } + + if (target instanceof NPC) + { + NPC npc = (NPC) target; + int npcId = npc.getId(); + Boss boss = Boss.findBoss(npcId); + if (boss != null) + { + this.boss = boss; + bossNpc = (NPC) target; + } + } + } + + @Subscribe + public void onExperienceChanged(ExperienceChanged experienceChanged) + { + if (experienceChanged.getSkill() != Skill.HITPOINTS) + { + return; + } + + final int xp = client.getSkillExperience(Skill.HITPOINTS); + if (boss == null || lastHpExp < 0 || xp <= lastHpExp) + { + lastHpExp = xp; + return; + } + + final int delta = xp - lastHpExp; + + float modifier; + if (Boss.isTOB(boss)) + { + int partySize = getTobPartySize(); + System.out.println(partySize); + modifier = boss.getModifier(partySize); + } + else + { + modifier = boss.getModifier(); + } + + final int hit = getHit(modifier, delta); + lastHpExp = xp; + + // Update local member + PartyMember localMember = partyService.getLocalMember(); + Player player = client.getLocalPlayer(); + // If not in a party, user local player name + final String name = localMember == null ? player.getName() : localMember.getName(); + DpsMember dpsMember = members.computeIfAbsent(name, n -> new DpsMember(name)); + dpsMember.addDamage(hit); + + if (dpsMember.isPaused()) + { + dpsMember.unpause(); + log.debug("Unpausing {}", dpsMember.getName()); + } + + if (hit > 0 && !partyService.getMembers().isEmpty()) + { + // Check the player is attacking the boss + if (bossNpc != null && player.getInteracting() == bossNpc) + { + final DpsUpdate specialCounterUpdate = new DpsUpdate(bossNpc.getId(), hit); + specialCounterUpdate.setMemberId(partyService.getLocalMember().getMemberId()); + wsClient.send(specialCounterUpdate); + } + } + } + + @Subscribe + public void onDpsUpdate(DpsUpdate dpsUpdate) + { + if (partyService.getLocalMember().getMemberId().equals(dpsUpdate.getMemberId())) + { + return; + } + + String name = partyService.getMemberById(dpsUpdate.getMemberId()).getName(); + if (name == null) + { + return; + } + + // Hmm - not attacking the same boss I am + if (bossNpc == null || dpsUpdate.getNpcId() != bossNpc.getId()) + { + return; + } + + DpsMember dpsMember = members.computeIfAbsent(name, DpsMember::new); + dpsMember.addDamage(dpsUpdate.getHit()); + + if (dpsMember.isPaused()) + { + dpsMember.unpause(); + log.debug("Unpausing {}", dpsMember.getName()); + } + } + + @Subscribe + public void onOverlayMenuClicked(OverlayMenuClicked event) + { + if (event.getEntry().getMenuAction() == MenuAction.RUNELITE_OVERLAY && + event.getEntry().getOption().equals("Reset") && + event.getEntry().getTarget().equals("DPS counter")) + { + members.clear(); + } + } + + @Subscribe + public void onNpcSpawned(NpcSpawned npcSpawned) + { + if (boss == null) + { + return; + } + + NPC npc = npcSpawned.getNpc(); + int npcId = npc.getId(); + if (!ArrayUtils.contains(boss.getIds(), npcId)) + { + return; + } + + log.debug("Boss has spawned!"); + bossNpc = npc; + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + if (bossNpc == null || npcDespawned.getNpc() != bossNpc) + { + return; + } + + if (bossNpc.isDead()) + { + log.debug("Boss has died!"); + pause(); + } + + bossNpc = null; + } + + private void pause() + { + for (DpsMember dpsMember : members.values()) + { + dpsMember.pause(); + } + } + + private int getHit(float modifier, int deltaExperience) + { + float modifierBase = 1f / modifier; + float damageOutput = (deltaExperience * modifierBase) / 1.3333f; + return Math.round(damageOutput); + } + + private int getTobPartySize() + { + int partySize = 0; + for (Varbits varbit : TOB_PARTY_ORBS_VARBITS) + { + if (client.getVar(varbit) != 0) + { + partySize++; + System.out.println(varbit.getId() + ": " + client.getVar(varbit)); + } + else + { + break; + } + } + return partySize; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsMember.java b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsMember.java new file mode 100644 index 0000000000..fd109479a0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsMember.java @@ -0,0 +1,54 @@ +package net.runelite.client.plugins.dpscounter; + +import java.time.Duration; +import java.time.Instant; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +class DpsMember +{ + private final String name; + private Instant start = Instant.now(); + private Instant end; + private int damage; + + void addDamage(int amount) + { + damage += amount; + } + + float getDps() + { + Instant now = end == null ? Instant.now() : end; + int diff = (int) (now.toEpochMilli() - start.toEpochMilli()) / 1000; + if (diff == 0) + { + return 0; + } + + return (float) damage / (float) diff; + } + + void pause() + { + end = Instant.now(); + } + + boolean isPaused() + { + return end != null; + } + + void unpause() + { + if (end == null) + { + return; + } + + start = start.plus(Duration.between(end, Instant.now())); + end = null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsOverlay.java new file mode 100644 index 0000000000..2d1f664fe7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsOverlay.java @@ -0,0 +1,66 @@ +package net.runelite.client.plugins.dpscounter; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.text.DecimalFormat; +import java.util.Map; +import javax.inject.Inject; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; +import net.runelite.client.ws.PartyService; + +public class DpsOverlay extends Overlay +{ + private static final DecimalFormat DPS_FORMAT = new DecimalFormat("#0.0"); + + private final DpsCounterPlugin dpsCounterPlugin; + private final DpsConfig dpsConfig; + private final PartyService partyService; + + private final PanelComponent panelComponent = new PanelComponent(); + + @Inject + DpsOverlay(DpsCounterPlugin dpsCounterPlugin, DpsConfig dpsConfig, PartyService partyService) + { + super(dpsCounterPlugin); + this.dpsCounterPlugin = dpsCounterPlugin; + this.dpsConfig = dpsConfig; + this.partyService = partyService; + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY, "Reset", "DPS counter")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + Map dpsMembers = dpsCounterPlugin.getMembers(); + if (dpsMembers.isEmpty()) + { + return null; + } + + boolean inParty = !partyService.getMembers().isEmpty(); + boolean showDamage = dpsConfig.showDamage(); + + panelComponent.getChildren().clear(); + + panelComponent.getChildren().add( + TitleComponent.builder() + .text(inParty ? "Party DPS" : "DPS") + .build()); + + for (DpsMember dpsMember : dpsMembers.values()) + { + panelComponent.getChildren().add( + LineComponent.builder() + .left(dpsMember.getName()) + .right(showDamage ? Integer.toString(dpsMember.getDamage()) : DPS_FORMAT.format(dpsMember.getDps())) + .build()); + } + + return panelComponent.render(graphics); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsUpdate.java b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsUpdate.java new file mode 100644 index 0000000000..5aa02da373 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/dpscounter/DpsUpdate.java @@ -0,0 +1,13 @@ +package net.runelite.client.plugins.dpscounter; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import net.runelite.http.api.ws.messages.party.PartyMemberMessage; + +@Value +@EqualsAndHashCode(callSuper = true) +public class DpsUpdate extends PartyMemberMessage +{ + private int npcId; + private int hit; +}