Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include rarity and luck in pet notifications #433

Merged
merged 28 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
84b5aca
feat: include rarity and luck in pet notifications
iProdigy Mar 4, 2024
b406ca1
feat: calculate agility pet luck
iProdigy Mar 5, 2024
34c89c4
Merge branch 'master' into feature/pet-rarity
iProdigy Mar 16, 2024
528d991
chore: update changelog
iProdigy Mar 16, 2024
aada140
fix: toa levels beyond 550 don't alter drop rate
iProdigy Mar 17, 2024
fdd85dd
fix: assume 3 searches per rift close instead of 1
iProdigy Mar 20, 2024
6b613dd
fix: use tempoross reward pool instead of kc
iProdigy Mar 20, 2024
b47541a
Merge branch 'master' into feature/pet-rarity
iProdigy Mar 22, 2024
6874538
chore: add Quetzin without rarity info
iProdigy Mar 22, 2024
3b34bf4
Merge branch 'master' into feature/pet-rarity
iProdigy Mar 23, 2024
cf8de35
Merge branch 'master' into feature/pet-rarity
iProdigy Mar 28, 2024
17e883a
Merge branch 'master' into feature/pet-rarity
iProdigy Mar 29, 2024
9d60269
feat: add best guess at quetzin rarity
iProdigy Mar 30, 2024
b98790d
fix: account for different drop rates per skill level
iProdigy Mar 30, 2024
b05d327
refactor: use MultiSource for agility pet
iProdigy Mar 30, 2024
6b2da59
fix: off-by-one for xp delta calculation
iProdigy Mar 30, 2024
5663ccc
Merge branch 'master' into feature/pet-rarity
iProdigy Mar 31, 2024
958bc31
chore: remove comment; quetzin rate was correct
iProdigy Apr 5, 2024
459f35b
Merge branch 'master' into feature/pet-rarity
iProdigy Apr 7, 2024
7bee735
feat: support smol heredit pet
iProdigy Apr 11, 2024
39e9683
chore: add unit test for SerializedLoot
iProdigy Apr 11, 2024
c751785
Merge branch 'master' into feature/pet-rarity
iProdigy Apr 12, 2024
1d306ef
Merge branch 'master' into feature/pet-rarity
Felanbird Apr 20, 2024
57f04bd
Merge branch 'master' into feature/pet-rarity
iProdigy Jun 26, 2024
78693c0
chore: update changelog
iProdigy Jun 26, 2024
9efa511
Merge branch 'master' into feature/pet-rarity
iProdigy Jul 20, 2024
c93efe5
Merge branch 'master' into feature/pet-rarity
iProdigy Aug 4, 2024
83af2f5
Merge branch 'master' into feature/pet-rarity
iProdigy Aug 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Unreleased

