diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f340df3..ee66a13f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## Unreleased
+- Major: Add leagues notifier for areas, relics, and tasks. (#366)
+
## 1.6.5
- Minor: Allow notifications on seasonal worlds to be ignored via advanced config. (#357)
diff --git a/README.md b/README.md
index 4eb88045..28575bb0 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ To use this plugin, a webhook URL is required; you can obtain one from Discord w
- [Player Kills](#player-kills): Sends a webhook message upon killing another player (while hitsplats are still visible)
- [Group Storage](#group-storage): Sends a webhook message upon Group Ironman Shared Bank transactions (i.e., depositing or withdrawing items)
- [Grand Exchange](#grand-exchange): Sends a webhook message upon buying or selling items on the GE (with customizable value threshold)
+- [Leagues](#leagues): Sends a webhook message upon completing a Leagues IV task or unlocking a region/relic
## Other Setup
@@ -759,6 +760,112 @@ See [javadocs](https://static.runelite.net/api/runelite-api/net/runelite/api/Gra
+### Leagues:
+
+Leagues notifications include: region unlocked, relic unlocked, and task completed (with customizable difficulty threshold).
+
+Each of these events can be independently enabled or disabled in the notifier settings.
+
+
+ JSON for Area Unlock Notifications:
+
+```json5
+{
+ "type": "LEAGUES_AREA",
+ "content": "%USERNAME% selected their second region: Kandarin.",
+ "playerName": "%USERNAME%",
+ "accountType": "IRONMAN",
+ "seasonalWorld": true,
+ "extra": {
+ "area": "Kandarin",
+ "index": 2,
+ "tasksCompleted": 200,
+ "tasksUntilNextArea": 200
+ }
+}
+```
+
+Note: `index` refers to the order of region unlocks.
+Here, Kandarin was the second region selected.
+For all players, Karamja is the _zeroth_ region selected (and there is no notification for Misthalin).
+
+
+
+
+ JSON for Relic Chosen Notifications:
+
+```json5
+{
+ "type": "LEAGUES_RELIC",
+ "content": "%USERNAME% unlocked a Tier 1 Relic: Production Prodigy.",
+ "playerName": "%USERNAME%",
+ "accountType": "IRONMAN",
+ "seasonalWorld": true,
+ "extra": {
+ "relic": "Production Prodigy",
+ "tier": 1,
+ "requiredPoints": 0,
+ "totalPoints": 20,
+ "pointsUntilNextTier": 480
+ }
+}
+```
+
+
+
+
+ JSON for Task Completed Notifications:
+
+```json5
+{
+ "type": "LEAGUES_TASK",
+ "content": "%USERNAME% completed a Easy task: Pickpocket a Citizen.",
+ "playerName": "%USERNAME%",
+ "accountType": "IRONMAN",
+ "seasonalWorld": true,
+ "extra": {
+ "taskName": "Pickpocket a Citizen",
+ "difficulty": "EASY",
+ "taskPoints": 10,
+ "totalPoints": 30,
+ "tasksCompleted": 3,
+ "pointsUntilNextRelic": 470,
+ "pointsUntilNextTrophy": 2470
+ }
+}
+```
+
+
+
+
+ JSON for Task Notifications that unlocked a Trophy:
+
+```json5
+{
+ "type": "LEAGUES_TASK",
+ "content": "%USERNAME% completed a Hard task, The Frozen Door, unlocking the Bronze trophy!",
+ "playerName": "%USERNAME%",
+ "accountType": "IRONMAN",
+ "seasonalWorld": true,
+ "extra": {
+ "taskName": "The Frozen Door",
+ "difficulty": "HARD",
+ "taskPoints": 80,
+ "totalPoints": 2520,
+ "tasksCompleted": 119,
+ "tasksUntilNextArea": 81,
+ "pointsUntilNextRelic": 1480,
+ "pointsUntilNextTrophy": 2480,
+ "earnedTrophy": "Bronze"
+ }
+}
+```
+
+
+
+Note: Fields like `tasksUntilNextArea`, `pointsUntilNextRelic`, and `pointsUntilNextTrophy` can be omitted
+if there is no next level of progression (i.e., all three regions selected, all relic tiers unlocked, all trophies acquired).
+
### Metadata:
On login, Dink can submit a character summary containing data that spans multiple notifiers to a custom webhook handler (configurable in the `Advanced` section). This login notification is delayed by at least 5 seconds in order to gather all of the relevant data. However, `collectionLog` data can be missing if the user does not have the Character Summary tab selected (since the client otherwise is not sent that data).
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index 93aacbef..4b1792b4 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -10,6 +10,7 @@
import dinkplugin.notifiers.GrandExchangeNotifier;
import dinkplugin.notifiers.GroupStorageNotifier;
import dinkplugin.notifiers.KillCountNotifier;
+import dinkplugin.notifiers.LeaguesNotifier;
import dinkplugin.notifiers.LevelNotifier;
import dinkplugin.notifiers.MetaNotifier;
import dinkplugin.notifiers.LootNotifier;
@@ -82,6 +83,7 @@ public class DinkPlugin extends Plugin {
private @Inject PlayerKillNotifier pkNotifier;
private @Inject GroupStorageNotifier groupStorageNotifier;
private @Inject GrandExchangeNotifier grandExchangeNotifier;
+ private @Inject LeaguesNotifier leaguesNotifier;
private @Inject MetaNotifier metaNotifier;
@Override
@@ -186,6 +188,7 @@ public void onChatMessage(ChatMessage message) {
combatTaskNotifier.onGameMessage(chatMessage);
deathNotifier.onGameMessage(chatMessage);
speedrunNotifier.onGameMessage(chatMessage);
+ leaguesNotifier.onGameMessage(chatMessage);
break;
case FRIENDSCHATNOTIFICATION:
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index 272b2051..d4160150 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -4,6 +4,7 @@
import dinkplugin.domain.ClueTier;
import dinkplugin.domain.CombatAchievementTier;
import dinkplugin.domain.FilterMode;
+import dinkplugin.domain.LeagueTaskDifficulty;
import dinkplugin.domain.PlayerLookupService;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
@@ -151,6 +152,14 @@ public interface DinkPluginConfig extends Config {
)
String grandExchangeSection = "Grand Exchange";
+ @ConfigSection(
+ name = "Leagues",
+ description = "Settings for notifying when you complete league tasks, unlock areas, and redeem relics",
+ position = 200,
+ closedByDefault = true
+ )
+ String leaguesSection = "Leagues";
+
@ConfigSection(
name = "Advanced",
description = "Do not modify without fully understanding these settings",
@@ -351,7 +360,8 @@ default String metadataWebhook() {
@ConfigItem(
keyName = "ignoreSeasonalWorlds",
name = "Ignore Seasonal Worlds",
- description = "Whether to suppress notifications that occur on seasonal worlds like Leagues",
+ description = "Whether to suppress notifications that occur on seasonal worlds like Leagues.
" +
+ "Note: the Leagues-specific notifier uses an independent config toggle",
position = 1015,
section = advancedSection
)
@@ -546,6 +556,18 @@ default String grandExchangeWebhook() {
return "";
}
+ @ConfigItem(
+ keyName = "leaguesWebhook",
+ name = "Leagues Webhook Override",
+ description = "If non-empty, Leagues messages are sent to this URL, instead of the primary URL.
" +
+ "Note: this only applies to the Leagues notifier, not every notifier in a seasonal world",
+ position = -1,
+ section = webhookSection
+ )
+ default String leaguesWebhook() {
+ return "";
+ }
+
@ConfigItem(
keyName = "collectionLogEnabled",
name = "Enable collection log",
@@ -1660,4 +1682,70 @@ default String grandExchangeNotifyMessage() {
return "%USERNAME% %TYPE% %ITEM% on the GE";
}
+ @ConfigItem(
+ keyName = "notifyLeagues",
+ name = "Enable Leagues",
+ description = "Enable notifications upon various leagues events",
+ position = 200,
+ section = leaguesSection
+ )
+ default boolean notifyLeagues() {
+ return false;
+ }
+
+ @ConfigItem(
+ keyName = "leaguesSendImage",
+ name = "Send Image",
+ description = "Send image with the notification",
+ position = 201,
+ section = leaguesSection
+ )
+ default boolean leaguesSendImage() {
+ return true;
+ }
+
+ @ConfigItem(
+ keyName = "leaguesAreaUnlock",
+ name = "Send Area Unlocks",
+ description = "Send notifications upon area unlocks",
+ position = 202,
+ section = leaguesSection
+ )
+ default boolean leaguesAreaUnlock() {
+ return true;
+ }
+
+ @ConfigItem(
+ keyName = "leaguesRelicUnlock",
+ name = "Send Relic Unlocks",
+ description = "Send notifications upon relic unlocks",
+ position = 203,
+ section = leaguesSection
+ )
+ default boolean leaguesRelicUnlock() {
+ return true;
+ }
+
+ @ConfigItem(
+ keyName = "leaguesTaskCompletion",
+ name = "Send Completed Tasks",
+ description = "Send notifications upon completing a task",
+ position = 204,
+ section = leaguesSection
+ )
+ default boolean leaguesTaskCompletion() {
+ return true;
+ }
+
+ @ConfigItem(
+ keyName = "leaguesTaskMinTier",
+ name = "Task Min Difficulty",
+ description = "The minimum tier of a task for a notification to be sent",
+ position = 205,
+ section = leaguesSection
+ )
+ default LeagueTaskDifficulty leaguesTaskMinTier() {
+ return LeagueTaskDifficulty.EASY;
+ }
+
}
diff --git a/src/main/java/dinkplugin/domain/LeagueRelicTier.java b/src/main/java/dinkplugin/domain/LeagueRelicTier.java
new file mode 100644
index 00000000..1ae9d4b4
--- /dev/null
+++ b/src/main/java/dinkplugin/domain/LeagueRelicTier.java
@@ -0,0 +1,38 @@
+package dinkplugin.domain;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Collections;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+@Getter
+@RequiredArgsConstructor
+public enum LeagueRelicTier {
+ ONE(0),
+ TWO(500),
+ THREE(1_200),
+ FOUR(2_000),
+ FIVE(4_000),
+ SIX(7_500),
+ SEVEN(15_000),
+ EIGHT(24_000);
+
+ /**
+ * Points required to unlock a relic of a given tier.
+ *
+ * @see Wiki Reference
+ */
+ private final int points;
+
+ public static final NavigableMap TIER_BY_POINTS;
+
+ static {
+ NavigableMap tiers = new TreeMap<>();
+ for (LeagueRelicTier tier : values()) {
+ tiers.put(tier.getPoints(), tier);
+ }
+ TIER_BY_POINTS = Collections.unmodifiableNavigableMap(tiers);
+ }
+}
diff --git a/src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java b/src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java
new file mode 100644
index 00000000..73d73215
--- /dev/null
+++ b/src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java
@@ -0,0 +1,37 @@
+package dinkplugin.domain;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Getter
+@RequiredArgsConstructor
+public enum LeagueTaskDifficulty {
+ EASY(10),
+ MEDIUM(40),
+ HARD(80),
+ ELITE(200),
+ MASTER(400);
+
+ /**
+ * Points earned from completed a task of the given difficulty.
+ *
+ * @see Wiki Reference
+ */
+ private final int points;
+ private final String displayName = this.name().charAt(0) + this.name().substring(1).toLowerCase();
+
+ @Override
+ public String toString() {
+ return this.displayName;
+ }
+
+ public static final Map TIER_BY_LOWER_NAME = Collections.unmodifiableMap(
+ Arrays.stream(values()).collect(Collectors.toMap(t -> t.name().toLowerCase(), Function.identity()))
+ );
+}
diff --git a/src/main/java/dinkplugin/message/DiscordMessageHandler.java b/src/main/java/dinkplugin/message/DiscordMessageHandler.java
index d2d029bd..be712525 100644
--- a/src/main/java/dinkplugin/message/DiscordMessageHandler.java
+++ b/src/main/java/dinkplugin/message/DiscordMessageHandler.java
@@ -219,12 +219,12 @@ private NotificationBody> enrichBody(NotificationBody> mBody, boolean sendIm
}
}
- NotificationBody.NotificationBodyBuilder> builder = mBody.toBuilder();
-
- if (!config.ignoreSeasonal()) {
- builder.seasonalWorld(client.getWorldType().contains(WorldType.SEASONAL));
+ if (!config.ignoreSeasonal() && !mBody.isSeasonalWorld() && client.getWorldType().contains(WorldType.SEASONAL)) {
+ mBody = mBody.withSeasonalWorld(true);
}
+ NotificationBody.NotificationBodyBuilder> builder = mBody.toBuilder();
+
if (config.sendDiscordUser()) {
builder.discordUser(DiscordProfile.of(discordService.getCurrentUser()));
}
@@ -333,7 +333,7 @@ private static List