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 new file mode 100644 index 0000000000..f34d2c13ee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.game; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.Map; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class NPCManager +{ + private final Map healthMap; + + @Inject + private NPCManager() + { + final Gson gson = new Gson(); + final Type typeToken = new TypeToken>() + { + }.getType(); + + final InputStream healthFile = getClass().getResourceAsStream("/npc_health.json"); + healthMap = gson.fromJson(new InputStreamReader(healthFile), typeToken); + } + + /** + * 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 + */ + @Nullable + public Integer getHealth(final String name, final int combatLevel) + { + return healthMap.get(name + "_" + combatLevel); + } +} 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 38ac7ac9f0..6c8cd97b69 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 @@ -38,6 +38,7 @@ import net.runelite.api.NPC; import net.runelite.api.Player; import net.runelite.api.Varbits; import net.runelite.client.game.HiscoreManager; +import net.runelite.client.game.NPCManager; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayPriority; @@ -57,6 +58,7 @@ class OpponentInfoOverlay extends Overlay private final OpponentInfoPlugin opponentInfoPlugin; private final OpponentInfoConfig opponentInfoConfig; private final HiscoreManager hiscoreManager; + private final NPCManager npcManager; private final PanelComponent panelComponent = new PanelComponent(); @@ -67,13 +69,18 @@ class OpponentInfoOverlay extends Overlay private String opponentsOpponentName; @Inject - private OpponentInfoOverlay(Client client, OpponentInfoPlugin opponentInfoPlugin, - OpponentInfoConfig opponentInfoConfig, HiscoreManager hiscoreManager) + private OpponentInfoOverlay( + Client client, + OpponentInfoPlugin opponentInfoPlugin, + OpponentInfoConfig opponentInfoConfig, + HiscoreManager hiscoreManager, + NPCManager npcManager) { this.client = client; this.opponentInfoPlugin = opponentInfoPlugin; this.opponentInfoConfig = opponentInfoConfig; this.hiscoreManager = hiscoreManager; + this.npcManager = npcManager; setPosition(OverlayPosition.TOP_LEFT); setPriority(OverlayPriority.HIGH); @@ -102,7 +109,7 @@ class OpponentInfoOverlay extends Overlay lastMaxHealth = null; if (opponent instanceof NPC) { - lastMaxHealth = opponentInfoPlugin.getOppInfoHealth().get(opponentName + "_" + opponent.getCombatLevel()); + lastMaxHealth = npcManager.getHealth(opponentName, opponent.getCombatLevel()); } else if (opponent instanceof Player) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java index a8e5694a6d..1f38dc136d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/opponentinfo/OpponentInfoPlugin.java @@ -26,16 +26,10 @@ package net.runelite.client.plugins.opponentinfo; import com.google.common.eventbus.Subscribe; -import com.google.common.reflect.TypeToken; -import com.google.gson.Gson; import com.google.inject.Provides; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Type; import java.time.Duration; import java.time.Instant; import java.util.EnumSet; -import java.util.Map; import javax.inject.Inject; import lombok.AccessLevel; import lombok.Getter; @@ -84,9 +78,6 @@ public class OpponentInfoPlugin extends Plugin private Instant lastTime; - @Getter(AccessLevel.PACKAGE) - private final Map oppInfoHealth = loadNpcHealth(); - @Provides OpponentInfoConfig provideConfig(ConfigManager configManager) { @@ -164,15 +155,4 @@ public class OpponentInfoPlugin extends Plugin } } } - - private Map loadNpcHealth() - { - Gson gson = new Gson(); - Type type = new TypeToken>() - { - }.getType(); - - InputStream healthFile = OpponentInfoPlugin.class.getResourceAsStream("/npc_health.json"); - return gson.fromJson(new InputStreamReader(healthFile), type); - } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpAction.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpAction.java new file mode 100644 index 0000000000..4831c47cbf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpAction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.xptracker; + +import lombok.Data; + +@Data +class XpAction +{ + private int actions = 0; + private boolean actionsHistoryInitialized = false; + private int[] actionExps = new int[10]; + private int actionExpIndex = 0; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpActionType.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpActionType.java new file mode 100644 index 0000000000..5c23ac39d8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpActionType.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Tomas Slusny + * 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.xptracker; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +enum XpActionType +{ + EXPERIENCE("Actions"), + ACTOR_HEALTH("Kills"); + + private final String label; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java index 17c1539f4e..7baf54a3ca 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java @@ -59,8 +59,8 @@ class XpInfoBox extends JPanel // Templates private static final String HTML_TOOL_TIP_TEMPLATE = - "%s actions done
" - + "%s actions/hr
" + "%s %s done
" + + "%s %s/hr
" + "%s till goal lvl"; private static final String HTML_LABEL_TEMPLATE = "%s%s"; @@ -197,7 +197,7 @@ class XpInfoBox extends JPanel // Update information labels expGained.setText(htmlLabel("XP Gained: ", xpSnapshotSingle.getXpGainedInSession())); expLeft.setText(htmlLabel("XP Left: ", xpSnapshotSingle.getXpRemainingToGoal())); - actionsLeft.setText(htmlLabel("Actions: ", xpSnapshotSingle.getActionsRemainingToGoal())); + actionsLeft.setText(htmlLabel(xpSnapshotSingle.getActionType().getLabel() + ": ", xpSnapshotSingle.getActionsRemainingToGoal())); // Update progress bar progressBar.setValue((int) xpSnapshotSingle.getSkillProgressToGoal()); @@ -210,7 +210,9 @@ class XpInfoBox extends JPanel progressBar.setToolTipText(String.format( HTML_TOOL_TIP_TEMPLATE, xpSnapshotSingle.getActionsInSession(), + xpSnapshotSingle.getActionType().getLabel(), xpSnapshotSingle.getActionsPerHour(), + xpSnapshotSingle.getActionType().getLabel(), xpSnapshotSingle.getTimeTillGoal())); progressBar.setDimmed(skillPaused); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java index cc087b6bec..bb7f8a5ef7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java @@ -31,6 +31,7 @@ import lombok.Value; @Value class XpSnapshotSingle { + private XpActionType actionType; private int startLevel; private int endLevel; private int startGoalXp; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java index da981077ef..616536bd1e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java @@ -27,6 +27,7 @@ package net.runelite.client.plugins.xptracker; import java.util.EnumMap; import java.util.Map; import lombok.NonNull; +import net.runelite.api.NPC; import net.runelite.api.Skill; /** @@ -37,8 +38,11 @@ import net.runelite.api.Skill; */ class XpState { + private static final double DEFAULT_XP_MODIFIER = 4.0; + private static final double SHARED_XP_MODIFIER = DEFAULT_XP_MODIFIER / 3.0; private final XpStateTotal xpTotal = new XpStateTotal(); private final Map xpSkills = new EnumMap<>(Skill.class); + private NPC interactedNPC; /** * Destroys all internal state, however any XpSnapshotSingle or XpSnapshotTotal remain unaffected. @@ -120,6 +124,79 @@ class XpState } } + private double getCombatXPModifier(Skill skill) + { + if (skill == Skill.HITPOINTS) + { + return SHARED_XP_MODIFIER; + } + + return DEFAULT_XP_MODIFIER; + } + + /** + * Updates skill with average actions based on currently interacted NPC. + * @param skill experience gained skill + * @param npc currently interacted NPC + * @param npcHealth health of currently interacted NPC + */ + void updateNpcExperience(Skill skill, NPC npc, Integer npcHealth) + { + if (npc == null || npc.getCombatLevel() <= 0 || npcHealth == null) + { + return; + } + + final XpStateSingle state = getSkill(skill); + final int actionExp = (int) (npcHealth * getCombatXPModifier(skill)); + final XpAction action = state.getXpAction(XpActionType.ACTOR_HEALTH); + + if (action.isActionsHistoryInitialized()) + { + action.getActionExps()[action.getActionExpIndex()] = actionExp; + + if (interactedNPC != npc) + { + action.setActionExpIndex((action.getActionExpIndex() + 1) % action.getActionExps().length); + } + } + else + { + // So we have a decent average off the bat, lets populate all values with what we see. + for (int i = 0; i < action.getActionExps().length; i++) + { + action.getActionExps()[i] = actionExp; + } + + action.setActionsHistoryInitialized(true); + } + + interactedNPC = npc; + state.setActionType(XpActionType.ACTOR_HEALTH); + } + + /** + * Update number of actions performed for skill (e.g amount of kills in this case) if last interacted + * NPC died + * @param skill skill to update actions for + * @param npc npc that just died + * @param npcHealth max health of npc that just died + * @return UPDATED in case new kill was successfully added + */ + XpUpdateResult updateNpcKills(Skill skill, NPC npc, Integer npcHealth) + { + XpStateSingle state = getSkill(skill); + + if (state.getStartXp() == -1 || npcHealth == null || npc != interactedNPC) + { + return XpUpdateResult.NO_CHANGE; + } + + final XpAction xpAction = state.getXpAction(XpActionType.ACTOR_HEALTH); + xpAction.setActions(xpAction.getActions() + 1); + return xpAction.isActionsHistoryInitialized() ? XpUpdateResult.UPDATED : XpUpdateResult.NO_CHANGE; + } + void tick(Skill skill, long delta) { getSkill(skill).tick(delta); @@ -137,7 +214,7 @@ class XpState } @NonNull - private XpStateSingle getSkill(Skill skill) + XpStateSingle getSkill(Skill skill) { return xpSkills.computeIfAbsent(skill, (s) -> new XpStateSingle(s, -1)); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java index 44e1d079ca..22dfd1b646 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java @@ -25,8 +25,11 @@ */ package net.runelite.client.plugins.xptracker; +import java.util.HashMap; +import java.util.Map; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Experience; import net.runelite.api.Skill; @@ -36,6 +39,7 @@ import net.runelite.api.Skill; class XpStateSingle { private final Skill skill; + private final Map actions = new HashMap<>(); @Getter private final int startXp; @@ -43,13 +47,18 @@ class XpStateSingle @Getter private int xpGained = 0; + @Setter + private XpActionType actionType = XpActionType.EXPERIENCE; + private long skillTime = 0; - private int actions = 0; private int startLevelExp = 0; private int endLevelExp = 0; - private boolean actionsHistoryInitialized = false; - private int[] actionExps = new int[10]; - private int actionExpIndex = 0; + + XpAction getXpAction(final XpActionType type) + { + actions.putIfAbsent(type, new XpAction()); + return actions.get(type); + } private int getCurrentXp() { @@ -58,7 +67,7 @@ class XpStateSingle private int getActionsHr() { - return toHourly(actions); + return toHourly(getXpAction(actionType).getActions()); } private int toHourly(int value) @@ -92,12 +101,14 @@ class XpStateSingle private int getActionsRemaining() { - if (actionsHistoryInitialized) + final XpAction action = getXpAction(actionType); + + if (action.isActionsHistoryInitialized()) { - long xpRemaining = getXpRemaining() * actionExps.length; + long xpRemaining = getXpRemaining() * action.getActionExps().length; long totalActionXp = 0; - for (int actionXp : actionExps) + for (int actionXp : action.getActionExps()) { totalActionXp += actionXp; } @@ -186,23 +197,26 @@ class XpStateSingle return false; } - if (actionsHistoryInitialized) + // Update EXPERIENCE action + final XpAction action = getXpAction(XpActionType.EXPERIENCE); + + if (action.isActionsHistoryInitialized()) { - actionExps[actionExpIndex] = actionExp; + action.getActionExps()[action.getActionExpIndex()] = actionExp; } else { // So we have a decent average off the bat, lets populate all values with what we see. - for (int i = 0; i < actionExps.length; i++) + for (int i = 0; i < action.getActionExps().length; i++) { - actionExps[i] = actionExp; + action.getActionExps()[i] = actionExp; } - actionsHistoryInitialized = true; + action.setActionsHistoryInitialized(true); } - actionExpIndex = (actionExpIndex + 1) % actionExps.length; - actions++; + action.setActionExpIndex((action.getActionExpIndex() + 1) % action.getActionExps().length); + action.setActions(action.getActions() + 1); // Calculate experience gained xpGained = currentXp - startXp; @@ -251,7 +265,8 @@ class XpStateSingle .xpRemainingToGoal(getXpRemaining()) .xpPerHour(getXpHr()) .skillProgressToGoal(getSkillProgress()) - .actionsInSession(actions) + .actionType(actionType) + .actionsInSession(getXpAction(actionType).getActions()) .actionsRemainingToGoal(getActionsRemaining()) .actionsPerHour(getActionsHr()) .timeTillGoal(getTimeTillLevel()) 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 796e1ecbc1..f3ca559e4a 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 @@ -26,18 +26,22 @@ package net.runelite.client.plugins.xptracker; import static com.google.common.base.MoreObjects.firstNonNull; +import com.google.common.collect.ImmutableList; import com.google.common.eventbus.Subscribe; import com.google.inject.Binder; import com.google.inject.Provides; import java.awt.image.BufferedImage; import java.time.temporal.ChronoUnit; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.Experience; import net.runelite.api.GameState; +import net.runelite.api.NPC; import net.runelite.api.Player; import net.runelite.api.Skill; import net.runelite.api.VarPlayer; @@ -45,14 +49,16 @@ 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.NpcDespawned; import net.runelite.client.config.ConfigManager; +import net.runelite.client.game.NPCManager; import net.runelite.client.game.SkillIconManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import static net.runelite.client.plugins.xptracker.XpWorldType.NORMAL; import net.runelite.client.task.Schedule; -import net.runelite.client.ui.NavigationButton; import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.ImageUtil; import net.runelite.http.api.xp.XpClient; @@ -64,6 +70,14 @@ import net.runelite.http.api.xp.XpClient; @Slf4j public class XpTrackerPlugin extends Plugin { + static final List COMBAT = ImmutableList.of( + Skill.ATTACK, + Skill.STRENGTH, + Skill.DEFENCE, + Skill.RANGED, + Skill.HITPOINTS, + Skill.MAGIC); + @Inject private ClientToolbar clientToolbar; @@ -76,6 +90,9 @@ public class XpTrackerPlugin extends Plugin @Inject private XpTrackerConfig xpTrackerConfig; + @Inject + private NPCManager npcManager; + private NavigationButton navButton; private XpPanel xpPanel; private XpWorldType lastWorldType; @@ -242,14 +259,44 @@ public class XpTrackerPlugin extends Plugin return; } - final XpUpdateResult updateResult = xpState.updateSkill(skill, currentXp, startGoalXp, endGoalXp); + final XpStateSingle state = xpState.getSkill(skill); + state.setActionType(XpActionType.EXPERIENCE); + final Actor interacting = client.getLocalPlayer().getInteracting(); + if (interacting instanceof NPC) + { + final NPC npc = (NPC) interacting; + xpState.updateNpcExperience(skill, npc, npcManager.getHealth(npc.getName(), npc.getCombatLevel())); + } + + final XpUpdateResult updateResult = xpState.updateSkill(skill, currentXp, startGoalXp, endGoalXp); final boolean updated = XpUpdateResult.UPDATED.equals(updateResult); xpPanel.updateSkillExperience(updated, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); xpState.recalculateTotal(); xpPanel.updateTotal(xpState.getTotalSnapshot()); } + @Subscribe + public void onNpcDespawned(NpcDespawned event) + { + final NPC npc = event.getNpc(); + + if (!npc.isDead()) + { + return; + } + + for (Skill skill : COMBAT) + { + final XpUpdateResult updateResult = xpState.updateNpcKills(skill, npc, npcManager.getHealth(npc.getName(), npc.getCombatLevel())); + final boolean updated = XpUpdateResult.UPDATED.equals(updateResult); + xpPanel.updateSkillExperience(updated, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + } + + xpState.recalculateTotal(); + xpPanel.updateTotal(xpState.getTotalSnapshot()); + } + @Subscribe public void onGameTick(GameTick event) {