- Minor: Include rarity and luck for pet notifications. (#433)
- Minor: Allow filtering of loot notifications if both rarity and value thresholds are not met. (#499)

## 1.10.2
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/dinkplugin/message/Field.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ public Field(String name, String value) {
this(name, value, true);
}

public static Field ofLuck(double probability, int kTrials) {
return ofLuck(MathUtils.cumulativeGeometric(probability, kTrials));
}

public static Field ofLuck(double geomCdf) {
String percentile = geomCdf < 0.5
? "Top " + MathUtils.formatPercentage(geomCdf, 2) + " (Lucky)"
: "Bottom " + MathUtils.formatPercentage(1 - geomCdf, 2) + " (Unlucky)";
return new Field("Luck", Field.formatBlock("", percentile));
}

public static String formatBlock(String codeBlockLanguage, String content) {
return String.format("```%s\n%s\n```", StringUtils.defaultString(codeBlockLanguage), content);
}
Expand Down
402 changes: 345 additions & 57 deletions src/main/java/dinkplugin/notifiers/PetNotifier.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import dinkplugin.message.Field;
import dinkplugin.util.Drop;
import dinkplugin.util.MathUtils;
import lombok.EqualsAndHashCode;
import lombok.Value;
import net.runelite.client.util.QuantityFormatter;
Expand Down Expand Up @@ -68,11 +67,7 @@ public List<Field> getFields() {
fields.add(new Field("Drop Rate", Field.formatProbability(dropRate)));
}
if (dropperKillCount != null && dropRate != null) {
double geomCdf = MathUtils.cumulativeGeometric(dropRate, dropperKillCount);
String percentile = geomCdf < 0.5
? "Top " + MathUtils.formatPercentage(geomCdf, 2) + " (Lucky)"
: "Bottom " + MathUtils.formatPercentage(1 - geomCdf, 2) + " (Unlucky)";
fields.add(new Field("Luck", Field.formatBlock("", percentile)));
fields.add(Field.ofLuck(dropRate, dropperKillCount));
}
return fields;
}
Expand Down
25 changes: 24 additions & 1 deletion src/main/java/dinkplugin/notifiers/data/PetNotificationData.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,41 @@ public class PetNotificationData extends NotificationData {
@Nullable
Boolean previouslyOwned;

/**
* The approximate drop rate of the pet.
* <p>
* This value is least accurate for skilling pets and raids.
*/
@Nullable
Double rarity;

/**
* The approximate number of actions performed that would roll a drop table containing the pet.
* <p>
* This value is least accurate for skilling pets and pets dropped by multiple NPCs.
*/
@Nullable
Integer estimatedActions;

@Nullable
transient Double luck;

@Override
public List<Field> getFields() {
if (petName == null || petName.isEmpty())
return super.getFields();

List<Field> fields = new ArrayList<>(3);
List<Field> fields = new ArrayList<>(5);
fields.add(new Field("Name", Field.formatBlock("", petName)));
String status = getStatus();
if (status != null)
fields.add(new Field("Status", Field.formatBlock("", status)));
if (milestone != null)
fields.add(new Field("Milestone", Field.formatBlock("", milestone)));
if (rarity != null)
fields.add(new Field("Rarity", Field.formatProbability(rarity)));
if (luck != null)
fields.add(Field.ofLuck(luck));
return fields;
}

Expand Down
33 changes: 28 additions & 5 deletions src/main/java/dinkplugin/util/KillCountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class KillCountService {

private static final String RL_CHAT_CMD_PLUGIN_NAME = ChatCommandsPlugin.class.getSimpleName().toLowerCase();
private static final String RL_LOOT_PLUGIN_NAME = LootTrackerPlugin.class.getSimpleName().toLowerCase();
private static final String RIFT_PREFIX = "Amount of rifts you have closed: ";
private static final String HERBIBOAR_PREFIX = "Your herbiboar harvest count is: ";

@Inject
private ConfigManager configManager;
Expand Down Expand Up @@ -128,6 +130,20 @@ public void onGameMessage(String message) {
return;
}

// guardians of the rift count (for pet tracking)
if (message.startsWith(RIFT_PREFIX)) {
int riftCount = Integer.parseInt(message.substring(RIFT_PREFIX.length(), message.length() - 1));
killCounts.put("Guardians of the Rift", riftCount);
iProdigy marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// herbiboar count (for pet tracking)
if (message.startsWith(HERBIBOAR_PREFIX)) {
int harvestCount = Integer.parseInt(message.substring(HERBIBOAR_PREFIX.length(), message.length() - 1));
killCounts.put("Herbiboar", harvestCount);
return;
}

// update cached KC via boss chat message with robustness for chat event coming before OR after the loot event
KillCountNotifier.parseBoss(message).ifPresent(pair -> {
String boss = pair.getKey();
Expand Down Expand Up @@ -225,6 +241,12 @@ private Integer getStoredKillCount(@NotNull LootRecordType type, @NotNull String
}
}

SerializedLoot lootRecord = getLootTrackerRecord(type, sourceName);
return lootRecord != null ? lootRecord.getKills() : null;
}

@Nullable
public SerializedLoot getLootTrackerRecord(@NotNull LootRecordType type, @NotNull String sourceName) {
if (ConfigUtil.isPluginDisabled(configManager, RL_LOOT_PLUGIN_NAME)) {
// assume stored kc is useless if loot tracker plugin is disabled
return null;
Expand All @@ -235,19 +257,20 @@ private Integer getStoredKillCount(@NotNull LootRecordType type, @NotNull String
);
if (json == null) {
// no kc stored implies first kill
return 0;
return new SerializedLoot();
}
try {
int kc = gson.fromJson(json, SerializedLoot.class).getKills();
SerializedLoot lootRecord = gson.fromJson(json, SerializedLoot.class);

// loot tracker doesn't count kill if no loot - https://github.com/runelite/runelite/issues/5077
OptionalDouble nothingProbability = rarityService.getRarity(sourceName, -1, 0);
if (nothingProbability.isPresent() && nothingProbability.getAsDouble() < 1.0) {
// estimate the actual kc (including kills with no loot)
kc = (int) Math.round(kc / (1 - nothingProbability.getAsDouble()));
int kc = (int) Math.round(lootRecord.getKills() / (1 - nothingProbability.getAsDouble()));
return lootRecord.withKills(kc);
} else {
return lootRecord;
}

return kc;
} catch (JsonSyntaxException e) {
// should not occur unless loot tracker changes stored loot POJO structure
log.warn("Failed to read kills from loot tracker config", e);
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/dinkplugin/util/MathUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ public class MathUtils {
public static final double EPSILON = 0.00001;
private static final int[] FACTORIALS;

public int sum(int[] array) {
int x = 0;
for (int i : array) {
x += i;
}
return x;
}

public boolean lessThanOrEqual(double a, double b) {
return a < b || DoubleMath.fuzzyEquals(a, b, EPSILON);
}
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/dinkplugin/util/SerializedLoot.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
package dinkplugin.util;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.With;

/**
* Contains kill count observed by base runelite loot tracker plugin, stored in profile configuration.
*
* @see <a href="https://github.com/runelite/runelite/blob/master/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/ConfigLoot.java#L41">RuneLite class</a>
*/
@Data
@With
@Setter(AccessLevel.PRIVATE)
@NoArgsConstructor
@AllArgsConstructor
public class SerializedLoot {
private int kills;
private int[] drops;

public int getQuantity(int itemId) {
final int n = drops != null ? drops.length : 0;
for (int i = 0; i < n; i += 2) {
if (drops[i] == itemId) {
return drops[i + 1];
}
}
return 0;
}
}
46 changes: 33 additions & 13 deletions src/test/java/dinkplugin/notifiers/PetNotifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@
import dinkplugin.notifiers.data.PetNotificationData;
import dinkplugin.util.ItemSearcher;
import dinkplugin.util.ItemUtils;
import dinkplugin.util.KillCountService;
import dinkplugin.util.MathUtils;
import net.runelite.api.ItemID;
import net.runelite.api.NPC;
import net.runelite.api.NpcID;
import net.runelite.api.Varbits;
import net.runelite.api.WorldType;
import net.runelite.client.events.NpcLootReceived;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import java.util.Collections;
import java.util.EnumSet;
import java.util.stream.IntStream;

import static dinkplugin.notifiers.PetNotifier.MAX_TICKS_WAIT;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -35,6 +42,10 @@ class PetNotifierTest extends MockedNotifierTest {
@Mock
ItemSearcher itemSearcher;

@Bind
@InjectMocks
KillCountService killCountService;

@Override
@BeforeEach
protected void setUp() {
Expand All @@ -60,7 +71,7 @@ void testNotify() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, false, null))
.extra(new PetNotificationData(null, null, false, null, null, null, null))
.text(buildTemplate(PLAYER_NAME + " feels something weird sneaking into their backpack"))
.type(NotificationType.PET)
.build()
Expand All @@ -81,7 +92,7 @@ void testNotifyDuplicate() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, true, true))
.extra(new PetNotificationData(null, null, true, true, null, null, null))
.text(buildTemplate(PLAYER_NAME + " has a funny feeling like they would have been followed..."))
.type(NotificationType.PET)
.build()
Expand All @@ -96,6 +107,15 @@ void testNotifyCollection() {
// prepare mocks
when(itemSearcher.findItemId("Tzrek-jad")).thenReturn(itemId);
when(client.getVarbitValue(Varbits.COLLECTION_LOG_NOTIFICATION)).thenReturn(1);
String npcName = "TzTok-Jad";
NPC npc = mock(NPC.class);
when(npc.getName()).thenReturn(npcName);
when(npc.getId()).thenReturn(NpcID.TZTOKJAD);
int kc = 100;
double rarity = 1.0 / 200;
double luck = MathUtils.cumulativeGeometric(rarity, kc);
when(configManager.getRSProfileConfiguration("killcount", npcName.toLowerCase(), int.class)).thenReturn(kc);
killCountService.onNpcKill(new NpcLootReceived(npc, Collections.emptyList()));

// send fake message
notifier.onChatMessage("You have a funny feeling like you're being followed.");
Expand All @@ -107,7 +127,7 @@ void testNotifyCollection() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(petName, null, false, false))
.extra(new PetNotificationData(petName, null, false, false, rarity, kc, luck))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand All @@ -134,7 +154,7 @@ void testNotifyLostExistingCollection() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(petName, null, false, true))
.extra(new PetNotificationData(petName, null, false, true, 1.0 / 200, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand All @@ -160,7 +180,7 @@ void testNotifyUntradeable() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(petName, null, false, null))
.extra(new PetNotificationData(petName, null, false, null, 1.0 / 200, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand All @@ -186,7 +206,7 @@ void testNotifyUntradeableDuplicate() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(petName, null, true, true))
.extra(new PetNotificationData(petName, null, true, true, 1.0 / 200, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand All @@ -209,7 +229,7 @@ void testNotifyUntradeableNotARealPet() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, false, null))
.extra(new PetNotificationData(null, null, false, null, null, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand Down Expand Up @@ -248,7 +268,7 @@ void testNotifyMultipleSameName() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(petName, "50 killcount", false, null))
.extra(new PetNotificationData(petName, "50 killcount", false, null, 1.0 / 200, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand Down Expand Up @@ -286,7 +306,7 @@ void testNotifyClan() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(petName, "50 killcount", false, null))
.extra(new PetNotificationData(petName, "50 killcount", false, null, 1.0 / 200, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.thumbnailUrl(ItemUtils.getItemImageUrl(itemId))
.type(NotificationType.PET)
Expand All @@ -311,7 +331,7 @@ void testNotifyOverride() {
"https://example.com",
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, false, null))
.extra(new PetNotificationData(null, null, false, null, null, null, null))
.text(buildTemplate(PLAYER_NAME + " has a funny feeling like they're being followed"))
.type(NotificationType.PET)
.build()
Expand Down Expand Up @@ -356,7 +376,7 @@ void testNotifySeasonal() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, false, null))
.extra(new PetNotificationData(null, null, false, null, null, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.type(NotificationType.PET)
.build()
Expand Down Expand Up @@ -392,7 +412,7 @@ void testNotifyIrrelevantNameIgnore() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, false, null))
.extra(new PetNotificationData(null, null, false, null, null, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.type(NotificationType.PET)
.build()
Expand All @@ -415,7 +435,7 @@ void testNotifyNameAllowList() {
PRIMARY_WEBHOOK_URL,
false,
NotificationBody.builder()
.extra(new PetNotificationData(null, null, false, null))
.extra(new PetNotificationData(null, null, false, null, null, null, null))
.text(buildTemplate(PLAYER_NAME + " got a pet"))
.type(NotificationType.PET)
.build());
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/dinkplugin/util/SerializedLootTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dinkplugin.util;

import com.google.gson.Gson;
import net.runelite.api.ItemID;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SerializedLootTest {

@Test
void deserialize() {
String json = "{\"type\":\"NPC\",\"name\":\"Bryophyta\",\"kills\":16,\"first\":1708910620551,\"last\":1708983457752,\"drops\":[23182,16,532,16,1618,5,1620,5,2363,2,560,100,1079,2,890,100,1303,1,1113,2,1147,2,562,200,1124,5,1289,4,563,200]}";
SerializedLoot lootRecord = new Gson().fromJson(json, SerializedLoot.class);
assertEquals(16, lootRecord.getKills());
assertEquals(2, lootRecord.getQuantity(ItemID.RUNE_CHAINBODY));
}

}
Loading