Skip to content

Commit

Permalink
feat: add leagues notifier for areas, relics, and tasks (#366)
Browse files Browse the repository at this point in the history
* feat: add leagues notifier for areas, relics, and tasks

* chore(relic): update rich embed field name

* feat: add tasksUntilNextArea to LeaguesTaskNotificationData

* chore: add mock tests

* chore: update changelog

* chore(region): update rich embed field name

* chore: update readme

* chore(readme): clarify area index

* chore(readme): markdown formatting

* feat: add requiredPoints to relic metadata

* chore(readme): add trophy unlock json example

* chore: link task names to general league tasks wiki article

* refactor: move some varbit ids to constant fields

* chore: clarify leagues webhook override

* chore: loosen default leaguesTaskMinTier

* fix: handle unknown relic names more gracefully

* fix: ensure computeEmbeds can read seasonal status

* unrelated commit: add some tests for Utils.sanitize

* docs: clarify more metadata field nullability

* refactor: prefer nested if over assignment within conditional

* chore: add another test case

---------

Co-authored-by: Rasmus Karlsson <[email protected]>
  • Loading branch information
iProdigy and pajlada authored Nov 17, 2023
1 parent d98697c commit c8d7d50
Show file tree
Hide file tree
Showing 15 changed files with 1,064 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -759,6 +760,112 @@ See [javadocs](https://static.runelite.net/api/runelite-api/net/runelite/api/Gra

</details>

### 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.

<details>
<summary>JSON for Area Unlock Notifications:</summary>

```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).

</details>

<details>
<summary>JSON for Relic Chosen Notifications:</summary>

```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
}
}
```

</details>

<details>
<summary>JSON for Task Completed Notifications:</summary>

```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
}
}
```

</details>

<details>
<summary>JSON for Task Notifications that unlocked a Trophy:</summary>

```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"
}
}
```

</details>

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).
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/dinkplugin/DinkPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -186,6 +188,7 @@ public void onChatMessage(ChatMessage message) {
combatTaskNotifier.onGameMessage(chatMessage);
deathNotifier.onGameMessage(chatMessage);
speedrunNotifier.onGameMessage(chatMessage);
leaguesNotifier.onGameMessage(chatMessage);
break;

case FRIENDSCHATNOTIFICATION:
Expand Down
90 changes: 89 additions & 1 deletion src/main/java/dinkplugin/DinkPluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.<br/>" +
"Note: the Leagues-specific notifier uses an independent config toggle",
position = 1015,
section = advancedSection
)
Expand Down Expand Up @@ -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.<br/>" +
"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",
Expand Down Expand Up @@ -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;
}

}
38 changes: 38 additions & 0 deletions src/main/java/dinkplugin/domain/LeagueRelicTier.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://oldschool.runescape.wiki/w/Trailblazer_Reloaded_League/Relics">Wiki Reference</a>
*/
private final int points;

public static final NavigableMap<Integer, LeagueRelicTier> TIER_BY_POINTS;

static {
NavigableMap<Integer, LeagueRelicTier> tiers = new TreeMap<>();
for (LeagueRelicTier tier : values()) {
tiers.put(tier.getPoints(), tier);
}
TIER_BY_POINTS = Collections.unmodifiableNavigableMap(tiers);
}
}
37 changes: 37 additions & 0 deletions src/main/java/dinkplugin/domain/LeagueTaskDifficulty.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://oldschool.runescape.wiki/w/Trailblazer_Reloaded_League/Tasks">Wiki Reference</a>
*/
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<String, LeagueTaskDifficulty> TIER_BY_LOWER_NAME = Collections.unmodifiableMap(
Arrays.stream(values()).collect(Collectors.toMap(t -> t.name().toLowerCase(), Function.identity()))
);
}
10 changes: 5 additions & 5 deletions src/main/java/dinkplugin/message/DiscordMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
Expand Down Expand Up @@ -333,7 +333,7 @@ private static List<Embed> computeEmbeds(@NotNull NotificationBody<?> body, bool
Author author = Author.builder()
.name(body.getPlayerName())
.url(playerLookupService.getPlayerUrl(body.getPlayerName()))
.iconUrl(Utils.getChatBadge(body.getAccountType()))
.iconUrl(Utils.getChatBadge(body.getAccountType(), body.isSeasonalWorld()))
.build();
Footer footer = StringUtils.isBlank(footerText) ? null : Footer.builder()
.text(Utils.truncate(footerText, Embed.MAX_FOOTER_LENGTH))
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/dinkplugin/message/NotificationType.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public enum NotificationType {
PLAYER_KILL("Player Kill", "playerKillImage.png", WIKI_IMG_BASE_URL + "Skull_(status)_icon.png"),
GROUP_STORAGE("Group Shared Storage", "groupStorage.png", WIKI_IMG_BASE_URL + "Coins_10000.png"),
GRAND_EXCHANGE("Grand Exchange", "grandExchange.png", WIKI_IMG_BASE_URL + "Grand_Exchange_icon.png"),
LEAGUES_AREA("Area Unlocked", "leaguesArea.png", WIKI_IMG_BASE_URL + "Trailblazer_Reloaded_League_-_%3F_Relic.png"),
LEAGUES_RELIC("Relic Chosen", "leaguesRelic.png", WIKI_IMG_BASE_URL + "Trailblazer_Reloaded_League_-_relics_icon.png"),
LEAGUES_TASK("Task Completed", "leaguesTask.png", WIKI_IMG_BASE_URL + "Trailblazer_Reloaded_League_icon.png"),
LOGIN("Player Login", "login.png", WIKI_IMG_BASE_URL + "Prop_sword.png");

private final String title;
Expand Down
Loading

0 comments on commit c8d7d50

Please sign in to comment.