From 77ea6c61540d14ad12072e1f4e848d57f0e95936 Mon Sep 17 00:00:00 2001 From: therealunull Date: Sun, 13 Dec 2020 18:41:14 -0500 Subject: [PATCH] api: develop --- .../main/java/net/runelite/api/MenuEntry.java | 14 +- .../api/events/MenuOptionClicked.java | 17 +- .../CombatLevelRequirement.java | 48 ++ .../achievementdiary/DiaryRequirement.java | 43 + .../DiaryRequirementsPlugin.java | 293 +++++++ .../achievementdiary/FavourRequirement.java | 51 ++ .../GenericDiaryRequirement.java | 42 + .../achievementdiary/OrRequirement.java | 61 ++ .../QuestPointRequirement.java | 49 ++ .../achievementdiary/QuestRequirement.java | 66 ++ .../plugins/achievementdiary/Requirement.java | 32 + .../achievementdiary/SkillRequirement.java | 50 ++ .../diaries/ArdougneDiaryRequirement.java | 137 ++++ .../diaries/DesertDiaryRequirement.java | 118 +++ .../diaries/FaladorDiaryRequirement.java | 121 +++ .../diaries/FremennikDiaryRequirement.java | 132 +++ .../diaries/KandarinDiaryRequirement.java | 130 +++ .../diaries/KaramjaDiaryRequirement.java | 134 ++++ .../diaries/KourendDiaryRequirement.java | 136 ++++ .../diaries/LumbridgeDiaryRequirement.java | 135 ++++ .../diaries/MorytaniaDiaryRequirement.java | 139 ++++ .../diaries/VarrockDiaryRequirement.java | 119 +++ .../diaries/WesternDiaryRequirement.java | 145 ++++ .../diaries/WildernessDiaryRequirement.java | 111 +++ .../plugins/agility/AgilityArenaTimer.java | 39 + .../client/plugins/agility/AgilityConfig.java | 279 +++++++ .../plugins/agility/AgilityOverlay.java | 188 +++++ .../client/plugins/agility/AgilityPlugin.java | 500 ++++++++++++ .../plugins/agility/AgilitySession.java | 105 +++ .../client/plugins/agility/Courses.java | 90 +++ .../plugins/agility/LapCounterOverlay.java | 106 +++ .../client/plugins/agility/Obstacle.java | 41 + .../client/plugins/agility/Obstacles.java | 141 ++++ .../client/plugins/ammo/AmmoCounter.java | 57 ++ .../client/plugins/ammo/AmmoPlugin.java | 148 ++++ .../client/plugins/xpglobes/XpGlobe.java | 42 + .../plugins/xpglobes/XpGlobesConfig.java | 192 +++++ .../plugins/xpglobes/XpGlobesOverlay.java | 324 ++++++++ .../plugins/xpglobes/XpGlobesPlugin.java | 194 +++++ .../client/plugins/xptracker/XpAction.java | 36 + .../plugins/xptracker/XpActionType.java | 38 + .../plugins/xptracker/XpGoalTimeType.java | 32 + .../client/plugins/xptracker/XpInfoBox.java | 347 ++++++++ .../plugins/xptracker/XpInfoBoxOverlay.java | 149 ++++ .../client/plugins/xptracker/XpPanel.java | 214 +++++ .../plugins/xptracker/XpPanelLabel.java | 71 ++ .../plugins/xptracker/XpPauseState.java | 104 +++ .../plugins/xptracker/XpPauseStateSingle.java | 99 +++ .../plugins/xptracker/XpProgressBarLabel.java | 44 + .../plugins/xptracker/XpSnapshotSingle.java | 49 ++ .../client/plugins/xptracker/XpState.java | 233 ++++++ .../plugins/xptracker/XpStateSingle.java | 303 +++++++ .../plugins/xptracker/XpTrackerConfig.java | 202 +++++ .../plugins/xptracker/XpTrackerPlugin.java | 749 ++++++++++++++++++ .../plugins/xptracker/XpTrackerService.java | 65 ++ .../xptracker/XpTrackerServiceImpl.java | 83 ++ .../plugins/xptracker/XpUpdateResult.java | 32 + .../client/plugins/xptracker/XpWorldType.java | 83 ++ .../java/net/runelite/mixins/MenuMixin.java | 4 +- .../net/runelite/mixins/RSClientMixin.java | 10 +- 60 files changed, 7706 insertions(+), 10 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/CombatLevelRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirementsPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/FavourRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/GenericDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/OrRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestPointRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/Requirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/SkillRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/ArdougneDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/DesertDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FaladorDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FremennikDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KandarinDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KaramjaDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KourendDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/LumbridgeDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/MorytaniaDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/VarrockDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WesternDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WildernessDiaryRequirement.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityArenaTimer.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilitySession.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/Courses.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/LapCounterOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacle.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacles.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoCounter.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpAction.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpActionType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpGoalTimeType.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanelLabel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpProgressBarLabel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerService.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpWorldType.java diff --git a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java index 60ff56452b..4ffa7bd792 100644 --- a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java +++ b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java @@ -48,7 +48,7 @@ public class MenuEntry implements Cloneable /** * An identifier value for the target of the action. */ - private int type; + private int identifier; /** * The action the entry will trigger. * {@link MenuAction} @@ -74,7 +74,7 @@ public class MenuEntry implements Cloneable { this.option = option; this.target = target; - this.type = type; + this.identifier = type; this.opcode = opcode; this.actionParam0 = actionParam0; this.actionParam1 = actionParam1; @@ -104,6 +104,16 @@ public class MenuEntry implements Cloneable this.actionParam1 = i; } + public void setType(int i) + { + this.opcode = i; + } + + public int getType() + { + return this.opcode; + } + /** * Get opcode, but as it's enum counterpart */ diff --git a/runelite-api/src/main/java/net/runelite/api/events/MenuOptionClicked.java b/runelite-api/src/main/java/net/runelite/api/events/MenuOptionClicked.java index cb567b17f5..aeb69b9309 100644 --- a/runelite-api/src/main/java/net/runelite/api/events/MenuOptionClicked.java +++ b/runelite-api/src/main/java/net/runelite/api/events/MenuOptionClicked.java @@ -88,10 +88,25 @@ public class MenuOptionClicked extends MenuEntry implements Event { setOption(e.getOption()); setTarget(e.getTarget()); - setType(e.getType()); + setIdentifier(e.getIdentifier()); setOpcode(e.getOpcode()); setActionParam0(e.getActionParam0()); setActionParam1(e.getActionParam1()); setForceLeftClick(e.isForceLeftClick()); } + + public int getWidgetId() + { + return getActionParam1(); + } + + public String getMenuTarget() + { + return getTarget(); + } + + public String getMenuOption() + { + return getOption(); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/CombatLevelRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/CombatLevelRequirement.java new file mode 100644 index 0000000000..0b79715498 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/CombatLevelRequirement.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Abex + * 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.achievementdiary; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Client; + +@RequiredArgsConstructor +@Getter +public class CombatLevelRequirement implements Requirement +{ + private final int level; + + @Override + public String toString() + { + return level + " " + "Combat"; + } + + @Override + public boolean satisfiesRequirement(Client client) + { + return client.getLocalPlayer() != null && client.getLocalPlayer().getCombatLevel() >= level; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirement.java new file mode 100644 index 0000000000..9da905fe14 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirement.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import lombok.Getter; + +@Getter +class DiaryRequirement +{ + private final String task; + private final List requirements; + + DiaryRequirement(String task, Requirement[] requirements) + { + this.task = task; + this.requirements = ImmutableList.copyOf(requirements); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirementsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirementsPlugin.java new file mode 100644 index 0000000000..9afce27f26 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/DiaryRequirementsPlugin.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.FontTypeFace; +import net.runelite.api.ScriptID; +import net.runelite.api.events.WidgetLoaded; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.achievementdiary.diaries.ArdougneDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.DesertDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.FaladorDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.FremennikDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.KandarinDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.KaramjaDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.KourendDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.LumbridgeDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.MorytaniaDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.VarrockDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.WesternDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.diaries.WildernessDiaryRequirement; +import net.runelite.client.util.Text; + +@Slf4j +@PluginDescriptor( + name = "Diary Requirements", + description = "Display level requirements in Achievement Diary interface", + tags = {"achievements", "tasks"} +) +public class DiaryRequirementsPlugin extends Plugin +{ + private static final String AND_JOINER = ", "; + private static final Pattern AND_JOINER_PATTERN = Pattern.compile("(?<=, )"); + + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Subscribe + public void onWidgetLoaded(final WidgetLoaded event) + { + if (event.getGroupId() == WidgetID.DIARY_QUEST_GROUP_ID) + { + String widgetTitle = Text.removeTags( + client.getWidget( + WidgetInfo.DIARY_QUEST_WIDGET_TITLE) + .getText()) + .replace(' ', '_') + .toUpperCase(); + if (widgetTitle.startsWith("ACHIEVEMENT_DIARY")) + { + showDiaryRequirements(); + } + } + } + + private void showDiaryRequirements() + { + Widget widget = client.getWidget(WidgetInfo.DIARY_QUEST_WIDGET_TEXT); + Widget[] children = widget.getStaticChildren(); + + Widget titleWidget = children[0]; + if (titleWidget == null) + { + return; + } + + FontTypeFace font = titleWidget.getFont(); + int maxWidth = titleWidget.getWidth(); + + List originalAchievements = getOriginalAchievements(children); + + // new requirements starts out as a copy of the original + List newRequirements = new ArrayList<>(originalAchievements); + + GenericDiaryRequirement requirements = getRequirementsForTitle(titleWidget.getText()); + if (requirements == null) + { + log.debug("Unknown achievement diary {}", titleWidget.getText()); + return; + } + + Map skillRequirements = buildRequirements(requirements.getRequirements()); + if (skillRequirements == null) + { + return; + } + + int offset = 0; + String taskBuffer = ""; + for (int i = 0; i < originalAchievements.size(); i++) + { + String rowText = Text.removeTags(originalAchievements.get(i)); + if (skillRequirements.get(taskBuffer + " " + rowText) != null) + { + taskBuffer = taskBuffer + " " + rowText; + } + else + { + taskBuffer = rowText; + } + + if (skillRequirements.get(taskBuffer) != null) + { + String levelRequirement = skillRequirements.get(taskBuffer); + String task = originalAchievements.get(i); + + int taskWidth = font.getTextWidth(task); + int ourWidth = font.getTextWidth(levelRequirement); + String strike = task.startsWith("") ? "" : ""; + + if (ourWidth + taskWidth < maxWidth) + { + // Merge onto 1 line + newRequirements.set(i + offset, task + levelRequirement); + } + else if (ourWidth < maxWidth) + { + // 2 line split + newRequirements.add(i + (++offset), strike + levelRequirement); + } + else + { + // Full text layout + StringBuilder b = new StringBuilder(); + b.append(task); + int runningWidth = font.getTextWidth(b.toString()); + for (String word : AND_JOINER_PATTERN.split(levelRequirement)) + { + int wordWidth = font.getTextWidth(word); + if (runningWidth == 0 || wordWidth + runningWidth < maxWidth) + { + runningWidth += wordWidth; + b.append(word); + } + else + { + newRequirements.add(i + (offset++), b.toString()); + b.delete(0, b.length()); + runningWidth = wordWidth; + b.append(strike); + b.append(word); + } + } + newRequirements.set(i + offset, b.toString()); + } + } + } + + int lastLine = 0; + for (int i = 0; i < newRequirements.size() && i < children.length; i++) + { + Widget achievementWidget = children[i]; + String text = newRequirements.get(i); + achievementWidget.setText(text); + if (text != null && !text.isEmpty()) + { + lastLine = i; + } + } + + int numLines = lastLine; + clientThread.invokeLater(() -> client.runScript(ScriptID.DIARY_QUEST_UPDATE_LINECOUNT, 1, numLines)); + } + + private List getOriginalAchievements(Widget[] children) + { + List preloadedRequirements = new ArrayList<>(children.length); + for (Widget requirementWidget : children) + { + preloadedRequirements.add(requirementWidget.getText()); + } + return preloadedRequirements; + } + + private GenericDiaryRequirement getRequirementsForTitle(String title) + { + String diaryName = Text.removeTags(title + .replaceAll(" ", "_") + .toUpperCase()); + + GenericDiaryRequirement diaryRequirementContainer; + switch (diaryName) + { + case "ARDOUGNE_AREA_TASKS": + diaryRequirementContainer = new ArdougneDiaryRequirement(); + break; + case "DESERT_TASKS": + diaryRequirementContainer = new DesertDiaryRequirement(); + break; + case "FALADOR_AREA_TASKS": + diaryRequirementContainer = new FaladorDiaryRequirement(); + break; + case "FREMENNIK_TASKS": + diaryRequirementContainer = new FremennikDiaryRequirement(); + break; + case "KANDARIN_TASKS": + diaryRequirementContainer = new KandarinDiaryRequirement(); + break; + case "KARAMJA_AREA_TASKS": + diaryRequirementContainer = new KaramjaDiaryRequirement(); + break; + case "KOUREND_&_KEBOS_TASKS": + diaryRequirementContainer = new KourendDiaryRequirement(); + break; + case "LUMBRIDGE_&_DRAYNOR_TASKS": + diaryRequirementContainer = new LumbridgeDiaryRequirement(); + break; + case "MORYTANIA_TASKS": + diaryRequirementContainer = new MorytaniaDiaryRequirement(); + break; + case "VARROCK_TASKS": + diaryRequirementContainer = new VarrockDiaryRequirement(); + break; + case "WESTERN_AREA_TASKS": + diaryRequirementContainer = new WesternDiaryRequirement(); + break; + case "WILDERNESS_AREA_TASKS": + diaryRequirementContainer = new WildernessDiaryRequirement(); + break; + default: + return null; + } + return diaryRequirementContainer; + } + + // returns a map of task -> level requirements + private Map buildRequirements(Collection requirements) + { + Map reqs = new HashMap<>(); + for (DiaryRequirement req : requirements) + { + StringBuilder b = new StringBuilder(); + b.append("("); + + assert !req.getRequirements().isEmpty(); + for (Requirement ireq : req.getRequirements()) + { + boolean satifisfied = ireq.satisfiesRequirement(client); + b.append(satifisfied ? "" : ""); + b.append(ireq.toString()); + b.append(satifisfied ? "" : ""); + b.append(AND_JOINER); + } + + b.delete(b.length() - AND_JOINER.length(), b.length()); + + b.append(")"); + + reqs.put(req.getTask(), b.toString()); + } + return reqs; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/FavourRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/FavourRequirement.java new file mode 100644 index 0000000000..a39953492c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/FavourRequirement.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 William + * 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.achievementdiary; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Client; +import net.runelite.api.Favour; + +@RequiredArgsConstructor +@Getter +public class FavourRequirement implements Requirement +{ + private final Favour house; + private final int percent; + + @Override + public String toString() + { + return percent + "% " + house.getName() + " favour"; + } + + @Override + public boolean satisfiesRequirement(Client client) + { + int realFavour = client.getVar(house.getVarbit()); + return (realFavour / 10) >= percent; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/GenericDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/GenericDiaryRequirement.java new file mode 100644 index 0000000000..ba7834b151 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/GenericDiaryRequirement.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary; + +import java.util.HashSet; +import java.util.Set; +import lombok.Getter; + +public abstract class GenericDiaryRequirement +{ + @Getter + private Set requirements = new HashSet<>(); + + protected void add(String task, Requirement... requirements) + { + DiaryRequirement diaryRequirement = new DiaryRequirement(task, requirements); + this.requirements.add(diaryRequirement); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/OrRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/OrRequirement.java new file mode 100644 index 0000000000..ac31fb6c45 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/OrRequirement.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019 Abex + * 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.achievementdiary; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import java.util.List; +import lombok.Getter; +import net.runelite.api.Client; + +public class OrRequirement implements Requirement +{ + @Getter + private final List requirements; + + public OrRequirement(Requirement... reqs) + { + this.requirements = ImmutableList.copyOf(reqs); + } + + @Override + public String toString() + { + return Joiner.on(" or ").join(requirements); + } + + @Override + public boolean satisfiesRequirement(Client client) + { + for (Requirement r : getRequirements()) + { + if (r.satisfiesRequirement(client)) + { + return true; + } + } + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestPointRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestPointRequirement.java new file mode 100644 index 0000000000..d126222544 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestPointRequirement.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 Abex + * 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.achievementdiary; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Client; +import net.runelite.api.VarPlayer; + +@RequiredArgsConstructor +@Getter +public class QuestPointRequirement implements Requirement +{ + private final int qp; + + @Override + public String toString() + { + return qp + " " + "Quest points"; + } + + @Override + public boolean satisfiesRequirement(Client client) + { + return client.getVar(VarPlayer.QUEST_POINTS) >= qp; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestRequirement.java new file mode 100644 index 0000000000..9b8a1c2e1b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/QuestRequirement.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019 Abex + * 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.achievementdiary; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Client; +import net.runelite.api.Quest; +import net.runelite.api.QuestState; + +@Getter +@RequiredArgsConstructor +public class QuestRequirement implements Requirement +{ + private final Quest quest; + private final boolean started; + + public QuestRequirement(Quest quest) + { + this(quest, false); + } + + @Override + public String toString() + { + if (started) + { + return "Started " + quest.getName(); + } + + return quest.getName(); + } + + @Override + public boolean satisfiesRequirement(Client client) + { + QuestState questState = quest.getState(client); + if (started) + { + return questState != QuestState.NOT_STARTED; + } + return questState == QuestState.FINISHED; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/Requirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/Requirement.java new file mode 100644 index 0000000000..3360a15a7f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/Requirement.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 Abex + * 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.achievementdiary; + +import net.runelite.api.Client; + +public interface Requirement +{ + boolean satisfiesRequirement(Client client); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/SkillRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/SkillRequirement.java new file mode 100644 index 0000000000..8469e7f0c4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/SkillRequirement.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 Abex + * 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.achievementdiary; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Client; +import net.runelite.api.Skill; + +@RequiredArgsConstructor +@Getter +public class SkillRequirement implements Requirement +{ + private final Skill skill; + private final int level; + + @Override + public String toString() + { + return level + " " + skill.getName(); + } + + @Override + public boolean satisfiesRequirement(Client client) + { + return client.getRealSkillLevel(skill) >= level; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/ArdougneDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/ArdougneDiaryRequirement.java new file mode 100644 index 0000000000..1772df73c1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/ArdougneDiaryRequirement.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class ArdougneDiaryRequirement extends GenericDiaryRequirement +{ + public ArdougneDiaryRequirement() + { + // EASY + add("Have Wizard Cromperty teleport you to the Rune Essence mine.", + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Steal a cake from the Ardougne market stalls.", + new SkillRequirement(Skill.THIEVING, 5)); + add("Enter the Combat Training Camp north of W. Ardougne.", + new QuestRequirement(Quest.BIOHAZARD)); + add("Go out fishing on the Fishing Trawler.", + new SkillRequirement(Skill.FISHING, 15)); + + // MEDIUM + add("Enter the Unicorn pen in Ardougne zoo using Fairy rings.", + new QuestRequirement(Quest.FAIRYTALE_II__CURE_A_QUEEN, true)); + add("Grapple over Yanille's south wall.", + new SkillRequirement(Skill.AGILITY, 39), + new SkillRequirement(Skill.STRENGTH, 38), + new SkillRequirement(Skill.RANGED, 21)); + add("Harvest some strawberries from the Ardougne farming patch.", + new SkillRequirement(Skill.FARMING, 31)); + add("Cast the Ardougne Teleport spell.", + new SkillRequirement(Skill.MAGIC, 51), + new QuestRequirement(Quest.PLAGUE_CITY)); + add("Travel to Castlewars by Hot Air Balloon.", + new SkillRequirement(Skill.FIREMAKING, 50), + new QuestRequirement(Quest.ENLIGHTENED_JOURNEY)); + add("Claim buckets of sand from Bert in Yanille.", + new SkillRequirement(Skill.CRAFTING, 49), + new QuestRequirement(Quest.THE_HAND_IN_THE_SAND)); + add("Catch any fish on the Fishing Platform.", + new QuestRequirement(Quest.SEA_SLUG, true)); + add("Pickpocket the master farmer north of Ardougne.", + new SkillRequirement(Skill.THIEVING, 38)); + add("Collect some Nightshade from the Skavid Caves.", + new QuestRequirement(Quest.WATCHTOWER, true)); + add("Kill a swordchick in the Tower of Life.", + new QuestRequirement(Quest.TOWER_OF_LIFE)); + add("Equip Iban's upgraded staff or upgrade an Iban staff.", + new SkillRequirement(Skill.MAGIC, 50), + new SkillRequirement(Skill.ATTACK, 50), + new QuestRequirement(Quest.UNDERGROUND_PASS)); + add("Visit the Island East of the Necromancer's tower.", + new QuestRequirement(Quest.FAIRYTALE_II__CURE_A_QUEEN, true)); + + // HARD + // When the task is completed "the Totem" changes to "Totem" - so we add + // both variations. + add("Recharge some Jewellery at the Totem in the Legends Guild.", + new QuestRequirement(Quest.LEGENDS_QUEST)); + add("Recharge some Jewellery at Totem in the Legends Guild.", + new QuestRequirement(Quest.LEGENDS_QUEST)); + add("Enter the Magic Guild.", + new SkillRequirement(Skill.MAGIC, 66)); + add("Attempt to steal from a chest in Ardougne Castle.", + new SkillRequirement(Skill.THIEVING, 72)); + add("Have a zookeeper put you in Ardougne Zoo's monkey cage.", + new QuestRequirement(Quest.MONKEY_MADNESS_I, true)); + add("Teleport to the Watchtower.", + new SkillRequirement(Skill.MAGIC, 58), + new QuestRequirement(Quest.WATCHTOWER)); + add("Catch a Red Salamander.", + new SkillRequirement(Skill.HUNTER, 59)); + add("Check the health of a Palm tree near tree gnome village.", + new SkillRequirement(Skill.FARMING, 68)); + add("Pick some Poison Ivy berries from the patch south of Ardougne.", + new SkillRequirement(Skill.FARMING, 70)); + add("Smith a Mithril platebody near Ardougne.", + new SkillRequirement(Skill.SMITHING, 68)); + add("Enter your POH from Yanille.", + new SkillRequirement(Skill.CONSTRUCTION, 50)); + add("Smith a Dragon sq shield in West Ardougne.", + new SkillRequirement(Skill.SMITHING, 60), + new QuestRequirement(Quest.LEGENDS_QUEST)); + add("Craft some Death runes.", + new SkillRequirement(Skill.RUNECRAFT, 65), + new QuestRequirement(Quest.MOURNINGS_END_PART_II)); + + // ELITE + add("Catch a Manta ray in the Fishing Trawler and cook it in Port Khazard.", + new SkillRequirement(Skill.FISHING, 81), + new SkillRequirement(Skill.COOKING, 91) + ); + add("Attempt to picklock the door to the basement of Yanille Agility Dungeon.", + new SkillRequirement(Skill.THIEVING, 82)); + add("Pickpocket a Hero.", + new SkillRequirement(Skill.THIEVING, 80)); + add("Make a rune crossbow yourself from scratch within Witchaven or Yanille.", + new SkillRequirement(Skill.CRAFTING, 10), + new SkillRequirement(Skill.SMITHING, 91), + new SkillRequirement(Skill.FLETCHING, 69)); + add("Imbue a salve amulet at Nightmare Zone or equip an imbued salve amulet.", + new QuestRequirement(Quest.HAUNTED_MINE)); + add("Pick some Torstol from the patch north of Ardougne.", + new SkillRequirement(Skill.FARMING, 85)); + add("Complete a lap of Ardougne's rooftop agility course.", + new SkillRequirement(Skill.AGILITY, 90)); + add("Cast Ice Barrage on another player within Castlewars.", + new SkillRequirement(Skill.MAGIC, 94), + new QuestRequirement(Quest.DESERT_TREASURE)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/DesertDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/DesertDiaryRequirement.java new file mode 100644 index 0000000000..4de2c0a4d3 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/DesertDiaryRequirement.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class DesertDiaryRequirement extends GenericDiaryRequirement +{ + public DesertDiaryRequirement() + { + // EASY + add("Catch a Golden Warbler.", + new SkillRequirement(Skill.HUNTER, 5)); + add("Mine 5 clay in the north-eastern desert.", + new SkillRequirement(Skill.MINING, 5)); + add("Open the Sarcophagus in the first room of Pyramid Plunder.", + new SkillRequirement(Skill.THIEVING, 21), + new QuestRequirement(Quest.ICTHLARINS_LITTLE_HELPER, true)); + + // MEDIUM + add("Climb to the summit of the Agility Pyramid.", + new SkillRequirement(Skill.AGILITY, 30)); + add("Slay a desert lizard.", + new SkillRequirement(Skill.SLAYER, 22)); + add("Catch an Orange Salamander.", + new SkillRequirement(Skill.HUNTER, 47)); + add("Steal a feather from the Desert Phoenix.", + new SkillRequirement(Skill.THIEVING, 25)); + add("Travel to Uzer via Magic Carpet.", + new QuestRequirement(Quest.THE_GOLEM)); + add("Travel to the Desert via Eagle.", + new QuestRequirement(Quest.EAGLES_PEAK)); + add("Pray at the Elidinis statuette in Nardah.", + new QuestRequirement(Quest.SPIRITS_OF_THE_ELID)); + add("Create a combat potion in the desert.", + new SkillRequirement(Skill.HERBLORE, 36)); + add("Teleport to Enakhra's Temple with the Camulet.", + new QuestRequirement(Quest.ENAKHRAS_LAMENT)); + add("Visit the Genie.", + new QuestRequirement(Quest.SPIRITS_OF_THE_ELID)); + add("Teleport to Pollnivneach with a redirected teleport to house tablet.", + new SkillRequirement(Skill.CONSTRUCTION, 20)); + add("Chop some Teak logs near Uzer.", + new SkillRequirement(Skill.WOODCUTTING, 35)); + + // HARD + add("Knock out and pickpocket a Menaphite Thug.", + new SkillRequirement(Skill.THIEVING, 65), + new QuestRequirement(Quest.THE_FEUD)); + add("Mine some Granite.", + new SkillRequirement(Skill.MINING, 45)); + add("Refill your waterskins in the Desert using Lunar magic.", + new SkillRequirement(Skill.MAGIC, 68), + new QuestRequirement(Quest.DREAM_MENTOR)); + add("Complete a lap of the Pollnivneach agility course.", + new SkillRequirement(Skill.AGILITY, 70)); + add("Slay a Dust Devil with a Slayer helmet equipped.", + new SkillRequirement(Skill.SLAYER, 65), + new SkillRequirement(Skill.DEFENCE, 10), + new SkillRequirement(Skill.CRAFTING, 55), + new QuestRequirement(Quest.DESERT_TREASURE, true)); + add("Activate Ancient Magicks at the altar in the Jaldraocht Pyramid.", + new QuestRequirement(Quest.DESERT_TREASURE)); + add("Defeat a Locust Rider with Keris.", + new SkillRequirement(Skill.ATTACK, 50), + new QuestRequirement(Quest.CONTACT)); + add("Burn some yew logs on the Nardah Mayor's balcony.", + new SkillRequirement(Skill.FIREMAKING, 60)); + add("Create a Mithril Platebody in Nardah.", + new SkillRequirement(Skill.SMITHING, 68)); + + // ELITE + add("Bake a wild pie at the Nardah Clay Oven.", + new SkillRequirement(Skill.COOKING, 85)); + add("Cast Ice Barrage against a foe in the Desert.", + new SkillRequirement(Skill.MAGIC, 94), + new QuestRequirement(Quest.DESERT_TREASURE)); + add("Fletch some Dragon darts at the Bedabin Camp.", + new SkillRequirement(Skill.FLETCHING, 95), + new QuestRequirement(Quest.THE_TOURIST_TRAP)); + add("Speak to the KQ head in your POH.", + new SkillRequirement(Skill.CONSTRUCTION, 78), + new QuestRequirement(Quest.PRIEST_IN_PERIL)); + add("Steal from the Grand Gold Chest in the final room of Pyramid Plunder.", + new SkillRequirement(Skill.THIEVING, 91), + new QuestRequirement(Quest.ICTHLARINS_LITTLE_HELPER, true)); + add("Restore at least 85 Prayer points when praying at the Altar in Sophanem.", + new SkillRequirement(Skill.PRAYER, 85), + new QuestRequirement(Quest.ICTHLARINS_LITTLE_HELPER, true)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FaladorDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FaladorDiaryRequirement.java new file mode 100644 index 0000000000..30f7843199 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FaladorDiaryRequirement.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class FaladorDiaryRequirement extends GenericDiaryRequirement +{ + public FaladorDiaryRequirement() + { + // EASY + add("Find out what your family crest is from Sir Renitee.", + new SkillRequirement(Skill.CONSTRUCTION, 16)); + add("Climb over the western Falador wall.", + new SkillRequirement(Skill.AGILITY, 5)); + add("Make a mind tiara.", + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Smith some Blurite Limbs on Doric's Anvil.", + new SkillRequirement(Skill.MINING, 10), + new SkillRequirement(Skill.SMITHING, 13), + new QuestRequirement(Quest.THE_KNIGHTS_SWORD), + new QuestRequirement(Quest.DORICS_QUEST)); + + // MEDIUM + add("Light a Bullseye lantern at the Chemist's in Rimmington.", + new SkillRequirement(Skill.FIREMAKING, 49)); + add("Telegrab some Wine of Zamorak at the Chaos Temple by the Wilderness.", + new SkillRequirement(Skill.MAGIC, 33)); + add("Place a Scarecrow in the Falador farming patch.", + new SkillRequirement(Skill.FARMING, 23)); + add("Kill a Mogre at Mudskipper Point.", + new SkillRequirement(Skill.SLAYER, 32), + new QuestRequirement(Quest.SKIPPY_AND_THE_MOGRES)); + add("Visit the Port Sarim Rat Pits.", + new QuestRequirement(Quest.RATCATCHERS, true)); + add("Grapple up and then jump off the north Falador wall.", + new SkillRequirement(Skill.AGILITY, 11), + new SkillRequirement(Skill.STRENGTH, 37), + new SkillRequirement(Skill.RANGED, 19)); + add("Pickpocket a Falador guard.", + new SkillRequirement(Skill.THIEVING, 40)); + add("Pray at the Altar of Guthix in Taverley whilst wearing full Initiate.", + new SkillRequirement(Skill.PRAYER, 10), + new SkillRequirement(Skill.DEFENCE, 20), + new QuestRequirement(Quest.RECRUITMENT_DRIVE)); + add("Mine some Gold ore at the Crafting Guild.", + new SkillRequirement(Skill.CRAFTING, 40), + new SkillRequirement(Skill.MINING, 40)); + add("Squeeze through the crevice in the Dwarven mines.", + new SkillRequirement(Skill.AGILITY, 42)); + add("Chop and burn some Willow logs in Taverley", + new SkillRequirement(Skill.WOODCUTTING, 30), + new SkillRequirement(Skill.FIREMAKING, 30)); + add("Craft a fruit basket on the Falador Farm loom.", + new SkillRequirement(Skill.CRAFTING, 36)); + add("Teleport to Falador.", + new SkillRequirement(Skill.MAGIC, 37)); + + // HARD + add("Craft 140 Mind runes simultaneously.", + new SkillRequirement(Skill.RUNECRAFT, 56)); + add("Change your family crest to the Saradomin symbol.", + new SkillRequirement(Skill.PRAYER, 70)); + add("Kill a Skeletal Wyvern in the Asgarnia Ice Dungeon.", + new SkillRequirement(Skill.SLAYER, 72)); + add("Complete a lap of the Falador rooftop agility course.", + new SkillRequirement(Skill.AGILITY, 50)); + add("Enter the mining guild wearing full prospector.", + new SkillRequirement(Skill.MINING, 60)); + add("Kill the Blue Dragon under the Heroes' Guild.", + new QuestRequirement(Quest.HEROES_QUEST)); + add("Crack a wall safe within Rogues Den.", + new SkillRequirement(Skill.THIEVING, 50)); + add("Recharge your prayer in the Port Sarim church while wearing full Proselyte.", + new SkillRequirement(Skill.DEFENCE, 30), + new QuestRequirement(Quest.THE_SLUG_MENACE)); + add("Equip a dwarven helmet within the dwarven mines.", + new SkillRequirement(Skill.DEFENCE, 50), + new QuestRequirement(Quest.GRIM_TALES)); + + // ELITE + add("Craft 252 Air Runes simultaneously.", + new SkillRequirement(Skill.RUNECRAFT, 88)); + add("Purchase a White 2h Sword from Sir Vyvin.", + new QuestRequirement(Quest.WANTED)); + add("Find at least 3 magic roots at once when digging up your magic tree in Falador.", + new SkillRequirement(Skill.FARMING, 91), + new SkillRequirement(Skill.WOODCUTTING, 75)); + add("Jump over the strange floor in Taverley dungeon.", + new SkillRequirement(Skill.AGILITY, 80)); + add("Mix a Saradomin brew in Falador east bank.", + new SkillRequirement(Skill.HERBLORE, 81)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FremennikDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FremennikDiaryRequirement.java new file mode 100644 index 0000000000..46c708efbb --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/FremennikDiaryRequirement.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class FremennikDiaryRequirement extends GenericDiaryRequirement +{ + public FremennikDiaryRequirement() + { + // EASY + add("Catch a Cerulean twitch.", + new SkillRequirement(Skill.HUNTER, 11)); + add("Change your boots at Yrsa's Shoe Store.", + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS)); + add("Craft a tiara from scratch in Rellekka.", + new SkillRequirement(Skill.CRAFTING, 23), + new SkillRequirement(Skill.MINING, 20), + new SkillRequirement(Skill.SMITHING, 20), + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS)); + add("Browse the Stonemasons shop.", + new QuestRequirement(Quest.THE_GIANT_DWARF, true)); + add("Steal from the Keldagrim crafting or baker's stall.", + new SkillRequirement(Skill.THIEVING, 5), + new QuestRequirement(Quest.THE_GIANT_DWARF, true)); + add("Enter the Troll Stronghold.", + new QuestRequirement(Quest.DEATH_PLATEAU), + new QuestRequirement(Quest.TROLL_STRONGHOLD, true)); + add("Chop and burn some oak logs in the Fremennik Province.", + new SkillRequirement(Skill.WOODCUTTING, 15), + new SkillRequirement(Skill.FIREMAKING, 15)); + + // MEDIUM + add("Slay a Brine rat.", + new SkillRequirement(Skill.SLAYER, 47), + new QuestRequirement(Quest.OLAFS_QUEST, true)); + add("Travel to the Snowy Hunter Area via Eagle.", + new QuestRequirement(Quest.EAGLES_PEAK)); + add("Mine some coal in Rellekka.", + new SkillRequirement(Skill.MINING, 30), + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS)); + add("Steal from the Rellekka Fish stalls.", + new SkillRequirement(Skill.THIEVING, 42), + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS)); + add("Travel to Miscellania by Fairy ring.", + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS), + new QuestRequirement(Quest.FAIRYTALE_II__CURE_A_QUEEN, true)); + add("Catch a Snowy knight.", + new SkillRequirement(Skill.HUNTER, 35)); + add("Pick up your Pet Rock from your POH Menagerie.", + new SkillRequirement(Skill.CONSTRUCTION, 37), + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS)); + add("Visit the Lighthouse from Waterbirth island.", + new QuestRequirement(Quest.HORROR_FROM_THE_DEEP), + new QuestRequirement(Quest.THE_FREMENNIK_TRIALS, true)); + add("Mine some gold at the Arzinian mine.", + new SkillRequirement(Skill.MINING, 40), + new QuestRequirement(Quest.BETWEEN_A_ROCK, true)); + + // HARD + add("Teleport to Trollheim.", + new SkillRequirement(Skill.MAGIC, 61), + new QuestRequirement(Quest.EADGARS_RUSE)); + add("Catch a Sabre-toothed Kyatt.", + new SkillRequirement(Skill.HUNTER, 55)); + add("Mix a super defence potion in the Fremennik province.", + new SkillRequirement(Skill.HERBLORE, 66)); + add("Steal from the Keldagrim Gem Stall.", + new SkillRequirement(Skill.THIEVING, 75), + new QuestRequirement(Quest.THE_GIANT_DWARF, true)); + add("Craft a Fremennik shield on Neitiznot.", + new SkillRequirement(Skill.WOODCUTTING, 56), + new QuestRequirement(Quest.THE_FREMENNIK_ISLES)); + add("Mine 5 Adamantite ores on Jatizso.", + new SkillRequirement(Skill.MINING, 70), + new QuestRequirement(Quest.THE_FREMENNIK_ISLES)); + add("Obtain 100% support from your kingdom subjects.", + new QuestRequirement(Quest.THRONE_OF_MISCELLANIA)); + add("Teleport to Waterbirth Island.", + new SkillRequirement(Skill.MAGIC, 72), + new QuestRequirement(Quest.LUNAR_DIPLOMACY)); + add("Obtain the Blast Furnace Foreman's permission to use the Blast Furnace for free.", + new SkillRequirement(Skill.SMITHING, 60), + new QuestRequirement(Quest.THE_GIANT_DWARF, true)); + + // ELITE + add("Craft 56 astral runes at once.", + new SkillRequirement(Skill.RUNECRAFT, 82), + new QuestRequirement(Quest.LUNAR_DIPLOMACY)); + add("Create a dragonstone amulet in the Neitiznot furnace.", + new SkillRequirement(Skill.CRAFTING, 80), + new QuestRequirement(Quest.THE_FREMENNIK_ISLES, true)); + add("Complete a lap of the Rellekka agility course.", + new SkillRequirement(Skill.AGILITY, 80)); + add("Kill each of the Godwars generals.", + new SkillRequirement(Skill.AGILITY, 70), + new SkillRequirement(Skill.STRENGTH, 70), + new SkillRequirement(Skill.HITPOINTS, 70), + new SkillRequirement(Skill.RANGED, 70), + new QuestRequirement(Quest.TROLL_STRONGHOLD)); + add("Slay a Spiritual mage within the Godwars Dungeon.", + new SkillRequirement(Skill.SLAYER, 83), + new QuestRequirement(Quest.TROLL_STRONGHOLD)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KandarinDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KandarinDiaryRequirement.java new file mode 100644 index 0000000000..ef9df50ce2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KandarinDiaryRequirement.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class KandarinDiaryRequirement extends GenericDiaryRequirement +{ + public KandarinDiaryRequirement() + { + // EASY + add("Catch a Mackerel at Catherby.", + new SkillRequirement(Skill.FISHING, 16)); + add("Plant some Jute seeds in the patch north of McGrubor's Wood.", + new SkillRequirement(Skill.FARMING, 13)); + add("Defeat one of each elemental in the workshop.", + new QuestRequirement(Quest.ELEMENTAL_WORKSHOP_I, true)); + add("Cross the Coal truck log shortcut.", + new SkillRequirement(Skill.AGILITY, 20)); + + // MEDIUM + add("Complete a lap of the Barbarian agility course.", + new SkillRequirement(Skill.AGILITY, 35), + new QuestRequirement(Quest.ALFRED_GRIMHANDS_BARCRAWL)); + add("Create a Super Antipoison potion from scratch in the Seers/Catherby Area.", + new SkillRequirement(Skill.HERBLORE, 48)); + add("Enter the Ranging guild.", + new SkillRequirement(Skill.RANGED, 40)); + add("Use the grapple shortcut to get from the water obelisk to Catherby shore.", + new SkillRequirement(Skill.AGILITY, 36), + new SkillRequirement(Skill.STRENGTH, 22), + new SkillRequirement(Skill.RANGED, 39)); + add("Catch and cook a Bass in Catherby.", + new SkillRequirement(Skill.FISHING, 46), + new SkillRequirement(Skill.COOKING, 43)); + add("Teleport to Camelot.", + new SkillRequirement(Skill.MAGIC, 45)); + add("String a Maple shortbow in Seers' Village bank.", + new SkillRequirement(Skill.FLETCHING, 50)); + add("Pick some Limpwurt root from the farming patch in Catherby.", + new SkillRequirement(Skill.FARMING, 26)); + add("Create a Mind helmet.", + new QuestRequirement(Quest.ELEMENTAL_WORKSHOP_II)); + add("Kill a Fire Giant inside Baxtorian Waterfall.", + new QuestRequirement(Quest.WATERFALL_QUEST, true)); + add("Steal from the chest in Hemenster.", + new SkillRequirement(Skill.THIEVING, 47)); + add("Travel to McGrubor's Wood by Fairy Ring.", + new QuestRequirement(Quest.FAIRYTALE_II__CURE_A_QUEEN, true)); + add("Mine some coal near the coal trucks.", + new SkillRequirement(Skill.MINING, 30)); + + // HARD + add("Catch a Leaping Sturgeon.", + new SkillRequirement(Skill.FISHING, 70), + new SkillRequirement(Skill.AGILITY, 45), + new SkillRequirement(Skill.STRENGTH, 45)); + add("Complete a lap of the Seers' Village agility course.", + new SkillRequirement(Skill.AGILITY, 60)); + add("Create a Yew Longbow from scratch around Seers' Village.", + new SkillRequirement(Skill.WOODCUTTING, 60), + new SkillRequirement(Skill.FLETCHING, 70), + new SkillRequirement(Skill.CRAFTING, 10)); + add("Enter the Seers' Village courthouse with piety turned on.", + new SkillRequirement(Skill.PRAYER, 70), + new SkillRequirement(Skill.DEFENCE, 70), + new QuestRequirement(Quest.KINGS_RANSOM)); + add("Charge a Water Orb.", + new SkillRequirement(Skill.MAGIC, 56)); + add("Burn some Maple logs with a bow in Seers' Village.", + new SkillRequirement(Skill.FIREMAKING, 65)); + add("Kill a Shadow Hound in the Shadow dungeon.", + new SkillRequirement(Skill.THIEVING, 53), + new QuestRequirement(Quest.DESERT_TREASURE, true)); + add("Purchase and equip a granite body from Barbarian Assault.", + new SkillRequirement(Skill.STRENGTH, 50), + new SkillRequirement(Skill.DEFENCE, 50)); + add("Have the Seers' estate agent decorate your house with Fancy Stone.", + new SkillRequirement(Skill.CONSTRUCTION, 50)); + add("Smith an Adamant spear at Otto's Grotto.", + new SkillRequirement(Skill.SMITHING, 75), + new QuestRequirement(Quest.TAI_BWO_WANNAI_TRIO)); + + // ELITE + add("Pick some Dwarf weed from the herb patch at Catherby.", + new SkillRequirement(Skill.FARMING, 79)); + add("Fish and Cook 5 Sharks in Catherby using the Cooking gauntlets.", + new SkillRequirement(Skill.FISHING, 76), + new SkillRequirement(Skill.COOKING, 80), + new QuestRequirement(Quest.FAMILY_CREST)); + add("Mix a Stamina Mix on top of the Seers' Village bank.", + new SkillRequirement(Skill.HERBLORE, 86), + new SkillRequirement(Skill.AGILITY, 60)); + add("Smith a Rune Hasta at Otto's Grotto.", + new SkillRequirement(Skill.SMITHING, 90)); + add("Construct a Pyre ship from Magic Logs.(Requires Chewed Bones.)", + new SkillRequirement(Skill.FIREMAKING, 85), + new SkillRequirement(Skill.CRAFTING, 85)); + add("Teleport to Catherby.", + new SkillRequirement(Skill.MAGIC, 87), + new QuestRequirement(Quest.LUNAR_DIPLOMACY)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KaramjaDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KaramjaDiaryRequirement.java new file mode 100644 index 0000000000..fc906cccb4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KaramjaDiaryRequirement.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.CombatLevelRequirement; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.OrRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class KaramjaDiaryRequirement extends GenericDiaryRequirement +{ + public KaramjaDiaryRequirement() + { + // EASY + add("Use the rope swing to travel to the small island north-west of Karamja, where the " + + "moss giants are.", + new SkillRequirement(Skill.AGILITY, 10)); + add("Mine some gold from the rocks on the north-west peninsula of Karamja.", + new SkillRequirement(Skill.MINING, 40)); + add("Explore Cairn Island to the west of Karamja.", + new SkillRequirement(Skill.AGILITY, 15)); + + // MEDIUM + add("Claim a ticket from the Agility Arena in Brimhaven.", + new SkillRequirement(Skill.AGILITY, 30)); + add("Discover hidden wall in the dungeon below the volcano.", + new QuestRequirement(Quest.DRAGON_SLAYER, true)); + add("Visit the Isle of Crandor via the dungeon below the volcano.", + new QuestRequirement(Quest.DRAGON_SLAYER, true)); + add("Use Vigroy and Hajedy's cart service.", + new QuestRequirement(Quest.SHILO_VILLAGE)); + add("Earn 100% favour in the village of Tai Bwo Wannai.", + new SkillRequirement(Skill.WOODCUTTING, 10), + new QuestRequirement(Quest.JUNGLE_POTION)); + add("Cook a spider on a stick.", + new SkillRequirement(Skill.COOKING, 16)); + add("Charter the Lady of the Waves from Cairn Isle to Port Khazard.", + new QuestRequirement(Quest.SHILO_VILLAGE)); + add("Cut a log from a teak tree.", + new SkillRequirement(Skill.WOODCUTTING, 35), + new QuestRequirement(Quest.JUNGLE_POTION)); + add("Cut a log from a mahogany tree.", + new SkillRequirement(Skill.WOODCUTTING, 50), + new QuestRequirement(Quest.JUNGLE_POTION)); + add("Catch a karambwan.", + new SkillRequirement(Skill.FISHING, 65), + new QuestRequirement(Quest.TAI_BWO_WANNAI_TRIO, true)); + add("Exchange gems for a machete.", + new QuestRequirement(Quest.JUNGLE_POTION)); + add("Use the gnome glider to travel to Karamja.", + new QuestRequirement(Quest.THE_GRAND_TREE)); + add("Grow a healthy fruit tree in the patch near Brimhaven.", + new SkillRequirement(Skill.FARMING, 27)); + add("Trap a horned graahk.", + new SkillRequirement(Skill.HUNTER, 41)); + add("Chop the vines to gain deeper access to Brimhaven Dungeon.", + new SkillRequirement(Skill.WOODCUTTING, 10)); + add("Cross the lava using the stepping stones within Brimhaven Dungeon.", + new SkillRequirement(Skill.AGILITY, 12)); + add("Climb the stairs within Brimhaven Dungeon.", + new SkillRequirement(Skill.WOODCUTTING, 10)); + add("Charter a ship from the shipyard in the far east of Karamja.", + new QuestRequirement(Quest.THE_GRAND_TREE)); + add("Mine a red topaz from a gem rock.", + new SkillRequirement(Skill.MINING, 40), + new OrRequirement( + new QuestRequirement(Quest.SHILO_VILLAGE), + new QuestRequirement(Quest.JUNGLE_POTION) + ) + ); + + // HARD + add("Craft some nature runes.", + new SkillRequirement(Skill.RUNECRAFT, 44), + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Cook a karambwan thoroughly.", + new SkillRequirement(Skill.COOKING, 30), + new QuestRequirement(Quest.TAI_BWO_WANNAI_TRIO)); + add("Kill a deathwing in the dungeon under the Kharazi Jungle.", + new SkillRequirement(Skill.WOODCUTTING, 15), + new SkillRequirement(Skill.STRENGTH, 50), + new SkillRequirement(Skill.AGILITY, 50), + new SkillRequirement(Skill.THIEVING, 50), + new SkillRequirement(Skill.MINING, 52), + new QuestRequirement(Quest.LEGENDS_QUEST)); + add("Use the crossbow short cut south of the volcano.", + new SkillRequirement(Skill.AGILITY, 53), + new SkillRequirement(Skill.RANGED, 42), + new SkillRequirement(Skill.STRENGTH, 21)); + add("Collect 5 palm leaves.", + new SkillRequirement(Skill.WOODCUTTING, 15), + new QuestRequirement(Quest.LEGENDS_QUEST)); + add("Be assigned a Slayer task by Duradel north of Shilo Village.", + new CombatLevelRequirement(100), + new SkillRequirement(Skill.SLAYER, 50), + new QuestRequirement(Quest.SHILO_VILLAGE)); + + // ELITE + add("Craft 56 Nature runes at once.", + new SkillRequirement(Skill.RUNECRAFT, 91)); + add("Check the health of a palm tree in Brimhaven.", + new SkillRequirement(Skill.FARMING, 68)); + add("Create an antivenom potion whilst standing in the horse shoe mine.", + new SkillRequirement(Skill.HERBLORE, 87)); + add("Check the health of your Calquat tree patch.", + new SkillRequirement(Skill.FARMING, 72)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KourendDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KourendDiaryRequirement.java new file mode 100644 index 0000000000..182b288a3b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/KourendDiaryRequirement.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019 William + * 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.achievementdiary.diaries; + +import net.runelite.api.Favour; +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.FavourRequirement; + +public class KourendDiaryRequirement extends GenericDiaryRequirement +{ + public KourendDiaryRequirement() + { + //EASY + add("Mine some Iron at the Mount Karuulm mine.", + new SkillRequirement(Skill.MINING, 15)); + add("Steal from a Hosidius Food Stall.", + new SkillRequirement(Skill.THIEVING, 25), + new FavourRequirement(Favour.HOSIDIUS, 15)); + add("Browse the Warrens General Store.", + new QuestRequirement(Quest.THE_QUEEN_OF_THIEVES, true)); + add("Enter your Player Owned House from Hosidius.", + new SkillRequirement(Skill.CONSTRUCTION, 25)); + add("Create a Strength potion in the Lovakengj Pub.", + new SkillRequirement(Skill.HERBLORE, 12)); + add("Fish a Trout from the River Molch.", + new SkillRequirement(Skill.FISHING, 20)); + + //MEDIUM + add("Travel to the Fairy Ring south of Mount Karuulm.", + new QuestRequirement(Quest.FAIRYTALE_II__CURE_A_QUEEN, true)); + add("Use Kharedst's memoirs to teleport to all five cities in Great Kourend.", + new QuestRequirement(Quest.THE_DEPTHS_OF_DESPAIR), + new QuestRequirement(Quest.THE_QUEEN_OF_THIEVES), + new QuestRequirement(Quest.TALE_OF_THE_RIGHTEOUS), + new QuestRequirement(Quest.THE_FORSAKEN_TOWER), + new QuestRequirement(Quest.THE_ASCENT_OF_ARCEUUS)); + add("Mine some Volcanic sulphur.", + new SkillRequirement(Skill.MINING, 42)); + add("Enter the Farming Guild.", + new SkillRequirement(Skill.FARMING, 45)); + add("Switch to the Necromancy Spellbook at Tyss.", + new FavourRequirement(Favour.ARCEUUS, 60)); + add("Repair a Piscarilius crane.", + new SkillRequirement(Skill.CRAFTING, 30)); + add("Deliver some intelligence to Captain Ginea.", + new FavourRequirement(Favour.SHAYZIEN, 40)); + add("Catch a Bluegill on Molch Island.", + new SkillRequirement(Skill.FISHING, 43), + new SkillRequirement(Skill.HUNTER, 35)); + add("Use the boulder leap in the Arceuus essence mine.", + new SkillRequirement(Skill.AGILITY, 49)); + add("Subdue the Wintertodt.", + new SkillRequirement(Skill.FIREMAKING, 50)); + add("Catch a Chinchompa in the Kourend Woodland.", + new SkillRequirement(Skill.HUNTER, 53), + new QuestRequirement(Quest.EAGLES_PEAK)); + add("Chop some Mahogany logs north of the Farming Guild.", + new SkillRequirement(Skill.WOODCUTTING, 50)); + + //HARD + add("Enter the Woodcutting Guild.", + new SkillRequirement(Skill.WOODCUTTING, 60), + new FavourRequirement(Favour.HOSIDIUS, 75)); + add("Smelt an Adamantite bar in The Forsaken Tower.", + new SkillRequirement(Skill.SMITHING, 70), + new QuestRequirement(Quest.THE_FORSAKEN_TOWER, true)); + add("Kill a Lizardman Shaman in Molch.", + new FavourRequirement(Favour.SHAYZIEN, 100)); + add("Mine some Lovakite.", + new SkillRequirement(Skill.MINING, 65), + new FavourRequirement(Favour.LOVAKENGJ, 30)); + add("Plant some Logavano seeds at the Tithe Farm.", + new SkillRequirement(Skill.FARMING, 74), + new FavourRequirement(Favour.HOSIDIUS, 100)); + add("Teleport to Xeric's Heart using Xeric's Talisman.", + new QuestRequirement(Quest.ARCHITECTURAL_ALLIANCE)); + add("Deliver an artefact to Captain Khaled.", + new SkillRequirement(Skill.THIEVING, 49), + new FavourRequirement(Favour.PISCARILIUS, 75)); + add("Kill a Wyrm in the Karuulm Slayer Dungeon.", + new SkillRequirement(Skill.SLAYER, 62)); + add("Cast Monster Examine on a Troll south of Mount Quidamortem.", + new SkillRequirement(Skill.MAGIC, 66), + new QuestRequirement(Quest.DREAM_MENTOR)); + + //ELITE + add("Craft one or more Blood runes.", + new SkillRequirement(Skill.RUNECRAFT, 77), + new SkillRequirement(Skill.MINING, 38), + new SkillRequirement(Skill.CRAFTING, 38), + new FavourRequirement(Favour.ARCEUUS, 100)); + add("Chop some Redwood logs.", + new SkillRequirement(Skill.WOODCUTTING, 90), + new FavourRequirement(Favour.HOSIDIUS, 75)); + add("Catch an Anglerfish and cook it whilst in Great Kourend.", + new SkillRequirement(Skill.FISHING, 82), + new SkillRequirement(Skill.COOKING, 84), + new FavourRequirement(Favour.PISCARILIUS, 100)); + add("Kill a Hydra in the Karuulm Slayer Dungeon.", + new SkillRequirement(Skill.SLAYER, 95)); + add("Create an Ape Atoll teleport tablet.", + new SkillRequirement(Skill.MAGIC, 90), + new SkillRequirement(Skill.MINING, 38), + new SkillRequirement(Skill.CRAFTING, 38), + new FavourRequirement(Favour.ARCEUUS, 100)); + add("Create your own Battlestaff from scratch within the Farming Guild.", + new SkillRequirement(Skill.FARMING, 85), + new SkillRequirement(Skill.FLETCHING, 40)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/LumbridgeDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/LumbridgeDiaryRequirement.java new file mode 100644 index 0000000000..7238e18a55 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/LumbridgeDiaryRequirement.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.CombatLevelRequirement; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class LumbridgeDiaryRequirement extends GenericDiaryRequirement +{ + public LumbridgeDiaryRequirement() + { + // EASY + add("Complete a lap of the Draynor Village agility course.", + new SkillRequirement(Skill.AGILITY, 10)); + add("Slay a Cave bug beneath Lumbridge Swamp.", + new SkillRequirement(Skill.SLAYER, 7)); + add("Have Sedridor teleport you to the Essence Mine.", + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Craft some water runes.", + new SkillRequirement(Skill.RUNECRAFT, 5), + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Chop and burn some oak logs in Lumbridge.", + new SkillRequirement(Skill.WOODCUTTING, 15), + new SkillRequirement(Skill.FIREMAKING, 15)); + add("Catch some Anchovies in Al Kharid.", + new SkillRequirement(Skill.FISHING, 15)); + add("Bake some Bread on the Lumbridge kitchen range.", + new QuestRequirement(Quest.COOKS_ASSISTANT)); + add("Mine some Iron ore at the Al Kharid mine.", + new SkillRequirement(Skill.MINING, 15)); + + // MEDIUM + add("Complete a lap of the Al Kharid agility course.", + new SkillRequirement(Skill.AGILITY, 20)); + add("Grapple across the River Lum.", + new SkillRequirement(Skill.AGILITY, 8), + new SkillRequirement(Skill.STRENGTH, 19), + new SkillRequirement(Skill.RANGED, 37)); + add("Purchase an upgraded device from Ava.", + new SkillRequirement(Skill.RANGED, 50), + new QuestRequirement(Quest.ANIMAL_MAGNETISM)); + add("Travel to the Wizards' Tower by Fairy ring.", + new QuestRequirement(Quest.FAIRYTALE_II__CURE_A_QUEEN, true)); + add("Cast the teleport to Lumbridge spell.", + new SkillRequirement(Skill.MAGIC, 31)); + add("Catch some Salmon in Lumbridge.", + new SkillRequirement(Skill.FISHING, 30)); + add("Craft a coif in the Lumbridge cow pen.", + new SkillRequirement(Skill.CRAFTING, 38)); + add("Chop some willow logs in Draynor Village.", + new SkillRequirement(Skill.WOODCUTTING, 30)); + add("Pickpocket Martin the Master Gardener.", + new SkillRequirement(Skill.THIEVING, 38)); + add("Get a slayer task from Chaeldar.", + new CombatLevelRequirement(70), + new QuestRequirement(Quest.LOST_CITY)); + add("Catch an Essence or Eclectic impling in Puro-Puro.", + new SkillRequirement(Skill.HUNTER, 42), + new QuestRequirement(Quest.LOST_CITY)); + add("Craft some Lava runes at the fire altar in Al Kharid.", + new SkillRequirement(Skill.RUNECRAFT, 23), + new QuestRequirement(Quest.RUNE_MYSTERIES)); + + // HARD + add("Cast Bones to Peaches in Al Kharid palace.", + new SkillRequirement(Skill.MAGIC, 60)); + add("Squeeze past the jutting wall on your way to the cosmic altar.", + new SkillRequirement(Skill.AGILITY, 46), + new QuestRequirement(Quest.LOST_CITY)); + add("Craft 56 Cosmic runes simultaneously.", + new SkillRequirement(Skill.RUNECRAFT, 59), + new QuestRequirement(Quest.LOST_CITY)); + add("Travel from Lumbridge to Edgeville on a Waka Canoe.", + new SkillRequirement(Skill.WOODCUTTING, 57)); + add("Collect at least 100 Tears of Guthix in one visit.", + new QuestRequirement(Quest.TEARS_OF_GUTHIX)); + add("Take the train from Dorgesh-Kaan to Keldagrim.", + new QuestRequirement(Quest.ANOTHER_SLICE_OF_HAM)); + add("Purchase some Barrows gloves from the Lumbridge bank chest.", + new QuestRequirement(Quest.RECIPE_FOR_DISASTER)); + add("Pick some Belladonna from the farming patch at Draynor Manor.", + new SkillRequirement(Skill.FARMING, 63)); + add("Light your mining helmet in the Lumbridge castle basement.", + new SkillRequirement(Skill.FIREMAKING, 65)); + add("Recharge your prayer at Clan Wars with Smite activated.", + new SkillRequirement(Skill.PRAYER, 52)); + add("Craft, string and enchant an Amulet of Power in Lumbridge.", + new SkillRequirement(Skill.CRAFTING, 70), + new SkillRequirement(Skill.MAGIC, 57)); + + // ELITE + add("Steal from a Dorgesh-Kaan rich chest.", + new SkillRequirement(Skill.THIEVING, 78), + new QuestRequirement(Quest.DEATH_TO_THE_DORGESHUUN)); + add("Pickpocket Movario on the Dorgesh-Kaan Agility course.", + new SkillRequirement(Skill.AGILITY, 70), + new SkillRequirement(Skill.RANGED, 70), + new SkillRequirement(Skill.STRENGTH, 70), + new QuestRequirement(Quest.DEATH_TO_THE_DORGESHUUN)); + add("Chop some magic logs at the Mage Training Arena.", + new SkillRequirement(Skill.WOODCUTTING, 75)); + add("Smith an Adamant platebody down Draynor sewer.", + new SkillRequirement(Skill.SMITHING, 88)); + add("Craft 140 or more Water runes at once.", + new SkillRequirement(Skill.RUNECRAFT, 76), + new QuestRequirement(Quest.RUNE_MYSTERIES)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/MorytaniaDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/MorytaniaDiaryRequirement.java new file mode 100644 index 0000000000..826b1a8410 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/MorytaniaDiaryRequirement.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.CombatLevelRequirement; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.OrRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class MorytaniaDiaryRequirement extends GenericDiaryRequirement +{ + public MorytaniaDiaryRequirement() + { + // EASY + add("Craft any Snelm from scratch in Morytania.", + new SkillRequirement(Skill.CRAFTING, 15)); + add("Cook a thin Snail on the Port Phasmatys range.", + new SkillRequirement(Skill.COOKING, 12)); + add("Get a slayer task from Mazchna.", + new CombatLevelRequirement(20)); + add("Kill a Banshee in the Slayer Tower.", + new SkillRequirement(Skill.SLAYER, 15)); + add("Place a Scarecrow in the Morytania flower patch.", + new SkillRequirement(Skill.FARMING, 23)); + add("Kill a werewolf in its human form using the Wolfbane Dagger.", + new QuestRequirement(Quest.PRIEST_IN_PERIL)); + add("Restore your prayer points at the nature altar.", + new QuestRequirement(Quest.NATURE_SPIRIT)); + + // MEDIUM + add("Catch a swamp lizard.", + new SkillRequirement(Skill.HUNTER, 29)); + add("Complete a lap of the Canifis agility course.", + new SkillRequirement(Skill.AGILITY, 40)); + add("Obtain some Bark from a Hollow tree.", + new SkillRequirement(Skill.WOODCUTTING, 45)); + add("Kill a Terror Dog.", + new SkillRequirement(Skill.SLAYER, 40), + new QuestRequirement(Quest.LAIR_OF_TARN_RAZORLOR)); + add("Complete a game of trouble brewing.", + new SkillRequirement(Skill.COOKING, 40), + new QuestRequirement(Quest.CABIN_FEVER)); + add("Make a batch of cannonballs at the Port Phasmatys furnace.", + new SkillRequirement(Skill.SMITHING, 35), + new QuestRequirement(Quest.DWARF_CANNON), + new QuestRequirement(Quest.GHOSTS_AHOY, true)); + add("Kill a Fever Spider on Braindeath Island.", + new SkillRequirement(Skill.SLAYER, 42), + new QuestRequirement(Quest.RUM_DEAL)); + add("Use an ectophial to return to Port Phasmatys.", + new QuestRequirement(Quest.GHOSTS_AHOY)); + add("Mix a Guthix Balance potion while in Morytania.", + new SkillRequirement(Skill.HERBLORE, 22), + new QuestRequirement(Quest.IN_AID_OF_THE_MYREQUE, true)); + + // HARD + add("Enter the Kharyrll portal in your POH.", + new SkillRequirement(Skill.MAGIC, 66), + new SkillRequirement(Skill.CONSTRUCTION, 50), + new QuestRequirement(Quest.DESERT_TREASURE)); + add("Climb the advanced spike chain within Slayer Tower.", + new SkillRequirement(Skill.AGILITY, 71)); + add("Harvest some Watermelon from the Allotment patch on Harmony Island.", + new SkillRequirement(Skill.FARMING, 47), + new QuestRequirement(Quest.THE_GREAT_BRAIN_ROBBERY, true)); + add("Chop and burn some mahogany logs on Mos Le'Harmless.", + new SkillRequirement(Skill.WOODCUTTING, 50), + new SkillRequirement(Skill.FIREMAKING, 50), + new QuestRequirement(Quest.CABIN_FEVER)); + add("Complete a temple trek with a hard companion.", + new QuestRequirement(Quest.IN_AID_OF_THE_MYREQUE)); + add("Kill a Cave Horror.", + new SkillRequirement(Skill.SLAYER, 58), + new QuestRequirement(Quest.CABIN_FEVER)); + add("Harvest some Bittercap Mushrooms from the patch in Canifis.", + new SkillRequirement(Skill.FARMING, 53)); + add("Pray at the Altar of Nature with Piety activated.", + new SkillRequirement(Skill.PRAYER, 70), + new SkillRequirement(Skill.DEFENCE, 70), + new QuestRequirement(Quest.NATURE_SPIRIT), + new QuestRequirement(Quest.KINGS_RANSOM)); + add("Use the shortcut to get to the bridge over the Salve.", + new SkillRequirement(Skill.AGILITY, 65)); + add("Mine some Mithril ore in the Abandoned Mine.", + new SkillRequirement(Skill.MINING, 55), + new QuestRequirement(Quest.HAUNTED_MINE)); + + // ELITE + add("Catch a shark in Burgh de Rott with your bare hands.", + new SkillRequirement(Skill.FISHING, 96), + new SkillRequirement(Skill.STRENGTH, 76), + new QuestRequirement(Quest.IN_AID_OF_THE_MYREQUE)); + add("Cremate any Shade remains on a Magic or Redwood pyre.", + new SkillRequirement(Skill.FIREMAKING, 80), + new QuestRequirement(Quest.SHADES_OF_MORTTON)); + add("Fertilize the Morytania herb patch using Lunar Magic.", + new SkillRequirement(Skill.MAGIC, 83), + new QuestRequirement(Quest.LUNAR_DIPLOMACY)); + add("Craft a Black dragonhide body in Canifis bank.", + new SkillRequirement(Skill.CRAFTING, 84)); + add("Kill an Abyssal demon in the Slayer Tower.", + new SkillRequirement(Skill.SLAYER, 85)); + add("Loot the Barrows chest while wearing any complete barrows set.", + new SkillRequirement(Skill.DEFENCE, 70), + new OrRequirement( + new SkillRequirement(Skill.ATTACK, 70), + new SkillRequirement(Skill.STRENGTH, 70), + new SkillRequirement(Skill.RANGED, 70), + new SkillRequirement(Skill.MAGIC, 70) + ) + ); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/VarrockDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/VarrockDiaryRequirement.java new file mode 100644 index 0000000000..bff800b4c2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/VarrockDiaryRequirement.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.CombatLevelRequirement; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestPointRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class VarrockDiaryRequirement extends GenericDiaryRequirement +{ + public VarrockDiaryRequirement() + { + // EASY + add("Have Aubury teleport you to the Essence mine.", + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Mine some Iron in the south east mining patch near Varrock.", + new SkillRequirement(Skill.MINING, 15)); + add("Jump over the fence south of Varrock.", + new SkillRequirement(Skill.AGILITY, 13)); + add("Spin a bowl on the pottery wheel and fire it in the oven in Barb Village.", + new SkillRequirement(Skill.CRAFTING, 8)); + add("Craft some Earth runes.", + new SkillRequirement(Skill.RUNECRAFT, 9)); + add("Catch some trout in the River Lum at Barbarian Village.", + new SkillRequirement(Skill.FISHING, 20)); + add("Steal from the Tea stall in Varrock.", + new SkillRequirement(Skill.THIEVING, 5)); + + // MEDIUM + add("Enter the Champions' Guild.", + new QuestPointRequirement(32)); + add("Select a colour for your kitten.", + new QuestRequirement(Quest.GARDEN_OF_TRANQUILLITY, true), + new QuestRequirement(Quest.GERTRUDES_CAT)); + add("Use the spirit tree north of Varrock.", + new QuestRequirement(Quest.TREE_GNOME_VILLAGE)); + add("Enter the Tolna dungeon after completing A Soul's Bane.", + new QuestRequirement(Quest.A_SOULS_BANE)); + add("Teleport to the digsite using a Digsite pendant.", + new QuestRequirement(Quest.THE_DIG_SITE)); + add("Cast the teleport to Varrock spell.", + new SkillRequirement(Skill.MAGIC, 25)); + add("Get a Slayer task from Vannaka.", + new CombatLevelRequirement(40)); + add("Pick a White tree fruit.", + new SkillRequirement(Skill.FARMING, 25), + new QuestRequirement(Quest.GARDEN_OF_TRANQUILLITY)); + add("Use the balloon to travel from Varrock.", + new SkillRequirement(Skill.FIREMAKING, 40), + new QuestRequirement(Quest.ENLIGHTENED_JOURNEY)); + add("Complete a lap of the Varrock Agility course.", + new SkillRequirement(Skill.AGILITY, 30)); + + // HARD + add("Trade furs with the Fancy Dress Seller for a spottier cape and equip it.", + new SkillRequirement(Skill.HUNTER, 66)); + add("Make a Waka Canoe near Edgeville.", + new SkillRequirement(Skill.WOODCUTTING, 57)); + add("Teleport to Paddewwa.", + new SkillRequirement(Skill.MAGIC, 54), + new QuestRequirement(Quest.DESERT_TREASURE)); + add("Chop some yew logs in Varrock and burn them at the top of the Varrock church.", + new SkillRequirement(Skill.WOODCUTTING, 60), + new SkillRequirement(Skill.FIREMAKING, 60)); + add("Have the Varrock estate agent decorate your house with Fancy Stone.", + new SkillRequirement(Skill.CONSTRUCTION, 50)); + add("Collect at least 2 yew roots from the Tree patch in Varrock Palace.", + new SkillRequirement(Skill.WOODCUTTING, 60), + new SkillRequirement(Skill.FARMING, 68)); + add("Pray at the altar in Varrock palace with Smite active.", + new SkillRequirement(Skill.PRAYER, 52)); + add("Squeeze through the obstacle pipe in Edgeville dungeon.", + new SkillRequirement(Skill.AGILITY, 51)); + + // ELITE + add("Create a super combat potion in Varrock west bank.", + new SkillRequirement(Skill.HERBLORE, 90), + new QuestRequirement(Quest.DRUIDIC_RITUAL)); + add("Use Lunar magic to make 20 mahogany planks at the Lumberyard.", + new SkillRequirement(Skill.MAGIC, 86), + new QuestRequirement(Quest.DREAM_MENTOR)); + add("Bake a summer pie in the Cooking Guild.", + new SkillRequirement(Skill.COOKING, 95)); + add("Smith and fletch ten rune darts within Varrock.", + new SkillRequirement(Skill.SMITHING, 89), + new SkillRequirement(Skill.FLETCHING, 81), + new QuestRequirement(Quest.THE_TOURIST_TRAP)); + add("Craft 100 or more earth runes simultaneously.", + new SkillRequirement(Skill.RUNECRAFT, 78), + new QuestRequirement(Quest.RUNE_MYSTERIES)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WesternDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WesternDiaryRequirement.java new file mode 100644 index 0000000000..6a182feb51 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WesternDiaryRequirement.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.CombatLevelRequirement; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class WesternDiaryRequirement extends GenericDiaryRequirement +{ + public WesternDiaryRequirement() + { + // EASY + add("Catch a Copper Longtail.", + new SkillRequirement(Skill.HUNTER, 9)); + add("Complete a novice game of Pest Control.", + new CombatLevelRequirement(40)); + add("Mine some Iron Ore near Piscatoris.", + new SkillRequirement(Skill.MINING, 15)); + add("Claim any Chompy bird hat from Rantz.", + new QuestRequirement(Quest.BIG_CHOMPY_BIRD_HUNTING)); + add("Have Brimstail teleport you to the Essence mine.", + new QuestRequirement(Quest.RUNE_MYSTERIES)); + add("Fletch an Oak shortbow from the Gnome Stronghold.", + new SkillRequirement(Skill.FLETCHING, 20)); + + // MEDIUM + add("Take the agility shortcut from the Grand Tree to Otto's Grotto.", + new SkillRequirement(Skill.AGILITY, 37), + new QuestRequirement(Quest.TREE_GNOME_VILLAGE), + new QuestRequirement(Quest.THE_GRAND_TREE)); + add("Travel to the Gnome Stronghold by Spirit Tree.", + new QuestRequirement(Quest.TREE_GNOME_VILLAGE)); + add("Trap a Spined Larupia.", + new SkillRequirement(Skill.HUNTER, 31)); + add("Fish some Bass on Ape Atoll.", + new SkillRequirement(Skill.FISHING, 46), + new QuestRequirement(Quest.MONKEY_MADNESS_I, true)); + add("Chop and burn some teak logs on Ape Atoll.", + new SkillRequirement(Skill.WOODCUTTING, 35), + new SkillRequirement(Skill.FIREMAKING, 35), + new QuestRequirement(Quest.MONKEY_MADNESS_I, true)); + add("Complete an intermediate game of Pest Control.", + new CombatLevelRequirement(70)); + add("Travel to the Feldip Hills by Gnome Glider.", + new QuestRequirement(Quest.ONE_SMALL_FAVOUR), + new QuestRequirement(Quest.THE_GRAND_TREE)); + add("Claim a Chompy bird hat from Rantz after registering at least 125 kills.", + new QuestRequirement(Quest.BIG_CHOMPY_BIRD_HUNTING)); + add("Travel from Eagles' Peak to the Feldip Hills by Eagle.", + new QuestRequirement(Quest.EAGLES_PEAK)); + add("Make a Chocolate Bomb at the Grand Tree.", + new SkillRequirement(Skill.COOKING, 42)); + add("Complete a delivery for the Gnome Restaurant.", + new SkillRequirement(Skill.COOKING, 29)); + add("Turn your small crystal seed into a Crystal saw.", + new QuestRequirement(Quest.THE_EYES_OF_GLOUPHRIE)); + add("Mine some Gold ore underneath the Grand Tree.", + new SkillRequirement(Skill.MINING, 40), + new QuestRequirement(Quest.THE_GRAND_TREE)); + + // HARD + add("Kill an Elf with a Crystal bow.", + new SkillRequirement(Skill.RANGED, 70), + new SkillRequirement(Skill.AGILITY, 56), + new QuestRequirement(Quest.ROVING_ELVES)); + add("Catch and cook a Monkfish in Piscatoris.", + new SkillRequirement(Skill.FISHING, 62), + new SkillRequirement(Skill.COOKING, 62), + new QuestRequirement(Quest.SWAN_SONG)); + add("Complete a Veteran game of Pest Control.", + new CombatLevelRequirement(100)); + add("Catch a Dashing Kebbit.", + new SkillRequirement(Skill.HUNTER, 69)); + add("Complete a lap of the Ape Atoll agility course.", + new SkillRequirement(Skill.AGILITY, 48), + new QuestRequirement(Quest.MONKEY_MADNESS_I)); + add("Chop and burn some Mahogany logs on Ape Atoll.", + new SkillRequirement(Skill.WOODCUTTING, 50), + new SkillRequirement(Skill.FIREMAKING, 50), + new QuestRequirement(Quest.MONKEY_MADNESS_I)); + add("Mine some Adamantite ore in Tirannwn.", + new SkillRequirement(Skill.MINING, 70), + new QuestRequirement(Quest.REGICIDE)); + add("Check the health of your Palm tree in Lletya.", + new SkillRequirement(Skill.FARMING, 68), + new QuestRequirement(Quest.MOURNINGS_END_PART_I, true)); + add("Claim a Chompy bird hat from Rantz after registering at least 300 kills.", + new QuestRequirement(Quest.BIG_CHOMPY_BIRD_HUNTING)); + add("Build an Isafdar painting in your POH Quest hall.", + new SkillRequirement(Skill.CONSTRUCTION, 65), + new QuestRequirement(Quest.ROVING_ELVES)); + add("Kill Zulrah.", + new QuestRequirement(Quest.REGICIDE, true)); + add("Teleport to Ape Atoll.", + new SkillRequirement(Skill.MAGIC, 64), + new QuestRequirement(Quest.RECIPE_FOR_DISASTER, true)); + add("Pickpocket a Gnome.", + new SkillRequirement(Skill.THIEVING, 75), + new QuestRequirement(Quest.TREE_GNOME_VILLAGE)); + + // ELITE + add("Fletch a Magic Longbow in Tirannwn.", + new SkillRequirement(Skill.FLETCHING, 85), + new QuestRequirement(Quest.MOURNINGS_END_PART_I)); + add("Kill the Thermonuclear Smoke devil (Does not require task).", + new SkillRequirement(Skill.SLAYER, 93)); + add("Have Prissy Scilla protect your Magic tree.", + new SkillRequirement(Skill.FARMING, 75)); + add("Use the Elven overpass advanced cliffside shortcut.", + new SkillRequirement(Skill.AGILITY, 85), + new QuestRequirement(Quest.UNDERGROUND_PASS)); + add("Claim a Chompy bird hat from Rantz after registering at least 1000 kills.", + new QuestRequirement(Quest.BIG_CHOMPY_BIRD_HUNTING)); + add("Pickpocket an Elf.", + new SkillRequirement(Skill.THIEVING, 85), + new QuestRequirement(Quest.MOURNINGS_END_PART_I, true)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WildernessDiaryRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WildernessDiaryRequirement.java new file mode 100644 index 0000000000..0a059a1785 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/achievementdiary/diaries/WildernessDiaryRequirement.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018, Marshall + * 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.plugins.achievementdiary.diaries; + +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import net.runelite.client.plugins.achievementdiary.GenericDiaryRequirement; +import net.runelite.client.plugins.achievementdiary.OrRequirement; +import net.runelite.client.plugins.achievementdiary.QuestRequirement; +import net.runelite.client.plugins.achievementdiary.SkillRequirement; + +public class WildernessDiaryRequirement extends GenericDiaryRequirement +{ + public WildernessDiaryRequirement() + { + // EASY + add("Cast Low Alchemy at the Fountain of Rune.", + new SkillRequirement(Skill.MAGIC, 21)); + add("Kill an Earth Warrior in the Wilderness beneath Edgeville.", + new SkillRequirement(Skill.AGILITY, 15)); + add("Mine some Iron ore in the Wilderness.", + new SkillRequirement(Skill.MINING, 15)); + add("Have the Mage of Zamorak teleport you to the Abyss.", + new QuestRequirement(Quest.ENTER_THE_ABYSS)); + + // MEDIUM + add("Mine some Mithril ore in the wilderness.", + new SkillRequirement(Skill.MINING, 55)); + add("Chop some yew logs from a fallen Ent.", + new SkillRequirement(Skill.WOODCUTTING, 61)); + add("Enter the Wilderness Godwars Dungeon.", + new OrRequirement( + new SkillRequirement(Skill.AGILITY, 60), + new SkillRequirement(Skill.STRENGTH, 60) + ) + ); + add("Complete a lap of the Wilderness Agility course.", + new SkillRequirement(Skill.AGILITY, 52)); + add("Charge an Earth Orb.", + new SkillRequirement(Skill.MAGIC, 60)); + add("Kill a Bloodveld in the Wilderness Godwars Dungeon.", + new SkillRequirement(Skill.SLAYER, 50)); + add("Smith a Golden helmet in the Resource Area.", + new SkillRequirement(Skill.SMITHING, 50), + new QuestRequirement(Quest.BETWEEN_A_ROCK, true)); + + // HARD + add("Cast one of the 3 God spells against another player in the Wilderness.", + new SkillRequirement(Skill.MAGIC, 60), + new QuestRequirement(Quest.THE_MAGE_ARENA)); + add("Charge an Air Orb.", + new SkillRequirement(Skill.MAGIC, 66)); + add("Catch a Black Salamander in the Wilderness.", + new SkillRequirement(Skill.HUNTER, 67)); + add("Smith an Adamant scimitar in the Resource Area.", + new SkillRequirement(Skill.SMITHING, 75)); + add("Take the agility shortcut from Trollheim into the Wilderness.", + new SkillRequirement(Skill.AGILITY, 64), + new QuestRequirement(Quest.DEATH_PLATEAU)); + add("Kill a Spiritual warrior in the Wilderness Godwars Dungeon.", + new SkillRequirement(Skill.SLAYER, 68)); + add("Fish some Raw Lava Eel in the Wilderness.", + new SkillRequirement(Skill.FISHING, 53)); + + // ELITE + add("Teleport to Ghorrock.", + new SkillRequirement(Skill.MAGIC, 96), + new QuestRequirement(Quest.DESERT_TREASURE)); + add("Fish and Cook a Dark Crab in the Resource Area.", + new SkillRequirement(Skill.FISHING, 85), + new SkillRequirement(Skill.COOKING, 90)); + add("Smith a rune scimitar from scratch in the Resource Area.", + new SkillRequirement(Skill.MINING, 85), + new SkillRequirement(Skill.SMITHING, 90)); + add("Steal from the Rogues' chest.", + new SkillRequirement(Skill.THIEVING, 84)); + add("Slay a spiritual mage inside the wilderness Godwars Dungeon.", + new SkillRequirement(Skill.SLAYER, 83), + new OrRequirement( + new SkillRequirement(Skill.AGILITY, 60), + new SkillRequirement(Skill.STRENGTH, 60) + ) + ); + add("Cut and burn some magic logs in the Resource Area.", + new SkillRequirement(Skill.WOODCUTTING, 75), + new SkillRequirement(Skill.FIREMAKING, 75)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityArenaTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityArenaTimer.java new file mode 100644 index 0000000000..b47abee85a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityArenaTimer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, Alex Kolpa + * 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.agility; + +import java.awt.image.BufferedImage; +import java.time.temporal.ChronoUnit; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.infobox.Timer; + +class AgilityArenaTimer extends Timer +{ + AgilityArenaTimer(Plugin plugin, BufferedImage image) + { + super(1, ChronoUnit.MINUTES, image, plugin); + setTooltip("Time left until location changes"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityConfig.java new file mode 100644 index 0000000000..de9962d678 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityConfig.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2018, Cas + * 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.agility; + +import java.awt.Color; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Units; + +@ConfigGroup("agility") +public interface AgilityConfig extends Config +{ + @ConfigSection( + name = "Hallowed Sepulchre", + description = "Settings for Hallowed Sepulchre highlights", + position = 17 + ) + String sepulchreSection = "Hallowed Sepulchre"; + + @ConfigItem( + keyName = "showClickboxes", + name = "Show Clickboxes", + description = "Show agility course obstacle clickboxes", + position = 0 + ) + default boolean showClickboxes() + { + return true; + } + + @ConfigItem( + keyName = "showLapCount", + name = "Show Lap Count", + description = "Enable/disable the lap counter", + position = 1 + ) + default boolean showLapCount() + { + return true; + } + + @ConfigItem( + keyName = "lapTimeout", + name = "Hide Lap Count", + description = "Time until the lap counter hides/resets", + position = 2 + ) + @Units(Units.MINUTES) + default int lapTimeout() + { + return 5; + } + + @ConfigItem( + keyName = "lapsToLevel", + name = "Show Laps Until Goal", + description = "Show number of laps remaining until next goal is reached.", + position = 3 + ) + default boolean lapsToLevel() + { + return true; + } + + @ConfigItem( + keyName = "lapsPerHour", + name = "Show Laps Per Hour", + description = "Shows how many laps you can expect to complete per hour.", + position = 4 + ) + default boolean lapsPerHour() + { + return true; + } + + @ConfigItem( + keyName = "overlayColor", + name = "Overlay Color", + description = "Color of Agility overlay", + position = 5 + ) + default Color getOverlayColor() + { + return Color.GREEN; + } + + @ConfigItem( + keyName = "highlightMarks", + name = "Highlight Marks of Grace", + description = "Enable/disable the highlighting of retrievable Marks of Grace", + position = 6 + ) + default boolean highlightMarks() + { + return true; + } + + @ConfigItem( + keyName = "markHighlight", + name = "Mark Highlight Color", + description = "Color of highlighted Marks of Grace", + position = 7 + ) + default Color getMarkColor() + { + return Color.RED; + } + + @ConfigItem( + keyName = "highlightPortals", + name = "Highlight Portals", + description = "Enable/disable the highlighting of Prifddinas portals", + position = 8 + ) + default boolean highlightPortals() + { + return true; + } + + @ConfigItem( + keyName = "portalsHighlight", + name = "Portals Highlight Color", + description = "Color of highlighted Prifddinas portals", + position = 9 + ) + default Color getPortalsColor() + { + return Color.MAGENTA; + } + + @ConfigItem( + keyName = "highlightShortcuts", + name = "Highlight Agility Shortcuts", + description = "Enable/disable the highlighting of Agility shortcuts", + position = 10 + ) + default boolean highlightShortcuts() + { + return true; + } + + @ConfigItem( + keyName = "trapOverlay", + name = "Show Trap Overlay", + description = "Enable/disable the highlighting of traps on Agility courses", + position = 11 + ) + default boolean showTrapOverlay() + { + return true; + } + + @ConfigItem( + keyName = "trapHighlight", + name = "Trap Overlay Color", + description = "Color of Agility trap overlay", + position = 12 + ) + default Color getTrapColor() + { + return Color.RED; + } + + @ConfigItem( + keyName = "agilityArenaNotifier", + name = "Agility Arena notifier", + description = "Notify on ticket location change in Agility Arena", + position = 13 + ) + default boolean notifyAgilityArena() + { + return true; + } + + @ConfigItem( + keyName = "agilityArenaTimer", + name = "Agility Arena timer", + description = "Configures whether Agility Arena timer is displayed", + position = 14 + ) + default boolean showAgilityArenaTimer() + { + return true; + } + + @ConfigItem( + keyName = "highlightStick", + name = "Highlight Stick", + description = "Highlight the retrievable stick in the Werewolf Agility Course", + position = 15 + ) + default boolean highlightStick() + { + return true; + } + + @ConfigItem( + keyName = "stickHighlightColor", + name = "Stick Highlight Color", + description = "Color of highlighted stick", + position = 16 + ) + default Color stickHighlightColor() + { + return Color.RED; + } + + @ConfigItem( + keyName = "highlightSepulchreNpcs", + name = "Highlight Projectiles", + description = "Highlights arrows and swords in the Sepulchre", + position = 17, + section = sepulchreSection + ) + default boolean highlightSepulchreNpcs() + { + return true; + } + + @ConfigItem( + keyName = "sepulchreHighlightColor", + name = "Projectile Color", + description = "Overlay color for arrows and swords", + position = 18, + section = sepulchreSection + ) + default Color sepulchreHighlightColor() + { + return Color.GREEN; + } + + @ConfigItem( + keyName = "highlightSepulchreObstacles", + name = "Highlight Obstacles", + description = "Highlights pillars and stairs in the Sepulchre", + position = 19, + section = sepulchreSection + ) + default boolean highlightSepulchreObstacles() + { + return true; + } + + @ConfigItem( + keyName = "highlightSepulchreSkilling", + name = "Highlight Skill Challenges", + description = "Highlights skilling challenges in the Sepulchre", + position = 20, + section = sepulchreSection + ) + default boolean highlightSepulchreSkilling() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityOverlay.java new file mode 100644 index 0000000000..a7996744c2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityOverlay.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2018, Adam + * Copyright (c) 2018, Cas + * 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.agility; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Shape; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.Tile; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.game.AgilityShortcut; +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; + +class AgilityOverlay extends Overlay +{ + private static final int MAX_DISTANCE = 2350; + private static final Color SHORTCUT_HIGH_LEVEL_COLOR = Color.ORANGE; + + private final Client client; + private final AgilityPlugin plugin; + private final AgilityConfig config; + + @Inject + private AgilityOverlay(Client client, AgilityPlugin plugin, AgilityConfig config) + { + super(plugin); + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + this.client = client; + this.plugin = plugin; + this.config = config; + } + + @Override + public Dimension render(Graphics2D graphics) + { + LocalPoint playerLocation = client.getLocalPlayer().getLocalLocation(); + Point mousePosition = client.getMouseCanvasPosition(); + final List marksOfGrace = plugin.getMarksOfGrace(); + final Tile stickTile = plugin.getStickTile(); + + plugin.getObstacles().forEach((object, obstacle) -> + { + if (Obstacles.SHORTCUT_OBSTACLE_IDS.containsKey(object.getId()) && !config.highlightShortcuts() || + Obstacles.TRAP_OBSTACLE_IDS.contains(object.getId()) && !config.showTrapOverlay() || + Obstacles.COURSE_OBSTACLE_IDS.contains(object.getId()) && !config.showClickboxes() || + Obstacles.SEPULCHRE_OBSTACLE_IDS.contains(object.getId()) && !config.highlightSepulchreObstacles() || + Obstacles.SEPULCHRE_SKILL_OBSTACLE_IDS.contains(object.getId()) && !config.highlightSepulchreSkilling()) + { + return; + } + + Tile tile = obstacle.getTile(); + if (tile.getPlane() == client.getPlane() + && object.getLocalLocation().distanceTo(playerLocation) < MAX_DISTANCE) + { + // This assumes that the obstacle is not clickable. + if (Obstacles.TRAP_OBSTACLE_IDS.contains(object.getId())) + { + Polygon polygon = object.getCanvasTilePoly(); + if (polygon != null) + { + OverlayUtil.renderPolygon(graphics, polygon, config.getTrapColor()); + } + return; + } + Shape objectClickbox = object.getClickbox(); + if (objectClickbox != null) + { + AgilityShortcut agilityShortcut = obstacle.getShortcut(); + Color configColor = agilityShortcut == null || agilityShortcut.getLevel() <= plugin.getAgilityLevel() ? config.getOverlayColor() : SHORTCUT_HIGH_LEVEL_COLOR; + if (config.highlightMarks() && !marksOfGrace.isEmpty()) + { + configColor = config.getMarkColor(); + } + + if (Obstacles.PORTAL_OBSTACLE_IDS.contains(object.getId())) + { + if (config.highlightPortals()) + { + configColor = config.getPortalsColor(); + } + else + { + return; + } + } + + if (objectClickbox.contains(mousePosition.getX(), mousePosition.getY())) + { + graphics.setColor(configColor.darker()); + } + else + { + graphics.setColor(configColor); + } + + graphics.draw(objectClickbox); + graphics.setColor(new Color(configColor.getRed(), configColor.getGreen(), configColor.getBlue(), 50)); + graphics.fill(objectClickbox); + } + } + + }); + + if (config.highlightMarks() && !marksOfGrace.isEmpty()) + { + for (Tile markOfGraceTile : marksOfGrace) + { + highlightTile(graphics, playerLocation, markOfGraceTile, config.getMarkColor()); + } + } + + if (stickTile != null && config.highlightStick()) + { + highlightTile(graphics, playerLocation, stickTile, config.stickHighlightColor()); + } + + Set npcs = plugin.getNpcs(); + if (!npcs.isEmpty() && config.highlightSepulchreNpcs()) + { + Color color = config.sepulchreHighlightColor(); + for (NPC npc : npcs) + { + NPCComposition npcComposition = npc.getComposition(); + int size = npcComposition.getSize(); + LocalPoint lp = npc.getLocalLocation(); + + Polygon tilePoly = Perspective.getCanvasTileAreaPoly(client, lp, size); + if (tilePoly != null) + { + OverlayUtil.renderPolygon(graphics, tilePoly, color); + } + } + } + + return null; + } + + private void highlightTile(Graphics2D graphics, LocalPoint playerLocation, Tile tile, Color color) + { + if (tile.getPlane() == client.getPlane() && tile.getItemLayer() != null + && tile.getLocalLocation().distanceTo(playerLocation) < MAX_DISTANCE) + { + final Polygon poly = tile.getItemLayer().getCanvasTilePoly(); + + if (poly != null) + { + OverlayUtil.renderPolygon(graphics, poly, color); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityPlugin.java new file mode 100644 index 0000000000..b79de676b2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilityPlugin.java @@ -0,0 +1,500 @@ +/* + * 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.plugins.agility; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Provides; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.ItemID; +import static net.runelite.api.ItemID.AGILITY_ARENA_TICKET; +import net.runelite.api.MenuAction; +import net.runelite.api.NPC; +import net.runelite.api.NullNpcID; +import net.runelite.api.Player; +import static net.runelite.api.Skill.AGILITY; +import net.runelite.api.Tile; +import net.runelite.api.TileItem; +import net.runelite.api.TileObject; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.DecorativeObjectChanged; +import net.runelite.api.events.DecorativeObjectDespawned; +import net.runelite.api.events.DecorativeObjectSpawned; +import net.runelite.api.events.GameObjectChanged; +import net.runelite.api.events.GameObjectDespawned; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.GroundObjectChanged; +import net.runelite.api.events.GroundObjectDespawned; +import net.runelite.api.events.GroundObjectSpawned; +import net.runelite.api.events.ItemDespawned; +import net.runelite.api.events.ItemSpawned; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.StatChanged; +import net.runelite.api.events.WallObjectChanged; +import net.runelite.api.events.WallObjectDespawned; +import net.runelite.api.events.WallObjectSpawned; +import net.runelite.client.Notifier; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.events.OverlayMenuClicked; +import net.runelite.client.game.AgilityShortcut; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDependency; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.xptracker.XpTrackerPlugin; +import net.runelite.client.plugins.xptracker.XpTrackerService; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; + +@PluginDescriptor( + name = "Agility", + description = "Show helpful information about agility courses and obstacles", + tags = {"grace", "marks", "overlay", "shortcuts", "skilling", "traps", "sepulchre"} +) +@PluginDependency(XpTrackerPlugin.class) +@Slf4j +public class AgilityPlugin extends Plugin +{ + private static final int AGILITY_ARENA_REGION_ID = 11157; + private static final Set SEPULCHRE_NPCS = ImmutableSet.of( + NullNpcID.NULL_9672, NullNpcID.NULL_9673, NullNpcID.NULL_9674, // arrows + NullNpcID.NULL_9669, NullNpcID.NULL_9670, NullNpcID.NULL_9671 // swords + ); + + @Getter + private final Map obstacles = new HashMap<>(); + + @Getter + private final List marksOfGrace = new ArrayList<>(); + + @Getter + private final Set npcs = new HashSet<>(); + + @Inject + private OverlayManager overlayManager; + + @Inject + private AgilityOverlay agilityOverlay; + + @Inject + private LapCounterOverlay lapCounterOverlay; + + @Inject + private Notifier notifier; + + @Inject + private Client client; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private AgilityConfig config; + + @Inject + private ItemManager itemManager; + + @Inject + private XpTrackerService xpTrackerService; + + @Getter + private AgilitySession session; + + private int lastAgilityXp; + private WorldPoint lastArenaTicketPosition; + + @Getter + private int agilityLevel; + + @Getter(AccessLevel.PACKAGE) + private Tile stickTile; + + @Provides + AgilityConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(AgilityConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(agilityOverlay); + overlayManager.add(lapCounterOverlay); + agilityLevel = client.getBoostedSkillLevel(AGILITY); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(agilityOverlay); + overlayManager.remove(lapCounterOverlay); + marksOfGrace.clear(); + obstacles.clear(); + session = null; + agilityLevel = 0; + stickTile = null; + npcs.clear(); + } + + @Subscribe + public void onOverlayMenuClicked(OverlayMenuClicked overlayMenuClicked) + { + OverlayMenuEntry overlayMenuEntry = overlayMenuClicked.getEntry(); + if (overlayMenuEntry.getMenuAction() == MenuAction.RUNELITE_OVERLAY + && overlayMenuClicked.getOverlay() == lapCounterOverlay + && overlayMenuClicked.getEntry().getOption().equals(LapCounterOverlay.AGILITY_RESET)) + { + session = null; + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case HOPPING: + case LOGIN_SCREEN: + session = null; + lastArenaTicketPosition = null; + removeAgilityArenaTimer(); + npcs.clear(); + break; + case LOADING: + marksOfGrace.clear(); + obstacles.clear(); + stickTile = null; + break; + case LOGGED_IN: + if (!isInAgilityArena()) + { + lastArenaTicketPosition = null; + removeAgilityArenaTimer(); + } + break; + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!config.showAgilityArenaTimer()) + { + removeAgilityArenaTimer(); + } + } + + @Subscribe + public void onStatChanged(StatChanged statChanged) + { + if (statChanged.getSkill() != AGILITY) + { + return; + } + + agilityLevel = statChanged.getBoostedLevel(); + + if (!config.showLapCount()) + { + return; + } + + // Determine how much EXP was actually gained + int agilityXp = client.getSkillExperience(AGILITY); + int skillGained = agilityXp - lastAgilityXp; + lastAgilityXp = agilityXp; + + // Get course + Courses course = Courses.getCourse(client.getLocalPlayer().getWorldLocation().getRegionID()); + if (course == null + || (course.getCourseEndWorldPoints().length == 0 + ? Math.abs(course.getLastObstacleXp() - skillGained) > 1 + : Arrays.stream(course.getCourseEndWorldPoints()).noneMatch(wp -> wp.equals(client.getLocalPlayer().getWorldLocation())))) + { + return; + } + + if (session != null && session.getCourse() == course) + { + session.incrementLapCount(client, xpTrackerService); + } + else + { + session = new AgilitySession(course); + // New course found, reset lap count and set new course + session.resetLapCount(); + session.incrementLapCount(client, xpTrackerService); + } + } + + @Subscribe + public void onItemSpawned(ItemSpawned itemSpawned) + { + if (obstacles.isEmpty()) + { + return; + } + + final TileItem item = itemSpawned.getItem(); + final Tile tile = itemSpawned.getTile(); + + if (item.getId() == ItemID.MARK_OF_GRACE) + { + marksOfGrace.add(tile); + } + + if (item.getId() == ItemID.STICK) + { + stickTile = tile; + } + } + + @Subscribe + public void onItemDespawned(ItemDespawned itemDespawned) + { + final TileItem item = itemDespawned.getItem(); + final Tile tile = itemDespawned.getTile(); + + marksOfGrace.remove(tile); + + if (item.getId() == ItemID.STICK && stickTile == tile) + { + stickTile = null; + } + } + + @Subscribe + public void onGameTick(GameTick tick) + { + if (isInAgilityArena()) + { + // Hint arrow has no plane, and always returns the current plane + WorldPoint newTicketPosition = client.getHintArrowPoint(); + WorldPoint oldTickPosition = lastArenaTicketPosition; + + lastArenaTicketPosition = newTicketPosition; + + if (oldTickPosition != null && newTicketPosition != null + && (oldTickPosition.getX() != newTicketPosition.getX() || oldTickPosition.getY() != newTicketPosition.getY())) + { + log.debug("Ticked position moved from {} to {}", oldTickPosition, newTicketPosition); + + if (config.notifyAgilityArena()) + { + notifier.notify("Ticket location changed"); + } + + if (config.showAgilityArenaTimer()) + { + showNewAgilityArenaTimer(); + } + } + } + } + + private boolean isInAgilityArena() + { + Player local = client.getLocalPlayer(); + if (local == null) + { + return false; + } + + WorldPoint location = local.getWorldLocation(); + return location.getRegionID() == AGILITY_ARENA_REGION_ID; + } + + private void removeAgilityArenaTimer() + { + infoBoxManager.removeIf(infoBox -> infoBox instanceof AgilityArenaTimer); + } + + private void showNewAgilityArenaTimer() + { + removeAgilityArenaTimer(); + infoBoxManager.addInfoBox(new AgilityArenaTimer(this, itemManager.getImage(AGILITY_ARENA_TICKET))); + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) + { + onTileObject(event.getTile(), null, event.getGameObject()); + } + + @Subscribe + public void onGameObjectChanged(GameObjectChanged event) + { + onTileObject(event.getTile(), event.getPrevious(), event.getGameObject()); + } + + @Subscribe + public void onGameObjectDespawned(GameObjectDespawned event) + { + onTileObject(event.getTile(), event.getGameObject(), null); + } + + @Subscribe + public void onGroundObjectSpawned(GroundObjectSpawned event) + { + onTileObject(event.getTile(), null, event.getGroundObject()); + } + + @Subscribe + public void onGroundObjectChanged(GroundObjectChanged event) + { + onTileObject(event.getTile(), event.getPrevious(), event.getGroundObject()); + } + + @Subscribe + public void onGroundObjectDespawned(GroundObjectDespawned event) + { + onTileObject(event.getTile(), event.getGroundObject(), null); + } + + @Subscribe + public void onWallObjectSpawned(WallObjectSpawned event) + { + onTileObject(event.getTile(), null, event.getWallObject()); + } + + @Subscribe + public void onWallObjectChanged(WallObjectChanged event) + { + onTileObject(event.getTile(), event.getPrevious(), event.getWallObject()); + } + + @Subscribe + public void onWallObjectDespawned(WallObjectDespawned event) + { + onTileObject(event.getTile(), event.getWallObject(), null); + } + + @Subscribe + public void onDecorativeObjectSpawned(DecorativeObjectSpawned event) + { + onTileObject(event.getTile(), null, event.getDecorativeObject()); + } + + @Subscribe + public void onDecorativeObjectChanged(DecorativeObjectChanged event) + { + onTileObject(event.getTile(), event.getPrevious(), event.getDecorativeObject()); + } + + @Subscribe + public void onDecorativeObjectDespawned(DecorativeObjectDespawned event) + { + onTileObject(event.getTile(), event.getDecorativeObject(), null); + } + + private void onTileObject(Tile tile, TileObject oldObject, TileObject newObject) + { + obstacles.remove(oldObject); + + if (newObject == null) + { + return; + } + + if (Obstacles.COURSE_OBSTACLE_IDS.contains(newObject.getId()) || + Obstacles.PORTAL_OBSTACLE_IDS.contains(newObject.getId()) || + (Obstacles.TRAP_OBSTACLE_IDS.contains(newObject.getId()) + && Obstacles.TRAP_OBSTACLE_REGIONS.contains(newObject.getWorldLocation().getRegionID())) || + Obstacles.SEPULCHRE_OBSTACLE_IDS.contains(newObject.getId()) || + Obstacles.SEPULCHRE_SKILL_OBSTACLE_IDS.contains(newObject.getId())) + { + obstacles.put(newObject, new Obstacle(tile, null)); + } + + if (Obstacles.SHORTCUT_OBSTACLE_IDS.containsKey(newObject.getId())) + { + AgilityShortcut closestShortcut = null; + int distance = -1; + + // Find the closest shortcut to this object + for (AgilityShortcut shortcut : Obstacles.SHORTCUT_OBSTACLE_IDS.get(newObject.getId())) + { + if (!shortcut.matches(newObject)) + { + continue; + } + + if (shortcut.getWorldLocation() == null) + { + closestShortcut = shortcut; + break; + } + else + { + int newDistance = shortcut.getWorldLocation().distanceTo2D(newObject.getWorldLocation()); + if (closestShortcut == null || newDistance < distance) + { + closestShortcut = shortcut; + distance = newDistance; + } + } + } + + if (closestShortcut != null) + { + obstacles.put(newObject, new Obstacle(tile, closestShortcut)); + } + } + } + + @Subscribe + public void onNpcSpawned(NpcSpawned npcSpawned) + { + NPC npc = npcSpawned.getNpc(); + + if (SEPULCHRE_NPCS.contains(npc.getId())) + { + npcs.add(npc); + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + NPC npc = npcDespawned.getNpc(); + npcs.remove(npc); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilitySession.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilitySession.java new file mode 100644 index 0000000000..2a1174a6e6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/AgilitySession.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2018, Seth + * 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.agility; + +import com.google.common.collect.EvictingQueue; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.client.plugins.xptracker.XpTrackerService; +import java.time.Duration; +import java.time.Instant; + +@Getter +@Setter +class AgilitySession +{ + private final Courses course; + private Instant lastLapCompleted; + private int totalLaps; + private int lapsTillGoal; + private final EvictingQueue lastLapTimes = EvictingQueue.create(30); + private int lapsPerHour; + + AgilitySession(Courses course) + { + this.course = course; + } + + void incrementLapCount(Client client, XpTrackerService xpTrackerService) + { + calculateLapsPerHour(); + + ++totalLaps; + + final int currentExp = client.getSkillExperience(Skill.AGILITY); + final int goalXp = xpTrackerService.getEndGoalXp(Skill.AGILITY); + final int goalRemainingXp = goalXp - currentExp; + double courseTotalExp = course.getTotalXp(); + if (course == Courses.PYRAMID) + { + // agility pyramid has a bonus exp drop on the last obstacle that scales with player level and caps at 1000 + // the bonus is not already accounted for in the total exp number in the courses enum + courseTotalExp += Math.min(300 + 8 * client.getRealSkillLevel(Skill.AGILITY), 1000); + } + + lapsTillGoal = goalRemainingXp > 0 ? (int) Math.ceil(goalRemainingXp / courseTotalExp) : 0; + } + + void calculateLapsPerHour() + { + Instant now = Instant.now(); + + if (lastLapCompleted != null) + { + Duration timeSinceLastLap = Duration.between(lastLapCompleted, now); + + if (!timeSinceLastLap.isNegative()) + { + lastLapTimes.add(timeSinceLastLap); + + Duration sum = Duration.ZERO; + for (Duration lapTime : lastLapTimes) + { + sum = sum.plus(lapTime); + } + + Duration averageLapTime = sum.dividedBy(lastLapTimes.size()); + lapsPerHour = (int) (Duration.ofHours(1).toMillis() / averageLapTime.toMillis()); + } + } + + lastLapCompleted = now; + } + + void resetLapCount() + { + totalLaps = 0; + lapsTillGoal = 0; + lastLapTimes.clear(); + lapsPerHour = 0; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/Courses.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/Courses.java new file mode 100644 index 0000000000..1193368164 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/Courses.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018, Seth + * 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.agility; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import lombok.Getter; +import net.runelite.api.coords.WorldPoint; + +enum Courses +{ + GNOME(86.5, 46, 9781), + DRAYNOR(120.0, 79, 12338), + AL_KHARID(180.0, 0, 13105, new WorldPoint(3299, 3194, 0)), + PYRAMID(722.0, 0, 13356, new WorldPoint(3364, 2830, 0)), + VARROCK(238.0, 125, 12853), + PENGUIN(540.0, 65, 10559), + BARBARIAN(139.5, 60, 10039), + CANIFIS(240.0, 175, 13878), + APE_ATOLL(580.0, 300, 11050), + FALADOR(440, 180, 12084), + WILDERNESS(571.0, 499, 11837), + WEREWOLF(730.0, 380, 14234), + SEERS(570.0, 435, 10806), + POLLNIVNEACH(890.0, 540, 13358), + RELLEKA(780.0, 475, 10553), + PRIFDDINAS(1337.0, 1037, 12895), + ARDOUGNE(793.0, 529, 10547); + + private final static Map coursesByRegion; + + @Getter + private final double totalXp; + + @Getter + private final int lastObstacleXp; + + @Getter + private final int regionId; + + @Getter + private final WorldPoint[] courseEndWorldPoints; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + + for (Courses course : values()) + { + builder.put(course.regionId, course); + } + + coursesByRegion = builder.build(); + } + + Courses(double totalXp, int lastObstacleXp, int regionId, WorldPoint... courseEndWorldPoints) + { + this.totalXp = totalXp; + this.lastObstacleXp = lastObstacleXp; + this.regionId = regionId; + this.courseEndWorldPoints = courseEndWorldPoints; + } + + static Courses getCourse(int regionId) + { + return coursesByRegion.get(regionId); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/LapCounterOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/LapCounterOverlay.java new file mode 100644 index 0000000000..e1e8863fbf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/LapCounterOverlay.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018, Seth + * 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.agility; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.time.Duration; +import java.time.Instant; +import javax.inject.Inject; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.components.LineComponent; + +class LapCounterOverlay extends OverlayPanel +{ + static final String AGILITY_RESET = "Reset"; + + private final AgilityPlugin plugin; + private final AgilityConfig config; + + @Inject + private LapCounterOverlay(AgilityPlugin plugin, AgilityConfig config) + { + super(plugin); + setPosition(OverlayPosition.TOP_LEFT); + setPriority(OverlayPriority.LOW); + this.plugin = plugin; + this.config = config; + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "Agility overlay")); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY, AGILITY_RESET, "Agility overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + AgilitySession session = plugin.getSession(); + + if (!config.showLapCount() || + session == null || + session.getLastLapCompleted() == null || + session.getCourse() == null) + { + return null; + } + + Duration lapTimeout = Duration.ofMinutes(config.lapTimeout()); + Duration sinceLap = Duration.between(session.getLastLapCompleted(), Instant.now()); + + if (sinceLap.compareTo(lapTimeout) >= 0) + { + // timeout session + session.setLastLapCompleted(null); + return null; + } + + panelComponent.getChildren().add(LineComponent.builder() + .left("Total Laps:") + .right(Integer.toString(session.getTotalLaps())) + .build()); + + if (config.lapsToLevel() && session.getLapsTillGoal() > 0) + { + panelComponent.getChildren().add(LineComponent.builder() + .left("Laps until goal:") + .right(Integer.toString(session.getLapsTillGoal())) + .build()); + } + + if (config.lapsPerHour() && session.getLapsPerHour() > 0) + { + panelComponent.getChildren().add(LineComponent.builder() + .left("Laps per hour:") + .right(Integer.toString(session.getLapsPerHour())) + .build()); + } + + return super.render(graphics); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacle.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacle.java new file mode 100644 index 0000000000..6038de468b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacle.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019, MrGroggle + * 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 HOLDER 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.agility; + +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Value; +import net.runelite.api.Tile; +import net.runelite.client.game.AgilityShortcut; + +@Value +@AllArgsConstructor +class Obstacle +{ + private final Tile tile; + @Nullable + private final AgilityShortcut shortcut; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacles.java b/runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacles.java new file mode 100644 index 0000000000..dd8809820b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/agility/Obstacles.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * 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.agility; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import java.util.List; +import java.util.Set; +import static net.runelite.api.NullObjectID.*; +import static net.runelite.api.ObjectID.*; +import net.runelite.client.game.AgilityShortcut; + +class Obstacles +{ + static final Set COURSE_OBSTACLE_IDS = ImmutableSet.of( + // Gnome + OBSTACLE_NET_23134, TREE_BRANCH_23559, TREE_BRANCH_23560, OBSTACLE_NET_23135, OBSTACLE_PIPE_23138, + OBSTACLE_PIPE_23139, LOG_BALANCE_23145, BALANCING_ROPE_23557, + // Brimhaven + PLANK_3572, PLANK_3571, PLANK_3570, ROPE_SWING, PILLAR_3578, LOW_WALL, LOG_BALANCE, LOG_BALANCE_3557, + BALANCING_LEDGE_3561, BALANCING_LEDGE, MONKEY_BARS_3564, BALANCING_ROPE, HAND_HOLDS_3583, + // Draynor + ROUGH_WALL, TIGHTROPE, TIGHTROPE_11406, NARROW_WALL, WALL_11630, GAP_11631, CRATE_11632, STILE_7527, + // Al-Kharid + ROUGH_WALL_11633, TIGHTROPE_14398, CABLE, ZIP_LINE_14403, TROPICAL_TREE_14404, ROOF_TOP_BEAMS, + TIGHTROPE_14409, GAP_14399, + // Pyramid + STAIRS_10857, LOW_WALL_10865, LEDGE_10860, PLANK_10868, GAP_10882, LEDGE_10886, STAIRS_10857, GAP_10884, + GAP_10859, GAP_10861, LOW_WALL_10865, GAP_10859, LEDGE_10888, PLANK_10868, CLIMBING_ROCKS_10851, DOORWAY_10855, + // Varrock + ROUGH_WALL_14412, CLOTHES_LINE, GAP_14414, WALL_14832, GAP_14833, GAP_14834, GAP_14835, LEDGE_14836, EDGE, + // Penguin + STEPPING_STONE_21120, STEPPING_STONE_21126, STEPPING_STONE_21128, STEPPING_STONE_21129, + STEPPING_STONE_21130, STEPPING_STONE_21131, STEPPING_STONE_21132, STEPPING_STONE_21133, + ICICLES, ICE, ICE_21149, ICE_21150, ICE_21151, ICE_21152, ICE_21153, ICE_21154, ICE_21155, ICE_21156, GATE_21172, + // Barbarian + ROPESWING_23131, LOG_BALANCE_23144, OBSTACLE_NET_20211, BALANCING_LEDGE_23547, LADDER_16682, CRUMBLING_WALL_1948, + // Canifis + TALL_TREE_14843, GAP_14844, GAP_14845, GAP_14848, GAP_14846, POLEVAULT, GAP_14847, GAP_14897, + // Ape atoll + STEPPING_STONE_15412, TROPICAL_TREE_15414, MONKEYBARS_15417, SKULL_SLOPE_15483, ROPE_15487, TROPICAL_TREE_16062, + // Falador + ROUGH_WALL_14898, TIGHTROPE_14899, HAND_HOLDS_14901, GAP_14903, GAP_14904, TIGHTROPE_14905, + TIGHTROPE_14911, GAP_14919, LEDGE_14920, LEDGE_14921, LEDGE_14922, LEDGE_14923, LEDGE_14924, EDGE_14925, + // Wilderness + OBSTACLE_PIPE_23137, ROPESWING_23132, STEPPING_STONE_23556, LOG_BALANCE_23542, ROCKS_23640, + // Seers + WALL_14927, GAP_14928, TIGHTROPE_14932, GAP_14929, GAP_14930, EDGE_14931, + // Dorgesh-Kaan + CABLE_22569, CABLE_22572, LADDER_22564, JUTTING_WALL_22552, TUNNEL_22557, PYLON_22664, + CONSOLE, BOILER_22635, STAIRS_22650, STAIRS_22651, STAIRS_22609, STAIRS_22608, + // Pollniveach + BASKET_14935, MARKET_STALL_14936, BANNER_14937, GAP_14938, TREE_14939, ROUGH_WALL_14940, + MONKEYBARS, TREE_14944, DRYING_LINE, + // Rellaka + ROUGH_WALL_14946, GAP_14947, TIGHTROPE_14987, GAP_14990, GAP_14991, TIGHTROPE_14992, PILE_OF_FISH, + // Ardougne + WOODEN_BEAMS, GAP_15609, PLANK_26635, GAP_15610, GAP_15611, STEEP_ROOF, GAP_15612, + // Meiyerditch + NULL_12945, ROCK_17958, ROCK_17959, ROCK_17960, BOAT_17961, NULL_18122, NULL_18124, WALL_RUBBLE, + WALL_RUBBLE_18038, FLOORBOARDS, FLOORBOARDS_18071, FLOORBOARDS_18072, FLOORBOARDS_18073, NULL_18129, NULL_18130, + WALL_18078, NULL_18132, NULL_18133, NULL_18083, TUNNEL_18085, SHELF_18086, SHELF_18087, WALL_18088, + FLOORBOARDS_18089, FLOORBOARDS_18090, DOOR_18091, FLOORBOARDS_18093, FLOORBOARDS_18094, SHELF_18095, + SHELF_18096, FLOORBOARDS_18097, FLOORBOARDS_18098, WASHING_LINE_18099, WASHING_LINE_18100, + NULL_18135, NULL_18136, SHELF_18105, SHELF_18106, SHELF_18107, SHELF_18108, FLOORBOARDS_18109, + FLOORBOARDS_18110, FLOORBOARDS_18112, FLOORBOARDS_18111, FLOORBOARDS_18114, FLOORBOARDS_18113, + NULL_18116, FLOORBOARDS_18117, FLOORBOARDS_18118, STAIRS_DOWN, WALL_17980, BARRICADE_18054, LADDER_17999, + LADDER_18000, LADDER_18001, LADDER_18002, ROCKY_SURFACE, WALL_39172, WALL_39173, + // Werewolf + STEPPING_STONE_11643, HURDLE, HURDLE_11639, HURDLE_11640, PIPE_11657, SKULL_SLOPE, ZIP_LINE, + ZIP_LINE_11645, ZIP_LINE_11646, + // Prifddinas + LADDER_36221, TIGHTROPE_36225, CHIMNEY_36227, ROOF_EDGE, DARK_HOLE_36229, LADDER_36231, LADDER_36232, + ROPE_BRIDGE_36233, TIGHTROPE_36234, ROPE_BRIDGE_36235, TIGHTROPE_36236, TIGHTROPE_36237, DARK_HOLE_36238 + ); + + static final Set PORTAL_OBSTACLE_IDS = ImmutableSet.of( + // Prifddinas portals + NULL_36241, NULL_36242, NULL_36243, NULL_36244, NULL_36245, NULL_36246 + ); + + static final Multimap SHORTCUT_OBSTACLE_IDS; + + static final Set TRAP_OBSTACLE_IDS = ImmutableSet.of( + // Agility pyramid + NULL_3550, NULL_10872, NULL_10873 + ); + + static final List TRAP_OBSTACLE_REGIONS = ImmutableList.of(12105, 13356); + + static + { + final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + for (final AgilityShortcut item : AgilityShortcut.values()) + { + for (int obstacle : item.getObstacleIds()) + { + builder.put(obstacle, item); + } + } + SHORTCUT_OBSTACLE_IDS = builder.build(); + } + + static final Set SEPULCHRE_OBSTACLE_IDS = ImmutableSet.of( + // Stairs and Platforms (and one Gate) + GATE_38460, PLATFORM_38455, PLATFORM_38456, PLATFORM_38457, PLATFORM_38458, PLATFORM_38459, + PLATFORM_38470, PLATFORM_38477, STAIRS_38462, STAIRS_38463, STAIRS_38464, STAIRS_38465, + STAIRS_38466, STAIRS_38467, STAIRS_38468, STAIRS_38469, STAIRS_38471, STAIRS_38472, + STAIRS_38473, STAIRS_38474, STAIRS_38475, STAIRS_38476 + ); + + static final Set SEPULCHRE_SKILL_OBSTACLE_IDS = ImmutableSet.of( + // Grapple, Portal, and Bridge skill obstacles + // They are multilocs, thus we use the NullObjectID + NULL_39524, NULL_39525, NULL_39526, NULL_39527, NULL_39528, NULL_39533 + ); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoCounter.java b/runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoCounter.java new file mode 100644 index 0000000000..8806fb78e1 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoCounter.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019 Hydrox6 + * 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.ammo; + +import java.awt.image.BufferedImage; +import lombok.Getter; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.infobox.Counter; +import net.runelite.client.util.QuantityFormatter; + +class AmmoCounter extends Counter +{ + @Getter + private final int itemID; + private final String name; + + AmmoCounter(Plugin plugin, int itemID, int count, String name, BufferedImage image) + { + super(image, plugin, count); + this.itemID = itemID; + this.name = name; + } + + @Override + public String getText() + { + return QuantityFormatter.quantityToRSDecimalStack(getCount()); + } + + @Override + public String getTooltip() + { + return name; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoPlugin.java new file mode 100644 index 0000000000..91aacbb22a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ammo/AmmoPlugin.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2019 Hydrox6 + * 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.ammo; + +import java.awt.image.BufferedImage; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.EquipmentInventorySlot; +import net.runelite.api.InventoryID; +import net.runelite.api.Item; +import net.runelite.api.ItemComposition; +import net.runelite.api.ItemContainer; +import net.runelite.api.events.ItemContainerChanged; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; + +@PluginDescriptor( + name = "Ammo", + description = "Shows the current ammo the player has equipped", + tags = {"bolts", "darts", "chinchompa", "equipment"} +) +public class AmmoPlugin extends Plugin +{ + @Inject + private Client client; + + @Inject + private ClientThread clientThread; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private ItemManager itemManager; + + private AmmoCounter counterBox; + + @Override + protected void startUp() throws Exception + { + clientThread.invokeLater(() -> + { + final ItemContainer container = client.getItemContainer(InventoryID.EQUIPMENT); + + if (container != null) + { + checkInventory(container.getItems()); + } + }); + } + + @Override + protected void shutDown() throws Exception + { + infoBoxManager.removeInfoBox(counterBox); + counterBox = null; + } + + @Subscribe + public void onItemContainerChanged(ItemContainerChanged event) + { + if (event.getItemContainer() != client.getItemContainer(InventoryID.EQUIPMENT)) + { + return; + } + + checkInventory(event.getItemContainer().getItems()); + } + + private void checkInventory(final Item[] items) + { + // Check for weapon slot items. This overrides the ammo slot, + // as the player will use the thrown weapon (eg. chinchompas, knives, darts) + if (items.length > EquipmentInventorySlot.WEAPON.getSlotIdx()) + { + final Item weapon = items[EquipmentInventorySlot.WEAPON.getSlotIdx()]; + final ItemComposition weaponComp = itemManager.getItemComposition(weapon.getId()); + if (weaponComp.isStackable()) + { + updateInfobox(weapon, weaponComp); + return; + } + } + + if (items.length <= EquipmentInventorySlot.AMMO.getSlotIdx()) + { + removeInfobox(); + return; + } + + final Item ammo = items[EquipmentInventorySlot.AMMO.getSlotIdx()]; + final ItemComposition comp = itemManager.getItemComposition(ammo.getId()); + + if (!comp.isStackable()) + { + removeInfobox(); + return; + } + + updateInfobox(ammo, comp); + } + + private void updateInfobox(final Item item, final ItemComposition comp) + { + if (counterBox != null && counterBox.getItemID() == item.getId()) + { + counterBox.setCount(item.getQuantity()); + return; + } + + removeInfobox(); + final BufferedImage image = itemManager.getImage(item.getId(), 5, false); + counterBox = new AmmoCounter(this, item.getId(), item.getQuantity(), comp.getName(), image); + infoBoxManager.addInfoBox(counterBox); + } + + private void removeInfobox() + { + infoBoxManager.removeInfoBox(counterBox); + counterBox = null; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java new file mode 100644 index 0000000000..51d0e1328d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobe.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Skill; + +@Getter +@Setter +@AllArgsConstructor +class XpGlobe +{ + private Skill skill; + private int currentXp; + private int currentLevel; + private Instant time; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java new file mode 100644 index 0000000000..c32a8c598a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesConfig.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import java.awt.Color; +import net.runelite.client.config.Alpha; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Units; + +@ConfigGroup("xpglobes") +public interface XpGlobesConfig extends Config +{ + @ConfigItem( + keyName = "enableTooltips", + name = "Enable Tooltips", + description = "Configures whether or not to show tooltips", + position = 0 + ) + default boolean enableTooltips() + { + return true; + } + + @ConfigItem( + keyName = "showXpLeft", + name = "Show XP Left", + description = "Shows XP Left inside the globe tooltip box", + position = 1 + ) + default boolean showXpLeft() + { + return true; + } + + @ConfigItem( + keyName = "showActionsLeft", + name = "Show actions left", + description = "Shows the number of actions left inside the globe tooltip box", + position = 2 + ) + default boolean showActionsLeft() + { + return true; + } + + @ConfigItem( + keyName = "showXpHour", + name = "Show XP/hr", + description = "Shows XP per hour inside the globe tooltip box", + position = 3 + ) + default boolean showXpHour() + { + return true; + } + + @ConfigItem( + keyName = "hideMaxed", + name = "Hide maxed skills", + description = "Stop globes from showing up for level 99 skills", + position = 4 + ) + default boolean hideMaxed() + { + return false; + } + + @ConfigItem( + keyName = "enableCustomArcColor", + name = "Enable custom arc color", + description = "Enables the custom coloring of the globe's arc instead of using the skill's default color.", + position = 5 + ) + default boolean enableCustomArcColor() + { + return false; + } + + @Alpha + @ConfigItem( + keyName = "Progress arc color", + name = "Progress arc color", + description = "Change the color of the progress arc in the xp orb", + position = 6 + ) + default Color progressArcColor() + { + return Color.ORANGE; + } + + @Alpha + @ConfigItem( + keyName = "Progress orb outline color", + name = "Progress orb outline color", + description = "Change the color of the progress orb outline", + position = 7 + ) + default Color progressOrbOutLineColor() + { + return Color.BLACK; + } + + @Alpha + @ConfigItem( + keyName = "Progress orb background color", + name = "Progress orb background color", + description = "Change the color of the progress orb background", + position = 8 + ) + default Color progressOrbBackgroundColor() + { + return new Color(128, 128, 128, 127); + } + + @ConfigItem( + keyName = "Progress arc width", + name = "Progress arc width", + description = "Change the stroke width of the progress arc", + position = 9 + ) + @Units(Units.PIXELS) + default int progressArcStrokeWidth() + { + return 2; + } + + @ConfigItem( + keyName = "Orb size", + name = "Size of orbs", + description = "Change the size of the xp orbs", + position = 10 + ) + @Units(Units.PIXELS) + default int xpOrbSize() + { + return 40; + } + + @ConfigItem( + keyName = "Orb duration", + name = "Duration of orbs", + description = "Change the duration the xp orbs are visible", + position = 11 + ) + @Units(Units.SECONDS) + default int xpOrbDuration() + { + return 10; + } + + @ConfigItem( + keyName = "alignOrbsVertically", + name = "Vertical Orbs", + description = "Aligns the orbs vertically instead of horizontally.", + hidden = true + ) + default boolean alignOrbsVertically() + { + return false; + } + + @ConfigItem( + keyName = "alignOrbsVertically", + name = "", + description = "" + ) + void setAlignOrbsVertically(Boolean alignOrbsVertically); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java new file mode 100644 index 0000000000..23da426c22 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesOverlay.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.geom.Arc2D; +import java.awt.geom.Ellipse2D; +import java.awt.image.BufferedImage; +import java.text.DecimalFormat; +import java.time.Instant; +import java.util.List; +import javax.inject.Inject; +import net.runelite.api.Client; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import net.runelite.api.Point; +import net.runelite.client.game.SkillIconManager; +import net.runelite.client.plugins.xptracker.XpActionType; +import net.runelite.client.plugins.xptracker.XpTrackerService; +import net.runelite.client.ui.SkillColor; +import net.runelite.client.ui.overlay.Overlay; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.tooltip.Tooltip; +import net.runelite.client.ui.overlay.tooltip.TooltipManager; + +public class XpGlobesOverlay extends Overlay +{ + private static final int MINIMUM_STEP = 10; + private static final int PROGRESS_RADIUS_START = 90; + private static final int PROGRESS_RADIUS_REMAINDER = 0; + private static final int TOOLTIP_RECT_SIZE_X = 150; + private static final Color DARK_OVERLAY_COLOR = new Color(0, 0, 0, 180); + static final String FLIP_ACTION = "Flip"; + + private final Client client; + private final XpGlobesPlugin plugin; + private final XpGlobesConfig config; + private final XpTrackerService xpTrackerService; + private final TooltipManager tooltipManager; + private final SkillIconManager iconManager; + private final Tooltip xpTooltip = new Tooltip(new PanelComponent()); + + @Inject + private XpGlobesOverlay( + Client client, + XpGlobesPlugin plugin, + XpGlobesConfig config, + XpTrackerService xpTrackerService, + SkillIconManager iconManager, + TooltipManager tooltipManager) + { + super(plugin); + this.iconManager = iconManager; + this.client = client; + this.plugin = plugin; + this.config = config; + this.xpTrackerService = xpTrackerService; + this.tooltipManager = tooltipManager; + this.xpTooltip.getComponent().setPreferredSize(new Dimension(TOOLTIP_RECT_SIZE_X, 0)); + setPosition(OverlayPosition.TOP_CENTER); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "XP Globes overlay")); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY, FLIP_ACTION, "XP Globes overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + final List xpGlobes = plugin.getXpGlobes(); + final int queueSize = xpGlobes.size(); + if (queueSize == 0) + { + return null; + } + + int curDrawPosition = 0; + for (final XpGlobe xpGlobe : xpGlobes) + { + int startXp = xpTrackerService.getStartGoalXp(xpGlobe.getSkill()); + int goalXp = xpTrackerService.getEndGoalXp(xpGlobe.getSkill()); + if (config.alignOrbsVertically()) + { + renderProgressCircle(graphics, xpGlobe, startXp, goalXp, 0, curDrawPosition, getBounds()); + } + else + { + renderProgressCircle(graphics, xpGlobe, startXp, goalXp, curDrawPosition, 0, getBounds()); + } + curDrawPosition += MINIMUM_STEP + config.xpOrbSize(); + } + + // Get length of markers + final int markersLength = (queueSize * (config.xpOrbSize())) + ((MINIMUM_STEP) * (queueSize - 1)); + if (config.alignOrbsVertically()) + { + return new Dimension(config.xpOrbSize(), markersLength); + } + else + { + return new Dimension(markersLength, config.xpOrbSize()); + } + } + + private double getSkillProgress(int startXp, int currentXp, int goalXp) + { + double xpGained = currentXp - startXp; + double xpGoal = goalXp - startXp; + + return ((xpGained / xpGoal) * 100); + } + + private double getSkillProgressRadius(int startXp, int currentXp, int goalXp) + { + return -(3.6 * getSkillProgress(startXp, currentXp, goalXp)); //arc goes backwards + } + + private void renderProgressCircle(Graphics2D graphics, XpGlobe skillToDraw, int startXp, int goalXp, int x, int y, Rectangle bounds) + { + double radiusCurrentXp = getSkillProgressRadius(startXp, skillToDraw.getCurrentXp(), goalXp); + double radiusToGoalXp = 360; //draw a circle + + Ellipse2D backgroundCircle = drawEllipse(graphics, x, y); + + drawSkillImage(graphics, skillToDraw, x, y); + + Point mouse = client.getMouseCanvasPosition(); + int mouseX = mouse.getX() - bounds.x; + int mouseY = mouse.getY() - bounds.y; + + // If mouse is hovering the globe + if (backgroundCircle.contains(mouseX, mouseY)) + { + // Fill a darker overlay circle + graphics.setColor(DARK_OVERLAY_COLOR); + graphics.fill(backgroundCircle); + + drawProgressLabel(graphics, skillToDraw, startXp, goalXp, x, y); + + if (config.enableTooltips()) + { + drawTooltip(skillToDraw, goalXp); + } + } + + graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + + drawProgressArc( + graphics, + x, y, + config.xpOrbSize(), config.xpOrbSize(), + PROGRESS_RADIUS_REMAINDER, radiusToGoalXp, + 5, + config.progressOrbOutLineColor() + ); + drawProgressArc( + graphics, + x, y, + config.xpOrbSize(), config.xpOrbSize(), + PROGRESS_RADIUS_START, radiusCurrentXp, + config.progressArcStrokeWidth(), + config.enableCustomArcColor() ? config.progressArcColor() : SkillColor.find(skillToDraw.getSkill()).getColor()); + } + + private void drawProgressLabel(Graphics2D graphics, XpGlobe globe, int startXp, int goalXp, int x, int y) + { + if (goalXp <= globe.getCurrentXp()) + { + return; + } + + // Convert to int just to limit the decimal cases + String progress = (int) (getSkillProgress(startXp, globe.getCurrentXp(), goalXp)) + "%"; + + final FontMetrics metrics = graphics.getFontMetrics(); + int drawX = x + (config.xpOrbSize() / 2) - (metrics.stringWidth(progress) / 2); + int drawY = y + (config.xpOrbSize() / 2) + (metrics.getHeight() / 2); + + OverlayUtil.renderTextLocation(graphics, new Point(drawX, drawY), progress, Color.WHITE); + } + + private void drawProgressArc(Graphics2D graphics, int x, int y, int w, int h, double radiusStart, double radiusEnd, int strokeWidth, Color color) + { + Stroke stroke = graphics.getStroke(); + graphics.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + graphics.setColor(color); + graphics.draw(new Arc2D.Double( + x, y, + w, h, + radiusStart, radiusEnd, + Arc2D.OPEN)); + graphics.setStroke(stroke); + } + + private Ellipse2D drawEllipse(Graphics2D graphics, int x, int y) + { + graphics.setColor(config.progressOrbBackgroundColor()); + Ellipse2D ellipse = new Ellipse2D.Double(x, y, config.xpOrbSize(), config.xpOrbSize()); + graphics.fill(ellipse); + graphics.draw(ellipse); + return ellipse; + } + + private void drawSkillImage(Graphics2D graphics, XpGlobe xpGlobe, int x, int y) + { + BufferedImage skillImage = iconManager.getSkillImage(xpGlobe.getSkill()); + + if (skillImage == null) + { + return; + } + + graphics.drawImage( + skillImage, + x + (config.xpOrbSize() / 2) - (skillImage.getWidth() / 2), + y + (config.xpOrbSize() / 2) - (skillImage.getHeight() / 2), + null + ); + } + + private void drawTooltip(XpGlobe mouseOverSkill, int goalXp) + { + // reset the timer on XpGlobe to prevent it from disappearing while hovered over it + mouseOverSkill.setTime(Instant.now()); + + String skillName = mouseOverSkill.getSkill().getName(); + String skillLevel = Integer.toString(mouseOverSkill.getCurrentLevel()); + + DecimalFormat decimalFormat = new DecimalFormat("###,###,###"); + String skillCurrentXp = decimalFormat.format(mouseOverSkill.getCurrentXp()); + + final PanelComponent xpTooltip = (PanelComponent) this.xpTooltip.getComponent(); + xpTooltip.getChildren().clear(); + + xpTooltip.getChildren().add(LineComponent.builder() + .left(skillName) + .right(skillLevel) + .build()); + + xpTooltip.getChildren().add(LineComponent.builder() + .left("Current XP:") + .leftColor(Color.ORANGE) + .right(skillCurrentXp) + .build()); + + if (goalXp > mouseOverSkill.getCurrentXp()) + { + XpActionType xpActionType = xpTrackerService.getActionType(mouseOverSkill.getSkill()); + + if (config.showActionsLeft()) + { + int actionsLeft = xpTrackerService.getActionsLeft(mouseOverSkill.getSkill()); + if (actionsLeft != Integer.MAX_VALUE) + { + String actionsLeftString = decimalFormat.format(actionsLeft); + xpTooltip.getChildren().add(LineComponent.builder() + .left(xpActionType.getLabel() + " left:") + .leftColor(Color.ORANGE) + .right(actionsLeftString) + .build()); + } + } + + if (config.showXpLeft()) + { + int xpLeft = goalXp - mouseOverSkill.getCurrentXp(); + String skillXpToLvl = decimalFormat.format(xpLeft); + xpTooltip.getChildren().add(LineComponent.builder() + .left("XP left:") + .leftColor(Color.ORANGE) + .right(skillXpToLvl) + .build()); + } + + if (config.showXpHour()) + { + int xpHr = xpTrackerService.getXpHr(mouseOverSkill.getSkill()); + if (xpHr != 0) + { + String xpHrString = decimalFormat.format(xpHr); + xpTooltip.getChildren().add(LineComponent.builder() + .left("XP per hour:") + .leftColor(Color.ORANGE) + .right(xpHrString) + .build()); + } + } + } + + tooltipManager.add(this.xpTooltip); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesPlugin.java new file mode 100644 index 0000000000..46b514151e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xpglobes/XpGlobesPlugin.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2017, Steve + * 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.xpglobes; + +import com.google.inject.Provides; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import javax.inject.Inject; +import lombok.Getter; +import net.runelite.api.Experience; +import net.runelite.api.MenuAction; +import net.runelite.api.Skill; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.StatChanged; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.OverlayMenuClicked; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDependency; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.xptracker.XpTrackerPlugin; +import net.runelite.client.task.Schedule; +import net.runelite.client.ui.overlay.OverlayManager; + +@PluginDescriptor( + name = "XP Globes", + description = "Show XP globes for the respective skill when gaining XP", + tags = {"experience", "levels", "overlay"}, + enabledByDefault = false +) +@PluginDependency(XpTrackerPlugin.class) +public class XpGlobesPlugin extends Plugin +{ + private static final int MAXIMUM_SHOWN_GLOBES = 5; + + private XpGlobe[] globeCache = new XpGlobe[Skill.values().length - 1]; //overall does not trigger xp change event + + @Getter + private final List xpGlobes = new ArrayList<>(); + + @Inject + private XpGlobesConfig config; + + @Inject + private OverlayManager overlayManager; + + @Inject + private XpGlobesOverlay overlay; + + @Provides + XpGlobesConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(XpGlobesConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(overlay); + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.remove(overlay); + } + + @Subscribe + public void onStatChanged(StatChanged statChanged) + { + Skill skill = statChanged.getSkill(); + int currentXp = statChanged.getXp(); + int currentLevel = statChanged.getLevel(); + int skillIdx = skill.ordinal(); + XpGlobe cachedGlobe = globeCache[skillIdx]; + + // StatChanged event occurs when stats drain/boost; check we have an change to actual xp + if (cachedGlobe != null && (cachedGlobe.getCurrentXp() >= currentXp)) + { + return; + } + + if (config.hideMaxed() && currentLevel >= Experience.MAX_REAL_LEVEL) + { + return; + } + + if (cachedGlobe != null) + { + cachedGlobe.setSkill(skill); + cachedGlobe.setCurrentXp(currentXp); + cachedGlobe.setCurrentLevel(currentLevel); + cachedGlobe.setTime(Instant.now()); + addXpGlobe(cachedGlobe); + } + else + { + // dont draw non cached globes, this is triggered on login to setup all of the initial values + globeCache[skillIdx] = new XpGlobe(skill, currentXp, currentLevel, Instant.now()); + } + } + + private void addXpGlobe(XpGlobe xpGlobe) + { + // insert the globe, ordered by skill, if it isn't already in the list to be drawn + int idx = Collections.binarySearch(xpGlobes, xpGlobe, Comparator.comparing(XpGlobe::getSkill)); + if (idx < 0) + { + xpGlobes.add(-idx - 1, xpGlobe); + + // remove the oldest globe if there are too many + if (xpGlobes.size() > MAXIMUM_SHOWN_GLOBES) + { + xpGlobes.stream() + .min(Comparator.comparing(XpGlobe::getTime)) + .ifPresent(xpGlobes::remove); + } + } + } + + @Schedule( + period = 1, + unit = ChronoUnit.SECONDS + ) + public void removeExpiredXpGlobes() + { + if (!xpGlobes.isEmpty()) + { + Instant expireTime = Instant.now() + .minusSeconds(config.xpOrbDuration()); + xpGlobes.removeIf(globe -> globe.getTime().isBefore(expireTime)); + } + } + + private void resetGlobeState() + { + xpGlobes.clear(); + globeCache = new XpGlobe[Skill.values().length - 1]; + } + + @Subscribe + public void onOverlayMenuClicked(final OverlayMenuClicked event) + { + if (!(event.getEntry().getMenuAction() == MenuAction.RUNELITE_OVERLAY + && event.getOverlay() == overlay)) + { + return; + } + + if (event.getEntry().getOption().equals(XpGlobesOverlay.FLIP_ACTION)) + { + config.setAlignOrbsVertically(!config.alignOrbsVertically()); + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case HOPPING: + case LOGGING_IN: + resetGlobeState(); + break; + } + } + +} 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..50acc52c02 --- /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 +public enum XpActionType +{ + EXPERIENCE("Actions"), + ACTOR_HEALTH("Kills"); + + private final String label; +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpGoalTimeType.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpGoalTimeType.java new file mode 100644 index 0000000000..b340d9b6b4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpGoalTimeType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020, Dasgust + * 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; + +public enum XpGoalTimeType +{ + DAYS, + HOURS, + SHORT +} 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 new file mode 100644 index 0000000000..343765bb79 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2018, Adam + * Copyright (c) 2018, Psikoi + * Copyright (c) 2020, Anthony + * 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 java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.Experience; +import net.runelite.api.Skill; +import net.runelite.api.WorldType; +import net.runelite.client.game.SkillIconManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.SkillColor; +import net.runelite.client.ui.components.MouseDragEventForwarder; +import net.runelite.client.ui.components.ProgressBar; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.LinkBrowser; +import net.runelite.client.util.QuantityFormatter; + +class XpInfoBox extends JPanel +{ + static final DecimalFormat TWO_DECIMAL_FORMAT = new DecimalFormat("0.00"); + + static + { + TWO_DECIMAL_FORMAT.setRoundingMode(RoundingMode.DOWN); + } + + // Templates + private static final String HTML_TOOL_TIP_TEMPLATE = + "%s %s done
" + + "%s %s/hr
" + + "%s %s"; + private static final String HTML_LABEL_TEMPLATE = + "%s%s"; + + private static final String REMOVE_STATE = "Remove from canvas"; + private static final String ADD_STATE = "Add to canvas"; + + // Instance members + private final JComponent panel; + + @Getter(AccessLevel.PACKAGE) + private final Skill skill; + + /* The tracker's wrapping container */ + private final JPanel container = new JPanel(); + + /* Contains the skill icon and the stats panel */ + private final JPanel headerPanel = new JPanel(); + + /* Contains all the skill information (exp gained, per hour, etc) */ + private final JPanel statsPanel = new JPanel(); + + private final ProgressBar progressBar = new ProgressBar(); + + private final JLabel topLeftStat = new JLabel(); + private final JLabel bottomLeftStat = new JLabel(); + private final JLabel topRightStat = new JLabel(); + private final JLabel bottomRightStat = new JLabel(); + private final JMenuItem pauseSkill = new JMenuItem("Pause"); + private final JMenuItem canvasItem = new JMenuItem(ADD_STATE); + + private final XpTrackerConfig xpTrackerConfig; + + private boolean paused = false; + + XpInfoBox(XpTrackerPlugin xpTrackerPlugin, XpTrackerConfig xpTrackerConfig, Client client, JComponent panel, Skill skill, SkillIconManager iconManager) + { + this.xpTrackerConfig = xpTrackerConfig; + this.panel = panel; + this.skill = skill; + + setLayout(new BorderLayout()); + setBorder(new EmptyBorder(5, 0, 0, 0)); + + container.setLayout(new BorderLayout()); + container.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + // Create open xp tracker menu + final JMenuItem openXpTracker = new JMenuItem("Open Wise Old Man"); + openXpTracker.addActionListener(e -> LinkBrowser.browse(XpPanel.buildXpTrackerUrl( + client.getLocalPlayer(), skill, client.getWorldType().contains(WorldType.LEAGUE)))); + + // Create reset menu + final JMenuItem reset = new JMenuItem("Reset"); + reset.addActionListener(e -> xpTrackerPlugin.resetSkillState(skill)); + + // Create reset others menu + final JMenuItem resetOthers = new JMenuItem("Reset others"); + resetOthers.addActionListener(e -> xpTrackerPlugin.resetOtherSkillState(skill)); + + // Create reset others menu + pauseSkill.addActionListener(e -> xpTrackerPlugin.pauseSkill(skill, !paused)); + + // Create popup menu + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(openXpTracker); + popupMenu.add(reset); + popupMenu.add(resetOthers); + popupMenu.add(pauseSkill); + popupMenu.add(canvasItem); + popupMenu.addPopupMenuListener(new PopupMenuListener() + { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent popupMenuEvent) + { + canvasItem.setText(xpTrackerPlugin.hasOverlay(skill) ? REMOVE_STATE : ADD_STATE); + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent popupMenuEvent) + { + } + + @Override + public void popupMenuCanceled(PopupMenuEvent popupMenuEvent) + { + } + }); + + canvasItem.addActionListener(e -> + { + if (canvasItem.getText().equals(REMOVE_STATE)) + { + xpTrackerPlugin.removeOverlay(skill); + } + else + { + xpTrackerPlugin.addOverlay(skill); + } + }); + + JLabel skillIcon = new JLabel(new ImageIcon(iconManager.getSkillImage(skill))); + skillIcon.setHorizontalAlignment(SwingConstants.CENTER); + skillIcon.setVerticalAlignment(SwingConstants.CENTER); + skillIcon.setPreferredSize(new Dimension(35, 35)); + + headerPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + headerPanel.setLayout(new BorderLayout()); + + statsPanel.setLayout(new DynamicGridLayout(2, 2)); + statsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + statsPanel.setBorder(new EmptyBorder(9, 2, 9, 2)); + + + topLeftStat.setFont(FontManager.getRunescapeSmallFont()); + bottomLeftStat.setFont(FontManager.getRunescapeSmallFont()); + topRightStat.setFont(FontManager.getRunescapeSmallFont()); + bottomRightStat.setFont(FontManager.getRunescapeSmallFont()); + + statsPanel.add(topLeftStat); // top left + statsPanel.add(topRightStat); // top right + statsPanel.add(bottomLeftStat); // bottom left + statsPanel.add(bottomRightStat); // bottom right + + headerPanel.add(skillIcon, BorderLayout.WEST); + headerPanel.add(statsPanel, BorderLayout.CENTER); + + JPanel progressWrapper = new JPanel(); + progressWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR); + progressWrapper.setLayout(new BorderLayout()); + progressWrapper.setBorder(new EmptyBorder(0, 7, 7, 7)); + + progressBar.setMaximumValue(100); + progressBar.setBackground(new Color(61, 56, 49)); + progressBar.setForeground(SkillColor.find(skill).getColor()); + progressBar.setDimmedText("Paused"); + + progressWrapper.add(progressBar, BorderLayout.NORTH); + + container.add(headerPanel, BorderLayout.NORTH); + container.add(progressWrapper, BorderLayout.SOUTH); + + container.setComponentPopupMenu(popupMenu); + progressBar.setComponentPopupMenu(popupMenu); + + // forward mouse drag events to parent panel for drag and drop reordering + MouseDragEventForwarder mouseDragEventForwarder = new MouseDragEventForwarder(panel); + container.addMouseListener(mouseDragEventForwarder); + container.addMouseMotionListener(mouseDragEventForwarder); + progressBar.addMouseListener(mouseDragEventForwarder); + progressBar.addMouseMotionListener(mouseDragEventForwarder); + + add(container, BorderLayout.NORTH); + } + + void reset() + { + canvasItem.setText(ADD_STATE); + panel.remove(this); + panel.revalidate(); + } + + void update(boolean updated, boolean paused, XpSnapshotSingle xpSnapshotSingle) + { + SwingUtilities.invokeLater(() -> rebuildAsync(updated, paused, xpSnapshotSingle)); + } + + private void rebuildAsync(boolean updated, boolean skillPaused, XpSnapshotSingle xpSnapshotSingle) + { + if (updated) + { + if (getParent() != panel) + { + panel.add(this); + panel.revalidate(); + } + + if (xpTrackerConfig.prioritizeRecentXpSkills()) + { + panel.setComponentZOrder(this, 0); + } + + paused = skillPaused; + + // Update progress bar + progressBar.setValue((int) xpSnapshotSingle.getSkillProgressToGoal()); + progressBar.setCenterLabel(xpTrackerConfig.progressBarLabel().getValueFunc().apply(xpSnapshotSingle)); + progressBar.setLeftLabel("Lvl. " + xpSnapshotSingle.getStartLevel()); + progressBar.setRightLabel(xpSnapshotSingle.getEndGoalXp() == Experience.MAX_SKILL_XP + ? "200M" + : "Lvl. " + xpSnapshotSingle.getEndLevel()); + + // Add intermediate level positions to progressBar + if (xpTrackerConfig.showIntermediateLevels() && xpSnapshotSingle.getEndLevel() - xpSnapshotSingle.getStartLevel() > 1) + { + final List positions = new ArrayList<>(); + + for (int level = xpSnapshotSingle.getStartLevel() + 1; level < xpSnapshotSingle.getEndLevel(); level++) + { + double relativeStartExperience = Experience.getXpForLevel(level) - xpSnapshotSingle.getStartGoalXp(); + double relativeEndExperience = xpSnapshotSingle.getEndGoalXp() - xpSnapshotSingle.getStartGoalXp(); + positions.add((int) (relativeStartExperience / relativeEndExperience * 100)); + } + + progressBar.setPositions(positions); + } + else + { + progressBar.setPositions(Collections.emptyList()); + } + + XpProgressBarLabel tooltipLabel = xpTrackerConfig.progressBarTooltipLabel(); + + progressBar.setToolTipText(String.format( + HTML_TOOL_TIP_TEMPLATE, + xpSnapshotSingle.getActionsInSession(), + xpSnapshotSingle.getActionType().getLabel(), + xpSnapshotSingle.getActionsPerHour(), + xpSnapshotSingle.getActionType().getLabel(), + tooltipLabel.getValueFunc().apply(xpSnapshotSingle), + tooltipLabel == XpProgressBarLabel.PERCENTAGE ? "of goal" : "till goal lvl")); + + progressBar.setDimmed(skillPaused); + + progressBar.repaint(); + } + else if (!paused && skillPaused) + { + // React to the skill state now being paused + progressBar.setDimmed(true); + progressBar.repaint(); + paused = true; + pauseSkill.setText("Unpause"); + } + else if (paused && !skillPaused) + { + // React to the skill being unpaused (without update) + progressBar.setDimmed(false); + progressBar.repaint(); + paused = false; + pauseSkill.setText("Pause"); + } + + // Update information labels + // Update exp per hour separately, every time (not only when there's an update) + topLeftStat.setText(htmlLabel(xpTrackerConfig.xpPanelLabel1(), xpSnapshotSingle)); + topRightStat.setText(htmlLabel(xpTrackerConfig.xpPanelLabel2(), xpSnapshotSingle)); + bottomLeftStat.setText(htmlLabel(xpTrackerConfig.xpPanelLabel3(), xpSnapshotSingle)); + bottomRightStat.setText(htmlLabel(xpTrackerConfig.xpPanelLabel4(), xpSnapshotSingle)); + } + + static String htmlLabel(XpPanelLabel panelLabel, XpSnapshotSingle xpSnapshotSingle) + { + String key = panelLabel.getActionKey(xpSnapshotSingle) + ": "; + String value = panelLabel.getValueFunc().apply(xpSnapshotSingle); + return htmlLabel(key, value); + } + + static String htmlLabel(String key, int value) + { + String valueStr = QuantityFormatter.quantityToRSDecimalStack(value, true); + return htmlLabel(key, valueStr); + } + + static String htmlLabel(String key, String valueStr) + { + return String.format(HTML_LABEL_TEMPLATE, ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), key, valueStr); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java new file mode 100644 index 0000000000..95f9beefbd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBoxOverlay.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2018, Jasper Ketelaar + * Copyright (c) 2020, Anthony + * 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 java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import lombok.AccessLevel; +import lombok.Getter; +import net.runelite.api.Experience; +import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG; +import net.runelite.api.Skill; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.SkillColor; +import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE; +import net.runelite.client.ui.overlay.OverlayMenuEntry; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.components.ComponentOrientation; +import net.runelite.client.ui.overlay.components.ImageComponent; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.ui.overlay.components.ProgressBarComponent; +import net.runelite.client.ui.overlay.components.SplitComponent; + +class XpInfoBoxOverlay extends OverlayPanel +{ + private static final int BORDER_SIZE = 2; + private static final int XP_AND_PROGRESS_BAR_GAP = 2; + private static final int XP_AND_ICON_GAP = 4; + private static final Rectangle XP_AND_ICON_COMPONENT_BORDER = new Rectangle(2, 1, 4, 0); + + private final PanelComponent iconXpSplitPanel = new PanelComponent(); + private final XpTrackerPlugin plugin; + private final XpTrackerConfig config; + + @Getter(AccessLevel.PACKAGE) + private final Skill skill; + private final BufferedImage icon; + + XpInfoBoxOverlay( + XpTrackerPlugin plugin, + XpTrackerConfig config, + Skill skill, + BufferedImage icon) + { + super(plugin); + this.plugin = plugin; + this.config = config; + this.skill = skill; + this.icon = icon; + panelComponent.setBorder(new Rectangle(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE)); + panelComponent.setGap(new Point(0, XP_AND_PROGRESS_BAR_GAP)); + iconXpSplitPanel.setBorder(XP_AND_ICON_COMPONENT_BORDER); + iconXpSplitPanel.setBackgroundColor(null); + getMenuEntries().add(new OverlayMenuEntry(RUNELITE_OVERLAY_CONFIG, OPTION_CONFIGURE, "XP Tracker overlay")); + } + + @Override + public Dimension render(Graphics2D graphics) + { + iconXpSplitPanel.getChildren().clear(); + + //Setting the font to rs small font so that the overlay isn't huge + graphics.setFont(FontManager.getRunescapeSmallFont()); + + final XpSnapshotSingle snapshot = plugin.getSkillSnapshot(skill); + + final String leftStr = config.onScreenDisplayMode().getActionKey(snapshot); + final String rightNum = config.onScreenDisplayMode().getValueFunc().apply(snapshot); + + final LineComponent xpLine = LineComponent.builder() + .left(leftStr + ":") + .right(rightNum) + .build(); + + final String bottomLeftStr = config.onScreenDisplayModeBottom().getActionKey(snapshot); + final String bottomRightNum = config.onScreenDisplayModeBottom().getValueFunc().apply(snapshot); + + final LineComponent xpLineBottom = LineComponent.builder() + .left(bottomLeftStr + ":") + .right(bottomRightNum) + .build(); + + final SplitComponent xpSplit = SplitComponent.builder() + .first(xpLine) + .second(xpLineBottom) + .orientation(ComponentOrientation.VERTICAL) + .build(); + + final ImageComponent imageComponent = new ImageComponent(icon); + final SplitComponent iconXpSplit = SplitComponent.builder() + .first(imageComponent) + .second(xpSplit) + .orientation(ComponentOrientation.HORIZONTAL) + .gap(new Point(XP_AND_ICON_GAP, 0)) + .build(); + + iconXpSplitPanel.getChildren().add(iconXpSplit); + + final ProgressBarComponent progressBarComponent = new ProgressBarComponent(); + + progressBarComponent.setBackgroundColor(new Color(61, 56, 49)); + progressBarComponent.setForegroundColor(SkillColor.find(skill).getColor()); + + progressBarComponent.setLeftLabel(String.valueOf(snapshot.getStartLevel())); + progressBarComponent.setRightLabel(snapshot.getEndGoalXp() == Experience.MAX_SKILL_XP + ? "200M" + : String.valueOf(snapshot.getEndLevel())); + + progressBarComponent.setValue(snapshot.getSkillProgressToGoal()); + + panelComponent.getChildren().add(iconXpSplitPanel); + panelComponent.getChildren().add(progressBarComponent); + + return super.render(graphics); + } + + @Override + public String getName() + { + return super.getName() + skill.getName(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java new file mode 100644 index 0000000000..aa7af98aed --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2017, Cameron + * Copyright (c) 2018, Psikoi + * 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 java.awt.BorderLayout; +import java.awt.GridLayout; +import java.util.HashMap; +import java.util.Map; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.api.WorldType; +import net.runelite.client.game.SkillIconManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.DragAndDropReorderPane; +import net.runelite.client.ui.components.PluginErrorPanel; +import net.runelite.client.util.LinkBrowser; +import okhttp3.HttpUrl; + +class XpPanel extends PluginPanel +{ + private final Map infoBoxes = new HashMap<>(); + + private final JLabel overallExpGained = new JLabel(XpInfoBox.htmlLabel("Gained: ", 0)); + private final JLabel overallExpHour = new JLabel(XpInfoBox.htmlLabel("Per hour: ", 0)); + + private final JPanel overallPanel = new JPanel(); + + /* This displays the "No exp gained" text */ + private final PluginErrorPanel errorPanel = new PluginErrorPanel(); + + XpPanel(XpTrackerPlugin xpTrackerPlugin, XpTrackerConfig xpTrackerConfig, Client client, SkillIconManager iconManager) + { + super(); + + setBorder(new EmptyBorder(6, 6, 6, 6)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + setLayout(new BorderLayout()); + + final JPanel layoutPanel = new JPanel(); + BoxLayout boxLayout = new BoxLayout(layoutPanel, BoxLayout.Y_AXIS); + layoutPanel.setLayout(boxLayout); + add(layoutPanel, BorderLayout.NORTH); + + overallPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + overallPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + overallPanel.setLayout(new BorderLayout()); + overallPanel.setVisible(false); // this will only become visible when the player gets exp + + // Create open xp tracker menu + final JMenuItem openXpTracker = new JMenuItem("Open Wise Old Man"); + openXpTracker.addActionListener(e -> LinkBrowser.browse(XpPanel.buildXpTrackerUrl( + client.getLocalPlayer(), Skill.OVERALL, client.getWorldType().contains(WorldType.LEAGUE)))); + + // Create reset all menu + final JMenuItem reset = new JMenuItem("Reset All"); + reset.addActionListener(e -> xpTrackerPlugin.resetAndInitState()); + + // Create pause all menu + final JMenuItem pauseAll = new JMenuItem("Pause All"); + pauseAll.addActionListener(e -> xpTrackerPlugin.pauseAllSkills(true)); + + // Create unpause all menu + final JMenuItem unpauseAll = new JMenuItem("Unpause All"); + unpauseAll.addActionListener(e -> xpTrackerPlugin.pauseAllSkills(false)); + + + // Create popup menu + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(openXpTracker); + popupMenu.add(reset); + popupMenu.add(pauseAll); + popupMenu.add(unpauseAll); + overallPanel.setComponentPopupMenu(popupMenu); + + final JLabel overallIcon = new JLabel(new ImageIcon(iconManager.getSkillImage(Skill.OVERALL))); + + final JPanel overallInfo = new JPanel(); + overallInfo.setBackground(ColorScheme.DARKER_GRAY_COLOR); + overallInfo.setLayout(new GridLayout(2, 1)); + overallInfo.setBorder(new EmptyBorder(0, 10, 0, 0)); + + overallExpGained.setFont(FontManager.getRunescapeSmallFont()); + overallExpHour.setFont(FontManager.getRunescapeSmallFont()); + + overallInfo.add(overallExpGained); + overallInfo.add(overallExpHour); + + overallPanel.add(overallIcon, BorderLayout.WEST); + overallPanel.add(overallInfo, BorderLayout.CENTER); + + final JComponent infoBoxPanel = new DragAndDropReorderPane(); + + layoutPanel.add(overallPanel); + layoutPanel.add(infoBoxPanel); + + for (Skill skill : Skill.values()) + { + if (skill == Skill.OVERALL) + { + break; + } + infoBoxes.put(skill, new XpInfoBox(xpTrackerPlugin, xpTrackerConfig, client, infoBoxPanel, skill, iconManager)); + } + + errorPanel.setContent("Exp trackers", "You have not gained experience yet."); + add(errorPanel); + } + + static String buildXpTrackerUrl(final Actor player, final Skill skill, boolean leagueWorld) + { + if (player == null) + { + return ""; + } + + final String host = leagueWorld ? "trailblazer.wiseoldman.net" : "wiseoldman.net"; + + return new HttpUrl.Builder() + .scheme("https") + .host(host) + .addPathSegment("players") + .addPathSegment(player.getName()) + .addPathSegment("gained") + .addPathSegment("skilling") + .addQueryParameter("metric", skill.getName().toLowerCase()) + .addQueryParameter("period", "week") + .build() + .toString(); + } + + void resetAllInfoBoxes() + { + infoBoxes.forEach((skill, xpInfoBox) -> xpInfoBox.reset()); + } + + void resetSkill(Skill skill) + { + XpInfoBox xpInfoBox = infoBoxes.get(skill); + if (xpInfoBox != null) + { + xpInfoBox.reset(); + } + } + + void updateSkillExperience(boolean updated, boolean paused, Skill skill, XpSnapshotSingle xpSnapshotSingle) + { + final XpInfoBox xpInfoBox = infoBoxes.get(skill); + + if (xpInfoBox != null) + { + xpInfoBox.update(updated, paused, xpSnapshotSingle); + } + } + + void updateTotal(XpSnapshotSingle xpSnapshotTotal) + { + // if player has gained exp and hasn't switched displays yet, hide error panel and show overall info + if (xpSnapshotTotal.getXpGainedInSession() > 0 && !overallPanel.isVisible()) + { + overallPanel.setVisible(true); + remove(errorPanel); + } + else if (xpSnapshotTotal.getXpGainedInSession() == 0 && overallPanel.isVisible()) + { + overallPanel.setVisible(false); + add(errorPanel); + } + + SwingUtilities.invokeLater(() -> rebuildAsync(xpSnapshotTotal)); + } + + private void rebuildAsync(XpSnapshotSingle xpSnapshotTotal) + { + overallExpGained.setText(XpInfoBox.htmlLabel("Gained: ", xpSnapshotTotal.getXpGainedInSession())); + overallExpHour.setText(XpInfoBox.htmlLabel("Per hour: ", xpSnapshotTotal.getXpPerHour())); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanelLabel.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanelLabel.java new file mode 100644 index 0000000000..d17c31872e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanelLabel.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, Anthony + * 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 java.util.function.Function; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.runelite.client.util.QuantityFormatter; + +@Getter +@AllArgsConstructor +public enum XpPanelLabel +{ + TIME_TO_LEVEL("TTL", XpSnapshotSingle::getTimeTillGoalShort), + + XP_GAINED("XP Gained", snap -> format(snap.getXpGainedInSession())), + XP_HOUR("XP/hr", snap -> format(snap.getXpPerHour())), + XP_LEFT("XP Left", snap -> format(snap.getXpRemainingToGoal())), + + ACTIONS_LEFT("Actions", snap -> format(snap.getActionsRemainingToGoal())), + ACTIONS_HOUR("Actions/hr", snap -> format(snap.getActionsPerHour())), + ACTIONS_DONE("Actions Done", snap -> format(snap.getActionsInSession())), + ; + + private final String key; + private final Function valueFunc; + + /** + * Get the action key label based on if the Action type is an xp drop or kill + * + * @param snapshot + * @return + */ + public String getActionKey(XpSnapshotSingle snapshot) + { + String actionKey = key; + if (snapshot.getActionType() == XpActionType.ACTOR_HEALTH) + { + return actionKey.replace("Action", "Kill"); + } + + return actionKey; + } + + private static String format(int val) + { + return QuantityFormatter.quantityToRSDecimalStack(val, true); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java new file mode 100644 index 0000000000..194c890c42 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseState.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018, Levi + * 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 java.util.EnumMap; +import java.util.Map; +import net.runelite.api.Skill; + +class XpPauseState +{ + // Internal state + private final Map skillPauses = new EnumMap<>(Skill.class); + private boolean cachedIsLoggedIn = false; + + boolean pauseSkill(Skill skill) + { + return findPauseState(skill).manualPause(); + } + + boolean unpauseSkill(Skill skill) + { + return findPauseState(skill).unpause(); + } + + boolean isPaused(Skill skill) + { + return findPauseState(skill).isPaused(); + } + + void tickXp(Skill skill, long currentXp, int pauseAfterMinutes) + { + final XpPauseStateSingle state = findPauseState(skill); + + if (state.getXp() != currentXp) + { + state.xpChanged(currentXp); + } + else if (pauseAfterMinutes > 0) + { + final long now = System.currentTimeMillis(); + final int pauseAfterMillis = pauseAfterMinutes * 60 * 1000; + final long lastChangeMillis = state.getLastChangeMillis(); + // When config.pauseSkillAfter is 0, it is effectively disabled + if (lastChangeMillis != 0 && (now - lastChangeMillis) >= pauseAfterMillis) + { + state.timeout(); + } + } + } + + void tickLogout(boolean pauseOnLogout, boolean loggedIn) + { + // Deduplicated login and logout calls + if (!cachedIsLoggedIn && loggedIn) + { + cachedIsLoggedIn = true; + + for (Skill skill : Skill.values()) + { + findPauseState(skill).login(); + } + } + else if (cachedIsLoggedIn && !loggedIn) + { + cachedIsLoggedIn = false; + + // If configured, then let the pause state know to pause with reason: logout + if (pauseOnLogout) + { + for (Skill skill : Skill.values()) + { + findPauseState(skill).logout(); + } + } + } + } + + private XpPauseStateSingle findPauseState(Skill skill) + { + return skillPauses.computeIfAbsent(skill, XpPauseStateSingle::new); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java new file mode 100644 index 0000000000..9e5cc55622 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPauseStateSingle.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018, Levi + * 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 java.util.EnumSet; +import java.util.Set; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Skill; + +@RequiredArgsConstructor +class XpPauseStateSingle +{ + @Getter + private final Skill skill; + private final Set pauseReasons = EnumSet.noneOf(XpPauseReason.class); + @Getter + private long lastChangeMillis; + @Getter + private long xp; + + boolean isPaused() + { + return !pauseReasons.isEmpty(); + } + + boolean login() + { + return pauseReasons.remove(XpPauseReason.PAUSED_LOGOUT); + } + + boolean logout() + { + return pauseReasons.add(XpPauseReason.PAUSED_LOGOUT); + } + + boolean timeout() + { + return pauseReasons.add(XpPauseReason.PAUSED_TIMEOUT); + } + + boolean manualPause() + { + return pauseReasons.add(XpPauseReason.PAUSE_MANUAL); + } + + boolean xpChanged(long xp) + { + this.xp = xp; + this.lastChangeMillis = System.currentTimeMillis(); + return clearAll(); + } + + boolean unpause() + { + this.lastChangeMillis = System.currentTimeMillis(); + return clearAll(); + } + + private boolean clearAll() + { + if (pauseReasons.isEmpty()) + { + return false; + } + + pauseReasons.clear(); + return true; + } + + private enum XpPauseReason + { + PAUSE_MANUAL, + PAUSED_LOGOUT, + PAUSED_TIMEOUT + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpProgressBarLabel.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpProgressBarLabel.java new file mode 100644 index 0000000000..35d19a63db --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpProgressBarLabel.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020, Anthony + * 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; + +import java.util.function.Function; + +import static net.runelite.client.plugins.xptracker.XpInfoBox.TWO_DECIMAL_FORMAT; + +@Getter +@AllArgsConstructor +public enum XpProgressBarLabel +{ + PERCENTAGE((snap) -> TWO_DECIMAL_FORMAT.format(snap.getSkillProgressToGoal()) + "%"), + TIME_TO_LEVEL(XpSnapshotSingle::getTimeTillGoal), + HOURS_TO_LEVEL(XpSnapshotSingle::getTimeTillGoalHours) + ; + + private final Function valueFunc; +} 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 new file mode 100644 index 0000000000..912b5b6b12 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpSnapshotSingle.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, Levi + * 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.Builder; +import lombok.Value; + +@Builder +@Value +class XpSnapshotSingle +{ + private XpActionType actionType; + private int startLevel; + private int endLevel; + private int startGoalXp; + private int endGoalXp; + private int xpGainedInSession; + private int xpRemainingToGoal; + private int xpPerHour; + private double skillProgressToGoal; + private int actionsInSession; + private int actionsRemainingToGoal; + private int actionsPerHour; + private String timeTillGoal; + private String timeTillGoalHours; + private String timeTillGoalShort; +} 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 new file mode 100644 index 0000000000..fac3c16410 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpState.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2018, Levi + * 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 java.util.EnumMap; +import java.util.Map; +import lombok.NonNull; +import net.runelite.api.NPC; +import net.runelite.api.Skill; + +/** + * Internal state for the XpTrackerPlugin + * + * Note: This class's operations are not currently synchronized. + * It is intended to be called by the XpTrackerPlugin on the client thread. + */ +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 Map xpSkills = new EnumMap<>(Skill.class); + private NPC interactedNPC; + + /** + * Destroys all internal state, however any XpSnapshotSingle or XpSnapshotTotal remain unaffected. + */ + void reset() + { + xpSkills.clear(); + } + + /** + * Resets a single skill + * @param skill Skill to reset + * @param currentXp Current XP to set to, if unknown set to -1 + */ + void resetSkill(Skill skill, long currentXp) + { + xpSkills.remove(skill); + xpSkills.put(skill, new XpStateSingle(skill, currentXp)); + } + + /** + * Updates a skill with the current known XP. + * When the result of this operation is XpUpdateResult.UPDATED, the UI should be updated accordingly. + * This is to distinguish events that reload all the skill's current values (such as world hopping) + * and also first-login when the skills are not initialized (the start XP will be -1 in this case). + * @param skill Skill to update + * @param currentXp Current known XP for this skill + * @param goalStartXp Possible XP start goal + * @param goalEndXp Possible XP end goal + * @return Whether or not the skill has been initialized, there was no change, or it has been updated + */ + XpUpdateResult updateSkill(Skill skill, long currentXp, int goalStartXp, int goalEndXp) + { + XpStateSingle state = getSkill(skill); + + if (state.getStartXp() == -1) + { + if (currentXp >= 0) + { + initializeSkill(skill, currentXp); + return XpUpdateResult.INITIALIZED; + } + else + { + return XpUpdateResult.NO_CHANGE; + } + } + else + { + long startXp = state.getStartXp(); + int gainedXp = state.getXpGained(); + + if (startXp + gainedXp > currentXp) + { + // Reinitialize with lesser currentXp, this can happen with negative xp lamps + initializeSkill(skill, currentXp); + return XpUpdateResult.INITIALIZED; + } + else + { + return state.update(currentXp, goalStartXp, goalEndXp) ? XpUpdateResult.UPDATED : XpUpdateResult.NO_CHANGE; + } + } + } + + 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, int xpModifier) + { + if (npc == null || npc.getCombatLevel() <= 0 || npcHealth == null) + { + return; + } + + final XpStateSingle state = getSkill(skill); + final int actionExp = (int) (npcHealth * getCombatXPModifier(skill) * xpModifier); + 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.getXpGained() <= 0 || 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); + } + + /** + * Forcefully initialize a skill with a known start XP from the current XP. + * This is used in resetAndInitState by the plugin. It should not result in showing the XP in the UI. + * @param skill Skill to initialize + * @param currentXp Current known XP for the skill + */ + void initializeSkill(Skill skill, long currentXp) + { + xpSkills.put(skill, new XpStateSingle(skill, currentXp)); + } + + boolean isInitialized(Skill skill) + { + XpStateSingle xpStateSingle = xpSkills.get(skill); + return xpStateSingle != null && xpStateSingle.getStartXp() != -1; + } + + @NonNull + XpStateSingle getSkill(Skill skill) + { + return xpSkills.computeIfAbsent(skill, (s) -> new XpStateSingle(s, -1)); + } + + /** + * Obtain an immutable snapshot of the provided skill + * intended for use with the UI which operates on another thread + * @param skill Skill to obtain the snapshot for + * @return An immutable snapshot of the specified skill for this session since first login or last reset + */ + @NonNull + XpSnapshotSingle getSkillSnapshot(Skill skill) + { + return getSkill(skill).snapshot(); + } + + /** + * Obtain an immutable snapshot of the provided skill + * intended for use with the UI which operates on another thread + * @return An immutable snapshot of total information for this session since first login or last reset + */ + @NonNull + XpSnapshotSingle getTotalSnapshot() + { + return getSkill(Skill.OVERALL).snapshot(); + } +} 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 new file mode 100644 index 0000000000..e10684e322 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpStateSingle.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2017, Cameron + * Copyright (c) 2018, Levi + * Copyright (c) 2020, Anthony + * 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 java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Experience; +import net.runelite.api.Skill; + +@Slf4j +class XpStateSingle +{ + private final Skill skill; + private final Map actions = new HashMap<>(); + + @Getter + @Setter + private long startXp; + + @Getter + private int xpGained = 0; + + @Setter + private XpActionType actionType = XpActionType.EXPERIENCE; + + private long skillTime = 0; + private int startLevelExp = 0; + private int endLevelExp = 0; + + XpStateSingle(Skill skill, long startXp) + { + this.skill = skill; + this.startXp = startXp; + } + + XpAction getXpAction(final XpActionType type) + { + actions.putIfAbsent(type, new XpAction()); + return actions.get(type); + } + + long getCurrentXp() + { + return startXp + xpGained; + } + + private int getActionsHr() + { + return toHourly(getXpAction(actionType).getActions()); + } + + private int toHourly(int value) + { + return (int) ((1.0 / (getTimeElapsedInSeconds() / 3600.0)) * value); + } + + private long getTimeElapsedInSeconds() + { + // If the skill started just now, we can divide by near zero, this results in odd behavior. + // To prevent that, pretend the skill has been active for a minute (60 seconds) + // This will create a lower estimate for the first minute, + // but it isn't ridiculous like saying 2 billion XP per hour. + return Math.max(60, skillTime / 1000); + } + + private int getXpRemaining() + { + return endLevelExp - (int) getCurrentXp(); + } + + private int getActionsRemaining() + { + final XpAction action = getXpAction(actionType); + + if (action.isActionsHistoryInitialized()) + { + long xpRemaining = getXpRemaining() * action.getActionExps().length; + long totalActionXp = 0; + + for (int actionXp : action.getActionExps()) + { + totalActionXp += actionXp; + } + + // Let's not divide by zero (or negative) + if (totalActionXp > 0) + { + // Make sure to account for the very last action at the end + long remainder = xpRemaining % totalActionXp; + long quotient = xpRemaining / totalActionXp; + return Math.toIntExact(quotient + (remainder > 0 ? 1 : 0)); + } + } + + return Integer.MAX_VALUE; + } + + private double getSkillProgress() + { + double xpGained = getCurrentXp() - startLevelExp; + double xpGoal = endLevelExp - startLevelExp; + return (xpGained / xpGoal) * 100; + } + + private long getSecondsTillLevel() + { + // Java 8 doesn't have good duration / period objects to represent spans of time that can be formatted + // Rather than importing another dependency like joda time (which is practically built into java 10) + // below will be a custom formatter that handles spans larger than 1 day + long seconds = getTimeElapsedInSeconds(); + + if (seconds <= 0 || xpGained <= 0) + { + return -1; + } + + // formula is xpRemaining / xpPerSecond + // xpPerSecond being xpGained / seconds + // This can be simplified so division is only done once and we can work in whole numbers! + return (getXpRemaining() * seconds) / xpGained; + } + + private String getTimeTillLevel(XpGoalTimeType goalTimeType) + { + long remainingSeconds = getSecondsTillLevel(); + if (remainingSeconds < 0) + { + return "\u221e"; + } + + long durationDays = remainingSeconds / (24 * 60 * 60); + long durationHours = (remainingSeconds % (24 * 60 * 60)) / (60 * 60); + long durationHoursTotal = remainingSeconds / (60 * 60); + long durationMinutes = (remainingSeconds % (60 * 60)) / 60; + long durationSeconds = remainingSeconds % 60; + + switch (goalTimeType) + { + case DAYS: + if (durationDays > 1) + { + return String.format("%d days %02d:%02d:%02d", durationDays, durationHours, durationMinutes, durationSeconds); + } + else if (durationDays == 1) + { + return String.format("1 day %02d:%02d:%02d", durationHours, durationMinutes, durationSeconds); + } + case HOURS: + if (durationHoursTotal > 1) + { + return String.format("%d hours %02d:%02d", durationHoursTotal, durationMinutes, durationSeconds); + } + else if (durationHoursTotal == 1) + { + return String.format("1 hour %02d:%02d", durationMinutes, durationSeconds); + } + case SHORT: + default: + // durationDays = 0 or durationHoursTotal = 0 or goalTimeType = SHORT if we got here. + // return time remaining in hh:mm:ss or mm:ss format where hh can be > 24 + if (durationHoursTotal > 0) + { + return String.format("%02d:%02d:%02d", durationHoursTotal, durationMinutes, durationSeconds); + } + + // Minutes and seconds will always be present + return String.format("%02d:%02d", durationMinutes, durationSeconds); + } + } + + int getXpHr() + { + return toHourly(xpGained); + } + + boolean update(long currentXp, int goalStartXp, int goalEndXp) + { + if (startXp == -1) + { + log.warn("Attempted to update skill state " + skill + " but was not initialized with current xp"); + return false; + } + + long originalXp = xpGained + startXp; + int actionExp = (int) (currentXp - originalXp); + + // No experience gained + if (actionExp == 0) + { + return false; + } + + // Update EXPERIENCE action + final XpAction action = getXpAction(XpActionType.EXPERIENCE); + + if (action.isActionsHistoryInitialized()) + { + action.getActionExps()[action.getActionExpIndex()] = actionExp; + } + else + { + // populate all values in our action history array with this first value that we see + // so the average value of our action history starts out as this first value we see + for (int i = 0; i < action.getActionExps().length; i++) + { + action.getActionExps()[i] = actionExp; + } + + action.setActionsHistoryInitialized(true); + } + + action.setActionExpIndex((action.getActionExpIndex() + 1) % action.getActionExps().length); + action.setActions(action.getActions() + 1); + + // Calculate experience gained + xpGained = (int) (currentXp - startXp); + + // Determine XP goals, overall has no goals + if (skill != Skill.OVERALL) + { + if (goalStartXp < 0 || currentXp > goalEndXp) + { + startLevelExp = Experience.getXpForLevel(Experience.getLevelForXp((int) currentXp)); + } + else + { + startLevelExp = goalStartXp; + } + + if (goalEndXp <= 0 || currentXp > goalEndXp) + { + int currentLevel = Experience.getLevelForXp((int) currentXp); + endLevelExp = currentLevel + 1 <= Experience.MAX_VIRT_LEVEL + ? Experience.getXpForLevel(currentLevel + 1) + : Experience.MAX_SKILL_XP; + } + else + { + endLevelExp = goalEndXp; + } + } + + return true; + } + + public void tick(long delta) + { + // Don't tick skills that have not gained XP or have been reset. + if (xpGained <= 0) + { + return; + } + skillTime += delta; + } + + XpSnapshotSingle snapshot() + { + return XpSnapshotSingle.builder() + .startLevel(Experience.getLevelForXp(startLevelExp)) + .endLevel(Experience.getLevelForXp(endLevelExp)) + .xpGainedInSession(xpGained) + .xpRemainingToGoal(getXpRemaining()) + .xpPerHour(getXpHr()) + .skillProgressToGoal(getSkillProgress()) + .actionType(actionType) + .actionsInSession(getXpAction(actionType).getActions()) + .actionsRemainingToGoal(getActionsRemaining()) + .actionsPerHour(getActionsHr()) + .timeTillGoal(getTimeTillLevel(XpGoalTimeType.DAYS)) + .timeTillGoalHours(getTimeTillLevel(XpGoalTimeType.HOURS)) + .timeTillGoalShort(getTimeTillLevel(XpGoalTimeType.SHORT)) + .startGoalXp(startLevelExp) + .endGoalXp(endLevelExp) + .build(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.java new file mode 100644 index 0000000000..f14c643f02 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerConfig.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2018, Levi + * Copyright (c) 2020, Anthony + * 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 net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Units; + +@ConfigGroup("xpTracker") +public interface XpTrackerConfig extends Config +{ + @ConfigSection( + name = "Overlay", + description = "Canvas overlay options", + position = 99 + ) + String overlaySection = "overlay"; + + @ConfigItem( + position = 0, + keyName = "hideMaxed", + name = "Hide maxed skills", + description = "Stop globes from showing up for level 99 skills " + ) + default boolean hideMaxed() + { + return false; + } + + @ConfigItem( + position = 1, + keyName = "logoutPausing", + name = "Pause on Logout", + description = "Configures whether skills should pause on logout" + ) + default boolean pauseOnLogout() + { + return true; + } + + @ConfigItem( + position = 2, + keyName = "intermediateLevelMarkers", + name = "Show intermediate level markers", + description = "Marks intermediate levels on the progressbar" + ) + default boolean showIntermediateLevels() + { + return false; + } + + @ConfigItem( + position = 3, + keyName = "pauseSkillAfter", + name = "Auto pause after", + description = "Configures how many minutes passes before pausing a skill while in game and there's no XP, 0 means disabled" + ) + @Units(Units.MINUTES) + default int pauseSkillAfter() + { + return 0; + } + + @ConfigItem( + position = 4, + keyName = "skillTabOverlayMenuOptions", + name = "Add skill tab canvas menu option", + description = "Configures whether a menu option to show/hide canvas XP trackers will be added to skills on the skill tab", + section = overlaySection + ) + default boolean skillTabOverlayMenuOptions() + { + return true; + } + + @ConfigItem( + position = 5, + keyName = "onScreenDisplayMode", + name = "On-screen tracker display mode (top)", + description = "Configures the information displayed in the first line of on-screen XP overlays", + section = overlaySection + ) + default XpPanelLabel onScreenDisplayMode() + { + return XpPanelLabel.XP_GAINED; + } + + @ConfigItem( + position = 6, + keyName = "onScreenDisplayModeBottom", + name = "On-screen tracker display mode (bottom)", + description = "Configures the information displayed in the second line of on-screen XP overlays", + section = overlaySection + ) + default XpPanelLabel onScreenDisplayModeBottom() + { + return XpPanelLabel.XP_HOUR; + } + + @ConfigItem( + position = 7, + keyName = "xpPanelLabel1", + name = "Top-left XP info label", + description = "Configures the information displayed in the top-left of XP info box" + ) + default XpPanelLabel xpPanelLabel1() + { + return XpPanelLabel.XP_GAINED; + } + + @ConfigItem( + position = 8, + keyName = "xpPanelLabel2", + name = "Top-right XP info label", + description = "Configures the information displayed in the top-right of XP info box" + ) + + default XpPanelLabel xpPanelLabel2() + { + return XpPanelLabel.XP_LEFT; + } + + @ConfigItem( + position = 9, + keyName = "xpPanelLabel3", + name = "Bottom-left XP info label", + description = "Configures the information displayed in the bottom-left of XP info box" + ) + default XpPanelLabel xpPanelLabel3() + { + return XpPanelLabel.XP_HOUR; + } + + @ConfigItem( + position = 10, + keyName = "xpPanelLabel4", + name = "Bottom-right XP info label", + description = "Configures the information displayed in the bottom-right of XP info box" + ) + default XpPanelLabel xpPanelLabel4() + { + return XpPanelLabel.ACTIONS_LEFT; + } + + @ConfigItem( + position = 11, + keyName = "progressBarLabel", + name = "Progress bar label", + description = "Configures the info box progress bar to show Time to goal or percentage complete" + ) + default XpProgressBarLabel progressBarLabel() + { + return XpProgressBarLabel.PERCENTAGE; + } + + @ConfigItem( + position = 12, + keyName = "progressBarTooltipLabel", + name = "Tooltip label", + description = "Configures the info box progress bar tooltip to show Time to goal or percentage complete" + ) + default XpProgressBarLabel progressBarTooltipLabel() + { + return XpProgressBarLabel.TIME_TO_LEVEL; + } + + @ConfigItem( + position = 13, + keyName = "prioritizeRecentXpSkills", + name = "Move recently trained skills to top", + description = "Configures whether skills should be organized by most recently gained xp" + ) + default boolean prioritizeRecentXpSkills() + { + return false; + } +} 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 new file mode 100644 index 0000000000..5abfc92c89 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerPlugin.java @@ -0,0 +1,749 @@ +/* + * Copyright (c) 2017, Cameron + * Copyright (c) 2018, Levi + * 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 com.google.common.annotations.VisibleForTesting; +import static com.google.common.base.MoreObjects.firstNonNull; +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.Provides; +import java.awt.image.BufferedImage; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import javax.inject.Inject; +import lombok.AccessLevel; +import lombok.Setter; +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.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.Skill; +import net.runelite.api.VarPlayer; +import net.runelite.api.WorldType; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.StatChanged; +import net.runelite.api.widgets.WidgetID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +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.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.Text; +import net.runelite.http.api.xp.XpClient; +import okhttp3.OkHttpClient; + +@PluginDescriptor( + name = "XP Tracker", + description = "Enable the XP Tracker panel", + tags = {"experience", "levels", "panel"} +) +@Slf4j +public class XpTrackerPlugin extends Plugin +{ + /** + * Amount of EXP that must be gained for an update to be submitted. + */ + private static final int XP_THRESHOLD = 10_000; + + private static final String MENUOP_ADD_CANVAS_TRACKER = "Add to canvas"; + private static final String MENUOP_REMOVE_CANVAS_TRACKER = "Remove from canvas"; + + static final List COMBAT = ImmutableList.of( + Skill.ATTACK, + Skill.STRENGTH, + Skill.DEFENCE, + Skill.RANGED, + Skill.HITPOINTS, + Skill.MAGIC); + + @Inject + private ClientToolbar clientToolbar; + + @Inject + private Client client; + + @Inject + private SkillIconManager skillIconManager; + + @Inject + private XpTrackerConfig xpTrackerConfig; + + @Inject + private NPCManager npcManager; + + @Inject + private OverlayManager overlayManager; + + @Inject + private XpClient xpClient; + + private NavigationButton navButton; + @Setter(AccessLevel.PACKAGE) + @VisibleForTesting + private XpPanel xpPanel; + private XpWorldType lastWorldType; + private String lastUsername; + private long lastTickMillis = 0; + private boolean fetchXp; // fetch lastXp for the online xp tracker + private long lastXp = 0; + private boolean initializeTracker; + + private final XpState xpState = new XpState(); + private final XpPauseState xpPauseState = new XpPauseState(); + + @Provides + XpTrackerConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(XpTrackerConfig.class); + } + + @Provides + XpClient provideXpClient(OkHttpClient okHttpClient) + { + return new XpClient(okHttpClient); + } + + @Override + public void configure(Binder binder) + { + binder.bind(XpTrackerService.class).to(XpTrackerServiceImpl.class); + } + + @Override + protected void startUp() throws Exception + { + xpPanel = new XpPanel(this, xpTrackerConfig, client, skillIconManager); + + final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "/skill_icons/overall.png"); + + navButton = NavigationButton.builder() + .tooltip("XP Tracker") + .icon(icon) + .priority(2) + .panel(xpPanel) + .build(); + + clientToolbar.addNavigation(navButton); + + // Initialize the tracker & last xp if already logged in + fetchXp = true; + initializeTracker = true; + } + + @Override + protected void shutDown() throws Exception + { + overlayManager.removeIf(e -> e instanceof XpInfoBoxOverlay); + xpState.reset(); + clientToolbar.removeNavigation(navButton); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + GameState state = event.getGameState(); + if (state == GameState.LOGGED_IN) + { + // LOGGED_IN is triggered between region changes too. + // Check that the username changed or the world type changed. + XpWorldType type = worldSetToType(client.getWorldType()); + + if (!Objects.equals(client.getUsername(), lastUsername) || lastWorldType != type) + { + // Reset + log.debug("World change: {} -> {}, {} -> {}", + lastUsername, client.getUsername(), + firstNonNull(lastWorldType, ""), + firstNonNull(type, "")); + + lastUsername = client.getUsername(); + // xp is not available until after login is finished, so fetch it on the next gametick + fetchXp = true; + lastWorldType = type; + resetState(); + // Must be set from hitting the LOGGING_IN or HOPPING case below + assert initializeTracker; + } + } + else if (state == GameState.LOGGING_IN || state == GameState.HOPPING) + { + initializeTracker = true; + } + else if (state == GameState.LOGIN_SCREEN) + { + Player local = client.getLocalPlayer(); + if (local == null) + { + return; + } + + String username = local.getName(); + if (username == null) + { + return; + } + + long totalXp = client.getOverallExperience(); + // Don't submit xptrack unless xp threshold is reached + if (Math.abs(totalXp - lastXp) > XP_THRESHOLD) + { + xpClient.update(username); + lastXp = totalXp; + } + } + } + + private XpWorldType worldSetToType(EnumSet types) + { + XpWorldType xpType = NORMAL; + for (WorldType type : types) + { + XpWorldType t = XpWorldType.of(type); + if (t != NORMAL) + { + xpType = t; + } + } + return xpType; + } + + /** + * Adds an overlay to the canvas for tracking a specific skill. + * + * @param skill the skill for which the overlay should be added + */ + void addOverlay(Skill skill) + { + removeOverlay(skill); + overlayManager.add(new XpInfoBoxOverlay(this, xpTrackerConfig, skill, skillIconManager.getSkillImage(skill))); + } + + /** + * Removes an overlay from the overlayManager if it's present. + * + * @param skill the skill for which the overlay should be removed. + */ + void removeOverlay(Skill skill) + { + overlayManager.removeIf(e -> e instanceof XpInfoBoxOverlay && ((XpInfoBoxOverlay) e).getSkill() == skill); + } + + /** + * Check if there is an overlay on the canvas for the skill. + * + * @param skill the skill which should have an overlay. + * @return true if the skill has an overlay. + */ + boolean hasOverlay(final Skill skill) + { + return overlayManager.anyMatch(o -> o instanceof XpInfoBoxOverlay && ((XpInfoBoxOverlay) o).getSkill() == skill); + } + + /** + * Reset internal state and re-initialize all skills with XP currently cached by the RS client + * This is called by the user manually clicking resetSkillState in the UI. + * It reloads the current skills from the client after resetting internal state. + */ + void resetAndInitState() + { + resetState(); + + for (Skill skill : Skill.values()) + { + long currentXp; + if (skill == Skill.OVERALL) + { + currentXp = client.getOverallExperience(); + } + else + { + currentXp = client.getSkillExperience(skill); + } + + xpState.initializeSkill(skill, currentXp); + removeOverlay(skill); + } + } + + /** + * Throw out everything, the user has chosen a different account or world type. + * This resets both the internal state and UI elements + */ + private void resetState() + { + xpState.reset(); + xpPanel.resetAllInfoBoxes(); + xpPanel.updateTotal(new XpSnapshotSingle.XpSnapshotSingleBuilder().build()); + overlayManager.removeIf(e -> e instanceof XpInfoBoxOverlay); + } + + /** + * Reset an individual skill with the client's current known state of the skill + * Will also clear the skill from the UI. + * @param skill Skill to reset + */ + void resetSkillState(Skill skill) + { + int currentXp = client.getSkillExperience(skill); + xpState.resetSkill(skill, currentXp); + xpPanel.resetSkill(skill); + removeOverlay(skill); + } + + /** + * Reset all skills except for the one provided + * @param skill Skill to ignore during reset + */ + void resetOtherSkillState(Skill skill) + { + for (Skill s : Skill.values()) + { + // Overall is not reset from resetting individual skills + if (skill != s && s != Skill.OVERALL) + { + resetSkillState(s); + } + } + } + + @Subscribe + public void onStatChanged(StatChanged statChanged) + { + final Skill skill = statChanged.getSkill(); + final int currentXp = statChanged.getXp(); + final int currentLevel = statChanged.getLevel(); + final VarPlayer startGoal = startGoalVarpForSkill(skill); + final VarPlayer endGoal = endGoalVarpForSkill(skill); + final int startGoalXp = startGoal != null ? client.getVar(startGoal) : -1; + final int endGoalXp = endGoal != null ? client.getVar(endGoal) : -1; + + if (initializeTracker) + { + // This is the XP sync on login, wait until after login to begin counting + return; + } + + if (xpTrackerConfig.hideMaxed() && currentLevel >= Experience.MAX_REAL_LEVEL) + { + return; + } + + final XpStateSingle state = xpState.getSkill(skill); + state.setActionType(XpActionType.EXPERIENCE); + + final Actor interacting = client.getLocalPlayer().getInteracting(); + if (interacting instanceof NPC && COMBAT.contains(skill)) + { + final int xpModifier = worldSetToType(client.getWorldType()).modifier(client);; + final NPC npc = (NPC) interacting; + xpState.updateNpcExperience(skill, npc, npcManager.getHealth(npc.getId()), xpModifier); + } + + final XpUpdateResult updateResult = xpState.updateSkill(skill, currentXp, startGoalXp, endGoalXp); + xpPanel.updateSkillExperience(updateResult == XpUpdateResult.UPDATED, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + + // Also update the total experience + xpState.updateSkill(Skill.OVERALL, client.getOverallExperience(), -1, -1); + 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.getId())); + final boolean updated = XpUpdateResult.UPDATED.equals(updateResult); + xpPanel.updateSkillExperience(updated, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + } + + xpPanel.updateTotal(xpState.getTotalSnapshot()); + } + + @Subscribe + public void onGameTick(GameTick event) + { + if (initializeTracker) + { + initializeTracker = false; + + // Check for xp gained while logged out + for (Skill skill : Skill.values()) + { + if (skill == Skill.OVERALL || !xpState.isInitialized(skill)) + { + continue; + } + + XpStateSingle skillState = xpState.getSkill(skill); + final int currentXp = client.getSkillExperience(skill); + if (skillState.getCurrentXp() != currentXp) + { + if (currentXp < skillState.getCurrentXp()) + { + log.debug("Xp is going backwards! {} {} -> {}", skill, skillState.getCurrentXp(), currentXp); + resetState(); + break; + } + + log.debug("Skill xp for {} changed when offline: {} -> {}", skill, skillState.getCurrentXp(), currentXp); + // Offset start xp for offline gains + long diff = currentXp - skillState.getCurrentXp(); + skillState.setStartXp(skillState.getStartXp() + diff); + } + } + + // Initialize the tracker with the initial xp if not already initialized + for (Skill skill : Skill.values()) + { + if (skill == Skill.OVERALL) + { + continue; + } + + if (!xpState.isInitialized(skill)) + { + final int currentXp = client.getSkillExperience(skill); + // goal exps are not necessary for skill initialization + XpUpdateResult xpUpdateResult = xpState.updateSkill(skill, currentXp, -1, -1); + assert xpUpdateResult == XpUpdateResult.INITIALIZED; + } + } + + // Initialize the overall xp + if (!xpState.isInitialized(Skill.OVERALL)) + { + long overallXp = client.getOverallExperience(); + log.debug("Initializing XP tracker with {} overall exp", overallXp); + xpState.initializeSkill(Skill.OVERALL, overallXp); + } + } + + if (fetchXp) + { + lastXp = client.getOverallExperience(); + fetchXp = false; + } + + rebuildSkills(); + } + + @Subscribe + public void onMenuEntryAdded(final MenuEntryAdded event) + { + int widgetID = event.getActionParam1(); + + if (TO_GROUP(widgetID) != WidgetID.SKILLS_GROUP_ID + || !event.getOption().startsWith("View") + || !xpTrackerConfig.skillTabOverlayMenuOptions()) + { + return; + } + + // Get skill from menu option, eg. "View Attack guide" + final String skillText = event.getOption().split(" ")[1]; + final Skill skill = Skill.valueOf(Text.removeTags(skillText).toUpperCase()); + + MenuEntry[] menuEntries = client.getMenuEntries(); + menuEntries = Arrays.copyOf(menuEntries, menuEntries.length + 1); + + MenuEntry menuEntry = menuEntries[menuEntries.length - 1] = new MenuEntry(); + menuEntry.setTarget(skillText); + menuEntry.setOption(hasOverlay(skill) ? MENUOP_REMOVE_CANVAS_TRACKER : MENUOP_ADD_CANVAS_TRACKER); + menuEntry.setParam0(event.getActionParam0()); + menuEntry.setParam1(widgetID); + menuEntry.setType(MenuAction.RUNELITE.getId()); + + client.setMenuEntries(menuEntries); + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked event) + { + if (event.getMenuAction().getId() != MenuAction.RUNELITE.getId() + || TO_GROUP(event.getWidgetId()) != WidgetID.SKILLS_GROUP_ID) + { + System.out.println("opcode " + event.getMenuAction()); + System.out.println("id " + event.getMenuAction().getId()); + System.out.println("group " + TO_GROUP(event.getWidgetId())); + return; + } + + final Skill skill; + try + { + skill = Skill.valueOf(Text.removeTags(event.getMenuTarget()).toUpperCase()); + } + catch (IllegalArgumentException ex) + { + log.debug(null, ex); + return; + } + + switch (event.getMenuOption()) + { + case MENUOP_ADD_CANVAS_TRACKER: + addOverlay(skill); + break; + case MENUOP_REMOVE_CANVAS_TRACKER: + removeOverlay(skill); + break; + default: + System.out.println(event.getMenuOption()); + } + } + + XpStateSingle getSkillState(Skill skill) + { + return xpState.getSkill(skill); + } + + XpSnapshotSingle getSkillSnapshot(Skill skill) + { + return xpState.getSkillSnapshot(skill); + } + + private static VarPlayer startGoalVarpForSkill(final Skill skill) + { + switch (skill) + { + case ATTACK: + return VarPlayer.ATTACK_GOAL_START; + case MINING: + return VarPlayer.MINING_GOAL_START; + case WOODCUTTING: + return VarPlayer.WOODCUTTING_GOAL_START; + case DEFENCE: + return VarPlayer.DEFENCE_GOAL_START; + case MAGIC: + return VarPlayer.MAGIC_GOAL_START; + case RANGED: + return VarPlayer.RANGED_GOAL_START; + case HITPOINTS: + return VarPlayer.HITPOINTS_GOAL_START; + case AGILITY: + return VarPlayer.AGILITY_GOAL_START; + case STRENGTH: + return VarPlayer.STRENGTH_GOAL_START; + case PRAYER: + return VarPlayer.PRAYER_GOAL_START; + case SLAYER: + return VarPlayer.SLAYER_GOAL_START; + case FISHING: + return VarPlayer.FISHING_GOAL_START; + case RUNECRAFT: + return VarPlayer.RUNECRAFT_GOAL_START; + case HERBLORE: + return VarPlayer.HERBLORE_GOAL_START; + case FIREMAKING: + return VarPlayer.FIREMAKING_GOAL_START; + case CONSTRUCTION: + return VarPlayer.CONSTRUCTION_GOAL_START; + case HUNTER: + return VarPlayer.HUNTER_GOAL_START; + case COOKING: + return VarPlayer.COOKING_GOAL_START; + case FARMING: + return VarPlayer.FARMING_GOAL_START; + case CRAFTING: + return VarPlayer.CRAFTING_GOAL_START; + case SMITHING: + return VarPlayer.SMITHING_GOAL_START; + case THIEVING: + return VarPlayer.THIEVING_GOAL_START; + case FLETCHING: + return VarPlayer.FLETCHING_GOAL_START; + default: + return null; + } + } + + private static VarPlayer endGoalVarpForSkill(final Skill skill) + { + switch (skill) + { + case ATTACK: + return VarPlayer.ATTACK_GOAL_END; + case MINING: + return VarPlayer.MINING_GOAL_END; + case WOODCUTTING: + return VarPlayer.WOODCUTTING_GOAL_END; + case DEFENCE: + return VarPlayer.DEFENCE_GOAL_END; + case MAGIC: + return VarPlayer.MAGIC_GOAL_END; + case RANGED: + return VarPlayer.RANGED_GOAL_END; + case HITPOINTS: + return VarPlayer.HITPOINTS_GOAL_END; + case AGILITY: + return VarPlayer.AGILITY_GOAL_END; + case STRENGTH: + return VarPlayer.STRENGTH_GOAL_END; + case PRAYER: + return VarPlayer.PRAYER_GOAL_END; + case SLAYER: + return VarPlayer.SLAYER_GOAL_END; + case FISHING: + return VarPlayer.FISHING_GOAL_END; + case RUNECRAFT: + return VarPlayer.RUNECRAFT_GOAL_END; + case HERBLORE: + return VarPlayer.HERBLORE_GOAL_END; + case FIREMAKING: + return VarPlayer.FIREMAKING_GOAL_END; + case CONSTRUCTION: + return VarPlayer.CONSTRUCTION_GOAL_END; + case HUNTER: + return VarPlayer.HUNTER_GOAL_END; + case COOKING: + return VarPlayer.COOKING_GOAL_END; + case FARMING: + return VarPlayer.FARMING_GOAL_END; + case CRAFTING: + return VarPlayer.CRAFTING_GOAL_END; + case SMITHING: + return VarPlayer.SMITHING_GOAL_END; + case THIEVING: + return VarPlayer.THIEVING_GOAL_END; + case FLETCHING: + return VarPlayer.FLETCHING_GOAL_END; + default: + return null; + } + } + + @Schedule( + period = 1, + unit = ChronoUnit.SECONDS + ) + public void tickSkillTimes() + { + // Adjust unpause states + for (Skill skill : Skill.values()) + { + long skillExperience; + if (skill == Skill.OVERALL) + { + skillExperience = client.getOverallExperience(); + } + else + { + skillExperience = client.getSkillExperience(skill); + } + + xpPauseState.tickXp(skill, skillExperience, xpTrackerConfig.pauseSkillAfter()); + } + + final boolean loggedIn; + switch (client.getGameState()) + { + case LOGIN_SCREEN: + case LOGGING_IN: + case LOGIN_SCREEN_AUTHENTICATOR: + loggedIn = false; + break; + default: + loggedIn = true; + break; + } + xpPauseState.tickLogout(xpTrackerConfig.pauseOnLogout(), loggedIn); + + if (lastTickMillis == 0) + { + lastTickMillis = System.currentTimeMillis(); + return; + } + + final long nowMillis = System.currentTimeMillis(); + final long tickDelta = nowMillis - lastTickMillis; + lastTickMillis = nowMillis; + + for (Skill skill : Skill.values()) + { + if (!xpPauseState.isPaused(skill)) + { + xpState.tick(skill, tickDelta); + } + } + + rebuildSkills(); + } + + private void rebuildSkills() + { + // Rebuild calculated values like xp/hr in panel + for (Skill skill : Skill.values()) + { + xpPanel.updateSkillExperience(false, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + } + + xpPanel.updateTotal(xpState.getTotalSnapshot()); + } + + void pauseSkill(Skill skill, boolean pause) + { + if (pause ? xpPauseState.pauseSkill(skill) : xpPauseState.unpauseSkill(skill)) + { + xpPanel.updateSkillExperience(false, xpPauseState.isPaused(skill), skill, xpState.getSkillSnapshot(skill)); + } + } + + void pauseAllSkills(boolean pause) + { + for (Skill skill : Skill.values()) + { + pauseSkill(skill, pause); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerService.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerService.java new file mode 100644 index 0000000000..f423757528 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerService.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.plugins.xptracker; + +import net.runelite.api.Skill; + +public interface XpTrackerService +{ + /** + * Get the number of actions done + */ + int getActions(Skill skill); + + /** + * Get the number of actions per hour + */ + int getActionsHr(Skill skill); + + /** + * Get the number of actions remaining + */ + int getActionsLeft(Skill skill); + + /** + * Get the action type + */ + XpActionType getActionType(Skill skill); + + /** + * Get the amount of xp per hour + */ + int getXpHr(Skill skill); + + /** + * Get the start goal XP + */ + int getStartGoalXp(Skill skill); + + /** + * Get the amount of XP left until goal level + */ + int getEndGoalXp(Skill skill); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java new file mode 100644 index 0000000000..0968d14892 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpTrackerServiceImpl.java @@ -0,0 +1,83 @@ +/* + * 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.plugins.xptracker; + +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Skill; + +@Singleton +class XpTrackerServiceImpl implements XpTrackerService +{ + private final XpTrackerPlugin plugin; + + @Inject + XpTrackerServiceImpl(XpTrackerPlugin plugin) + { + this.plugin = plugin; + } + + @Override + public int getActions(Skill skill) + { + return plugin.getSkillSnapshot(skill).getActionsInSession(); + } + + @Override + public int getActionsHr(Skill skill) + { + return plugin.getSkillSnapshot(skill).getActionsPerHour(); + } + + @Override + public int getActionsLeft(Skill skill) + { + return plugin.getSkillSnapshot(skill).getActionsRemainingToGoal(); + } + + @Override + public XpActionType getActionType(Skill skill) + { + return plugin.getSkillSnapshot(skill).getActionType(); + } + + @Override + public int getXpHr(Skill skill) + { + return plugin.getSkillSnapshot(skill).getXpPerHour(); + } + + @Override + public int getStartGoalXp(Skill skill) + { + return plugin.getSkillSnapshot(skill).getStartGoalXp(); + } + + @Override + public int getEndGoalXp(Skill skill) + { + return plugin.getSkillSnapshot(skill).getEndGoalXp(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java new file mode 100644 index 0000000000..fde5e08470 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpUpdateResult.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Levi + * 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; + +enum XpUpdateResult +{ + NO_CHANGE, + INITIALIZED, + UPDATED, +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpWorldType.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpWorldType.java new file mode 100644 index 0000000000..245057e209 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpWorldType.java @@ -0,0 +1,83 @@ +/* + * 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.plugins.xptracker; + +import net.runelite.api.Client; +import net.runelite.api.Varbits; +import net.runelite.api.WorldType; + +enum XpWorldType +{ + NORMAL, + TOURNEY, + DMM + { + @Override + int modifier(Client client) + { + return 5; + } + }, + LEAGUE + { + @Override + int modifier(Client client) + { + if (client.getVar(Varbits.LEAGUE_RELIC_6) != 0) + { + return 16; + } + if (client.getVar(Varbits.LEAGUE_RELIC_4) != 0) + { + return 12; + } + if (client.getVar(Varbits.LEAGUE_RELIC_2) != 0) + { + return 8; + } + return 5; + } + }; + + int modifier(Client client) + { + return 1; + } + + static XpWorldType of(WorldType type) + { + switch (type) + { + case TOURNAMENT: + return TOURNEY; + case DEADMAN: + return DMM; + case LEAGUE: + return LEAGUE; + default: + return NORMAL; + } + } +} diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/MenuMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/MenuMixin.java index 69136a7ffe..bd9687bb28 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/MenuMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/MenuMixin.java @@ -186,7 +186,7 @@ public abstract class MenuMixin implements RSClient final int i = getMenuOptionCount() - 1; getMenuOptions()[i] = entry.getOption(); getMenuTargets()[i] = entry.getTarget(); - getMenuIdentifiers()[i] = entry.getType(); + getMenuIdentifiers()[i] = entry.getIdentifier(); getMenuOpcodes()[i] = entry.getOpcode(); getMenuArguments1()[i] = entry.getActionParam0(); getMenuArguments2()[i] = entry.getActionParam1(); @@ -214,7 +214,7 @@ public abstract class MenuMixin implements RSClient tempMenuAction.setOption(entry.getOption()); tempMenuAction.setOpcode(entry.getOpcode()); - tempMenuAction.setIdentifier(entry.getType()); + tempMenuAction.setIdentifier(entry.getIdentifier()); tempMenuAction.setParam0(entry.getActionParam0()); tempMenuAction.setParam1(entry.getActionParam1()); } diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java index 9ea4712c17..6f751bb1c3 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSClientMixin.java @@ -752,7 +752,7 @@ public abstract class RSClientMixin implements RSClient MenuEntry entry = entries[i] = new MenuEntry(); entry.setOption(menuOptions[i]); entry.setTarget(menuTargets[i]); - entry.setType(menuIdentifiers[i]); + entry.setIdentifier(menuIdentifiers[i]); entry.setOpcode(menuTypes[i]); entry.setActionParam0(params0[i]); entry.setActionParam1(params1[i]); @@ -783,7 +783,7 @@ public abstract class RSClientMixin implements RSClient menuOptions[count] = entry.getOption(); menuTargets[count] = entry.getTarget(); - menuIdentifiers[count] = entry.getType(); + menuIdentifiers[count] = entry.getIdentifier(); menuTypes[count] = entry.getOpcode(); params0[count] = entry.getActionParam0(); params1[count] = entry.getActionParam1(); @@ -830,7 +830,7 @@ public abstract class RSClientMixin implements RSClient { options[oldCount] = event.getOption(); targets[oldCount] = event.getTarget(); - identifiers[oldCount] = event.getType(); + identifiers[oldCount] = event.getIdentifier(); opcodes[oldCount] = event.getOpcode(); arguments1[oldCount] = event.getActionParam0(); arguments2[oldCount] = event.getActionParam1(); @@ -1425,14 +1425,14 @@ public abstract class RSClientMixin implements RSClient { client.getLogger().info( "|MenuAction|: MenuOption={} MenuTarget={} Id={} Opcode={} Param0={} Param1={} CanvasX={} CanvasY={} Authentic={}", - menuOptionClicked.getOption(), menuOptionClicked.getTarget(), menuOptionClicked.getType(), + menuOptionClicked.getOption(), menuOptionClicked.getTarget(), menuOptionClicked.getIdentifier(), menuOptionClicked.getOpcode(), menuOptionClicked.getActionParam0(), menuOptionClicked.getActionParam1(), canvasX, canvasY, authentic ); } copy$menuAction(menuOptionClicked.getActionParam0(), menuOptionClicked.getActionParam1(), menuOptionClicked.getOpcode(), - menuOptionClicked.getType(), menuOptionClicked.getOption(), menuOptionClicked.getTarget(), canvasX, canvasY); + menuOptionClicked.getIdentifier(), menuOptionClicked.getOption(), menuOptionClicked.getTarget(), canvasX, canvasY); } @Override