slayer plugin: better support multikills

This observes deaths of tagged npcs each tick and will use those if
available instead of assuming one kill per xpdrop
This commit is contained in:
Adam
2020-07-01 14:30:03 -04:00
parent 6dc6e78f51
commit 67030e38a4
2 changed files with 132 additions and 16 deletions

View File

@@ -30,11 +30,14 @@ import com.google.inject.Provides;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import static java.lang.Integer.max;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -44,18 +47,22 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Actor;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.Hitsplat;
import net.runelite.api.ItemID;
import net.runelite.api.MessageNode;
import net.runelite.api.NPC;
import net.runelite.api.NPCComposition;
import static net.runelite.api.Skill.SLAYER;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ActorDeath;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.HitsplatApplied;
import net.runelite.api.events.NpcDespawned;
import net.runelite.api.events.NpcSpawned;
import net.runelite.api.events.StatChanged;
@@ -174,6 +181,10 @@ public class SlayerPlugin extends Plugin
@Getter(AccessLevel.PACKAGE)
private List<NPC> highlightedTargets = new ArrayList<>();
private final Set<NPC> taggedNpcs = new HashSet<>();
private int taggedNpcsDiedPrevTick;
private int taggedNpcsDiedThisTick;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private int amount;
@@ -237,6 +248,7 @@ public class SlayerPlugin extends Plugin
overlayManager.remove(targetMinimapOverlay);
removeCounter();
highlightedTargets.clear();
taggedNpcs.clear();
cachedXp = -1;
chatCommandManager.unregisterCommand(TASK_COMMAND_STRING);
@@ -260,6 +272,7 @@ public class SlayerPlugin extends Plugin
amount = 0;
loginFlag = true;
highlightedTargets.clear();
taggedNpcs.clear();
break;
case LOGGED_IN:
if (config.amount() != -1
@@ -299,6 +312,7 @@ public class SlayerPlugin extends Plugin
public void onNpcDespawned(NpcDespawned npcDespawned)
{
NPC npc = npcDespawned.getNpc();
taggedNpcs.remove(npc);
highlightedTargets.remove(npc);
}
@@ -391,6 +405,9 @@ public class SlayerPlugin extends Plugin
removeCounter();
}
}
taggedNpcsDiedPrevTick = taggedNpcsDiedThisTick;
taggedNpcsDiedThisTick = 0;
}
@Subscribe
@@ -541,20 +558,49 @@ public class SlayerPlugin extends Plugin
return;
}
final Task task = Task.getTask(taskName);
// null tasks are technically valid, it only means they arent explicitly defined in the Task enum
// allow them through so that if there is a task capture failure the counter will still work
final int taskKillExp = task != null ? task.getExpectedKillExp() : 0;
// Only count exp gain as a kill if the task either has no expected exp for a kill, or if the exp gain is equal
// to the expected exp gain for the task.
if (taskKillExp == 0 || taskKillExp == slayerExp - cachedXp)
{
killedOne();
}
final int delta = slayerExp - cachedXp;
cachedXp = slayerExp;
log.debug("Slayer xp change delta: {}, killed npcs: {}", delta, taggedNpcsDiedPrevTick);
final Task task = Task.getTask(taskName);
if (task != null && task.getExpectedKillExp() > 0)
{
// Only decrement a kill if the xp drop matches the expected drop. This is just for Tzhaar tasks.
if (task.getExpectedKillExp() == delta)
{
killed(1);
}
}
else
{
// This is at least one kill, but if we observe multiple tagged NPCs dieing on the previous tick, count them
// instead.
killed(max(taggedNpcsDiedPrevTick, 1));
}
}
@Subscribe
public void onHitsplatApplied(HitsplatApplied hitsplatApplied)
{
Actor actor = hitsplatApplied.getActor();
Hitsplat hitsplat = hitsplatApplied.getHitsplat();
if (hitsplat.getHitsplatType() == Hitsplat.HitsplatType.DAMAGE_ME && highlightedTargets.contains(actor))
{
// If the actor is in highlightedTargets it must be an NPC and also a task assignment
taggedNpcs.add((NPC) actor);
}
}
@Subscribe
public void onActorDeath(ActorDeath actorDeath)
{
Actor actor = actorDeath.getActor();
if (taggedNpcs.contains(actor))
{
log.debug("Tagged NPC {} has died", actor.getName());
++taggedNpcsDiedThisTick;
}
}
@Subscribe
@@ -576,16 +622,17 @@ public class SlayerPlugin extends Plugin
}
@VisibleForTesting
void killedOne()
void killed(int amt)
{
if (amount == 0)
{
return;
}
amount--;
amount -= amt;
if (doubleTroubleExtraKill())
{
assert amt == 1;
amount--;
}

View File

@@ -28,19 +28,25 @@ import com.google.inject.Guice;
import com.google.inject.testing.fieldbinder.Bind;
import com.google.inject.testing.fieldbinder.BoundFieldModule;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledExecutorService;
import javax.inject.Inject;
import net.runelite.api.ChatMessageType;
import static net.runelite.api.ChatMessageType.GAMEMESSAGE;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.Hitsplat;
import net.runelite.api.MessageNode;
import net.runelite.api.NPC;
import net.runelite.api.NPCComposition;
import net.runelite.api.Player;
import net.runelite.api.Skill;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.events.ActorDeath;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.HitsplatApplied;
import net.runelite.api.events.StatChanged;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
@@ -771,7 +777,7 @@ public class SlayerPluginTest
slayerPlugin.onChatMessage(chatMessage);
assertEquals("Suqahs", slayerPlugin.getTaskName());
slayerPlugin.killedOne();
slayerPlugin.killed(1);
assertEquals(30, slayerPlugin.getAmount());
}
@@ -863,4 +869,67 @@ public class SlayerPluginTest
verify(infoBoxManager, never()).addInfoBox(any());
}
@Test
public void testMultikill()
{
final Player player = mock(Player.class);
when(player.getLocalLocation()).thenReturn(new LocalPoint(0, 0));
when(client.getLocalPlayer()).thenReturn(player);
// Setup xp cache
StatChanged statChanged = new StatChanged(
Skill.SLAYER,
0,
1,
1
);
slayerPlugin.onStatChanged(statChanged);
NPCComposition npcComposition = mock(NPCComposition.class);
when(npcComposition.getActions()).thenReturn(new String[]{"Attack"});
NPC npc1 = mock(NPC.class);
when(npc1.getName()).thenReturn("Suqah");
when(npc1.getTransformedComposition()).thenReturn(npcComposition);
NPC npc2 = mock(NPC.class);
when(npc2.getName()).thenReturn("Suqah");
when(npc2.getTransformedComposition()).thenReturn(npcComposition);
when(client.getNpcs()).thenReturn(Arrays.asList(npc1, npc2));
// Set task
Widget npcDialog = mock(Widget.class);
when(npcDialog.getText()).thenReturn(TASK_NEW);
when(client.getWidget(WidgetInfo.DIALOG_NPC_TEXT)).thenReturn(npcDialog);
slayerPlugin.onGameTick(new GameTick());
// Damage both npcs
Hitsplat hitsplat = new Hitsplat(Hitsplat.HitsplatType.DAMAGE_ME, 1, 1);
HitsplatApplied hitsplatApplied = new HitsplatApplied();
hitsplatApplied.setHitsplat(hitsplat);
hitsplatApplied.setActor(npc1);
slayerPlugin.onHitsplatApplied(hitsplatApplied);
hitsplatApplied.setActor(npc2);
slayerPlugin.onHitsplatApplied(hitsplatApplied);
// Kill both npcs
slayerPlugin.onActorDeath(new ActorDeath(npc1));
slayerPlugin.onActorDeath(new ActorDeath(npc2));
slayerPlugin.onGameTick(new GameTick());
statChanged = new StatChanged(
Skill.SLAYER,
105,
2,
2
);
slayerPlugin.onStatChanged(statChanged);
assertEquals("Suqahs", slayerPlugin.getTaskName());
assertEquals(229, slayerPlugin.getAmount()); // 2 kills
}
}