This commit is contained in:
therealunull
2020-12-16 14:27:14 -05:00
parent 665259b5e5
commit 3be6ba5cc0
10 changed files with 1948 additions and 2 deletions

View File

@@ -67,7 +67,7 @@ public interface NPC extends Actor
* @return the transformed NPC
*/
@Nullable
NPCComposition getTransformedDefinition();
NPCComposition getTransformedComposition();
void onDefinitionChanged(NPCComposition composition);
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright (c) 2017, Seth <Sethtroll3@gmail.com>
* Copyright (c) 2018, Shaun Dreclin <shaundreclin@gmail.com>
* 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.slayer;
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.Units;
@ConfigGroup("slayer")
public interface SlayerConfig extends Config
{
@ConfigItem(
position = 1,
keyName = "infobox",
name = "Task InfoBox",
description = "Display task information in an InfoBox"
)
default boolean showInfobox()
{
return true;
}
@ConfigItem(
position = 2,
keyName = "itemoverlay",
name = "Count on Items",
description = "Display task count remaining on slayer items"
)
default boolean showItemOverlay()
{
return true;
}
@ConfigItem(
position = 3,
keyName = "superiornotification",
name = "Superior foe notification",
description = "Toggles notifications on superior foe encounters"
)
default boolean showSuperiorNotification()
{
return true;
}
@ConfigItem(
position = 4,
keyName = "statTimeout",
name = "InfoBox Expiry",
description = "Set the time until the InfoBox expires"
)
@Units(Units.MINUTES)
default int statTimeout()
{
return 5;
}
@ConfigItem(
position = 5,
keyName = "highlightTargets",
name = "Highlight Targets",
description = "Highlight monsters you can kill for your current slayer assignment"
)
default boolean highlightTargets()
{
return false;
}
@ConfigItem(
position = 6,
keyName = "targetColor",
name = "Target Color",
description = "Color of the highlighted targets"
)
default Color getTargetColor()
{
return Color.RED;
}
@ConfigItem(
position = 7,
keyName = "weaknessPrompt",
name = "Show Monster Weakness",
description = "Show an overlay on a monster when it is weak enough to finish off (Only Lizards, Gargoyles & Rockslugs)"
)
default boolean weaknessPrompt()
{
return true;
}
@ConfigItem(
position = 8,
keyName = "taskCommand",
name = "Task Command",
description = "Configures whether the slayer task command is enabled<br> !task"
)
default boolean taskCommand()
{
return true;
}
// Stored data
@ConfigItem(
keyName = "taskName",
name = "",
description = "",
hidden = true
)
default String taskName()
{
return "";
}
@ConfigItem(
keyName = "taskName",
name = "",
description = ""
)
void taskName(String key);
@ConfigItem(
keyName = "amount",
name = "",
description = "",
hidden = true
)
default int amount()
{
return -1;
}
@ConfigItem(
keyName = "amount",
name = "",
description = ""
)
void amount(int amt);
@ConfigItem(
keyName = "initialAmount",
name = "",
description = "",
hidden = true
)
default int initialAmount()
{
return -1;
}
@ConfigItem(
keyName = "initialAmount",
name = "",
description = ""
)
void initialAmount(int initialAmount);
@ConfigItem(
keyName = "taskLocation",
name = "",
description = "",
hidden = true
)
default String taskLocation()
{
return "";
}
@ConfigItem(
keyName = "taskLocation",
name = "",
description = ""
)
void taskLocation(String key);
@ConfigItem(
keyName = "streak",
name = "",
description = "",
hidden = true
)
default int streak()
{
return -1;
}
@ConfigItem(
keyName = "streak",
name = "",
description = ""
)
void streak(int streak);
@ConfigItem(
keyName = "points",
name = "",
description = "",
hidden = true
)
default int points()
{
return -1;
}
@ConfigItem(
keyName = "points",
name = "",
description = ""
)
void points(int points);
@ConfigItem(
keyName = "expeditious",
name = "",
description = "",
hidden = true
)
default int expeditious()
{
return -1;
}
@ConfigItem(
keyName = "expeditious",
name = "",
description = ""
)
void expeditious(int expeditious);
@ConfigItem(
keyName = "slaughter",
name = "",
description = "",
hidden = true
)
default int slaughter()
{
return -1;
}
@ConfigItem(
keyName = "slaughter",
name = "",
description = ""
)
void slaughter(int slaughter);
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (c) 2017, Seth <Sethtroll3@gmail.com>
* 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.slayer;
import com.google.common.collect.ImmutableSet;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.Set;
import javax.inject.Inject;
import net.runelite.api.ItemID;
import net.runelite.api.widgets.WidgetItem;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.overlay.WidgetItemOverlay;
import net.runelite.client.ui.overlay.components.TextComponent;
class SlayerOverlay extends WidgetItemOverlay
{
private final static Set<Integer> SLAYER_JEWELRY = ImmutableSet.of(
ItemID.SLAYER_RING_1,
ItemID.SLAYER_RING_2,
ItemID.SLAYER_RING_3,
ItemID.SLAYER_RING_4,
ItemID.SLAYER_RING_5,
ItemID.SLAYER_RING_6,
ItemID.SLAYER_RING_7,
ItemID.SLAYER_RING_8
);
private final static Set<Integer> ALL_SLAYER_ITEMS = ImmutableSet.of(
ItemID.SLAYER_HELMET,
ItemID.SLAYER_HELMET_I,
ItemID.BLACK_SLAYER_HELMET,
ItemID.BLACK_SLAYER_HELMET_I,
ItemID.GREEN_SLAYER_HELMET,
ItemID.GREEN_SLAYER_HELMET_I,
ItemID.PURPLE_SLAYER_HELMET,
ItemID.PURPLE_SLAYER_HELMET_I,
ItemID.RED_SLAYER_HELMET,
ItemID.RED_SLAYER_HELMET_I,
ItemID.TURQUOISE_SLAYER_HELMET,
ItemID.TURQUOISE_SLAYER_HELMET_I,
ItemID.TWISTED_SLAYER_HELMET,
ItemID.TWISTED_SLAYER_HELMET_I,
ItemID.HYDRA_SLAYER_HELMET,
ItemID.HYDRA_SLAYER_HELMET_I,
ItemID.SLAYER_RING_ETERNAL,
ItemID.ENCHANTED_GEM,
ItemID.ETERNAL_GEM,
ItemID.BRACELET_OF_SLAUGHTER,
ItemID.EXPEDITIOUS_BRACELET,
ItemID.SLAYER_RING_1,
ItemID.SLAYER_RING_2,
ItemID.SLAYER_RING_3,
ItemID.SLAYER_RING_4,
ItemID.SLAYER_RING_5,
ItemID.SLAYER_RING_6,
ItemID.SLAYER_RING_7,
ItemID.SLAYER_RING_8
);
private final SlayerConfig config;
private final SlayerPlugin plugin;
@Inject
private SlayerOverlay(SlayerPlugin plugin, SlayerConfig config)
{
this.plugin = plugin;
this.config = config;
showOnInventory();
showOnEquipment();
}
@Override
public void renderItemOverlay(Graphics2D graphics, int itemId, WidgetItem itemWidget)
{
if (!ALL_SLAYER_ITEMS.contains(itemId))
{
return;
}
if (!config.showItemOverlay())
{
return;
}
int amount = plugin.getAmount();
if (amount <= 0)
{
return;
}
int slaughterCount = plugin.getSlaughterChargeCount();
int expeditiousCount = plugin.getExpeditiousChargeCount();
graphics.setFont(FontManager.getRunescapeSmallFont());
final Rectangle bounds = itemWidget.getCanvasBounds();
final TextComponent textComponent = new TextComponent();
switch (itemId)
{
case ItemID.EXPEDITIOUS_BRACELET:
textComponent.setText(String.valueOf(expeditiousCount));
break;
case ItemID.BRACELET_OF_SLAUGHTER:
textComponent.setText(String.valueOf(slaughterCount));
break;
default:
textComponent.setText(String.valueOf(amount));
break;
}
// Draw the counter in the bottom left for equipment, and top left for jewelry
textComponent.setPosition(new Point(bounds.x - 1, bounds.y - 1 + (SLAYER_JEWELRY.contains(itemId)
? bounds.height
: graphics.getFontMetrics().getHeight())));
textComponent.render(graphics);
}
}

View File

@@ -0,0 +1,903 @@
/*
* Copyright (c) 2017, Tyler <https://github.com/tylerthardy>
* Copyright (c) 2018, Shaun Dreclin <shaundreclin@gmail.com>
* 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.slayer;
import com.google.common.annotations.VisibleForTesting;
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;
import javax.inject.Inject;
import joptsimple.internal.Strings;
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;
import net.runelite.api.vars.SlayerUnlock;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetInfo;
import net.runelite.client.Notifier;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.chat.ChatColorType;
import net.runelite.client.chat.ChatCommandManager;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ChatInput;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.Text;
import net.runelite.http.api.chat.ChatClient;
@PluginDescriptor(
name = "Slayer",
description = "Show additional slayer task related information",
tags = {"combat", "notifications", "overlay", "tasks"}
)
@Slf4j
public class SlayerPlugin extends Plugin
{
//Chat messages
private static final Pattern CHAT_GEM_PROGRESS_MESSAGE = Pattern.compile("^(?:You're assigned to kill|You have received a new Slayer assignment from .*:) (?:[Tt]he )?(?<name>.+?)(?: (?:in|on|south of) (?:the )?(?<location>[^;]+))?(?:; only | \\()(?<amount>\\d+)(?: more to go\\.|\\))$");
private static final String CHAT_GEM_COMPLETE_MESSAGE = "You need something new to hunt.";
private static final Pattern CHAT_COMPLETE_MESSAGE = Pattern.compile("(?:\\d+,)*\\d+");
private static final String CHAT_CANCEL_MESSAGE = "Your task has been cancelled.";
private static final String CHAT_CANCEL_MESSAGE_JAD = "You no longer have a slayer task as you left the fight cave.";
private static final String CHAT_CANCEL_MESSAGE_ZUK = "You no longer have a slayer task as you left the Inferno.";
private static final String CHAT_SUPERIOR_MESSAGE = "A superior foe has appeared...";
private static final String CHAT_BRACELET_SLAUGHTER = "Your bracelet of slaughter prevents your slayer";
private static final Pattern CHAT_BRACELET_SLAUGHTER_REGEX = Pattern.compile("Your bracelet of slaughter prevents your slayer count from decreasing. It has (\\d{1,2}) charges? left\\.");
private static final String CHAT_BRACELET_EXPEDITIOUS = "Your expeditious bracelet helps you progress your";
private static final Pattern CHAT_BRACELET_EXPEDITIOUS_REGEX = Pattern.compile("Your expeditious bracelet helps you progress your slayer (?:task )?faster. It has (\\d{1,2}) charges? left\\.");
private static final String CHAT_BRACELET_SLAUGHTER_CHARGE = "Your bracelet of slaughter has ";
private static final Pattern CHAT_BRACELET_SLAUGHTER_CHARGE_REGEX = Pattern.compile("Your bracelet of slaughter has (\\d{1,2}) charges? left\\.");
private static final String CHAT_BRACELET_EXPEDITIOUS_CHARGE = "Your expeditious bracelet has ";
private static final Pattern CHAT_BRACELET_EXPEDITIOUS_CHARGE_REGEX = Pattern.compile("Your expeditious bracelet has (\\d{1,2}) charges? left\\.");
private static final Pattern COMBAT_BRACELET_TASK_UPDATE_MESSAGE = Pattern.compile("^You still need to kill (\\d+) monsters to complete your current Slayer assignment");
//NPC messages
private static final Pattern NPC_ASSIGN_MESSAGE = Pattern.compile(".*(?:Your new task is to kill|You are to bring balance to)\\s*(?<amount>\\d+) (?<name>.+?)(?: (?:in|on|south of) (?:the )?(?<location>.+))?\\.");
private static final Pattern NPC_ASSIGN_BOSS_MESSAGE = Pattern.compile("^(?:Excellent\\. )?You're now assigned to (?:kill|bring balance to) (?:the )?(.*) (\\d+) times.*Your reward point tally is (.*)\\.$");
private static final Pattern NPC_ASSIGN_FIRST_MESSAGE = Pattern.compile("^We'll start you off (?:hunting|bringing balance to) (.*), you'll need to kill (\\d*) of them\\.$");
private static final Pattern NPC_CURRENT_MESSAGE = Pattern.compile("^You're (?:still(?: meant to be)?|currently assigned to) (?:hunting|bringing balance to|kill|bring balance to|slaying) (?<name>.+?)(?: (?:in|on|south of) (?:the )?(?<location>.+))?(?:, with|; (?:you have|only)) (?<amount>\\d+)(?: more)? to go\\..*");
//Reward UI
private static final Pattern REWARD_POINTS = Pattern.compile("Reward points: ((?:\\d+,)*\\d+)");
private static final int GROTESQUE_GUARDIANS_REGION = 6727;
private static final int EXPEDITIOUS_CHARGE = 30;
private static final int SLAUGHTER_CHARGE = 30;
// Chat Command
private static final String TASK_COMMAND_STRING = "!task";
private static final Pattern TASK_STRING_VALIDATION = Pattern.compile("[^a-zA-Z0-9' -]");
private static final int TASK_STRING_MAX_LENGTH = 50;
@Inject
private Client client;
@Inject
private SlayerConfig config;
@Inject
private OverlayManager overlayManager;
@Inject
private SlayerOverlay overlay;
@Inject
private InfoBoxManager infoBoxManager;
@Inject
private ItemManager itemManager;
@Inject
private Notifier notifier;
@Inject
private ClientThread clientThread;
@Inject
private TargetClickboxOverlay targetClickboxOverlay;
@Inject
private TargetWeaknessOverlay targetWeaknessOverlay;
@Inject
private TargetMinimapOverlay targetMinimapOverlay;
@Inject
private ChatMessageManager chatMessageManager;
@Inject
private ChatCommandManager chatCommandManager;
@Inject
private ScheduledExecutorService executor;
@Inject
private ChatClient chatClient;
@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;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private int initialAmount;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private String taskLocation;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private int expeditiousChargeCount;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private int slaughterChargeCount;
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private String taskName;
private TaskCounter counter;
private int cachedXp = -1;
private Instant infoTimer;
private boolean loginFlag;
private final List<String> targetNames = new ArrayList<>();
@Override
protected void startUp() throws Exception
{
overlayManager.add(overlay);
overlayManager.add(targetClickboxOverlay);
overlayManager.add(targetWeaknessOverlay);
overlayManager.add(targetMinimapOverlay);
if (client.getGameState() == GameState.LOGGED_IN)
{
cachedXp = client.getSkillExperience(SLAYER);
if (config.amount() != -1
&& !config.taskName().isEmpty())
{
setExpeditiousChargeCount(config.expeditious());
setSlaughterChargeCount(config.slaughter());
clientThread.invoke(() -> setTask(config.taskName(), config.amount(), config.initialAmount(), config.taskLocation(), false));
}
}
chatCommandManager.registerCommandAsync(TASK_COMMAND_STRING, this::taskLookup, this::taskSubmit);
}
@Override
protected void shutDown() throws Exception
{
overlayManager.remove(overlay);
overlayManager.remove(targetClickboxOverlay);
overlayManager.remove(targetWeaknessOverlay);
overlayManager.remove(targetMinimapOverlay);
removeCounter();
highlightedTargets.clear();
taggedNpcs.clear();
cachedXp = -1;
chatCommandManager.unregisterCommand(TASK_COMMAND_STRING);
}
@Provides
SlayerConfig provideSlayerConfig(ConfigManager configManager)
{
return configManager.getConfig(SlayerConfig.class);
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
switch (event.getGameState())
{
case HOPPING:
case LOGGING_IN:
cachedXp = -1;
taskName = "";
amount = 0;
loginFlag = true;
highlightedTargets.clear();
taggedNpcs.clear();
break;
case LOGGED_IN:
if (config.amount() != -1
&& !config.taskName().isEmpty()
&& loginFlag)
{
setExpeditiousChargeCount(config.expeditious());
setSlaughterChargeCount(config.slaughter());
setTask(config.taskName(), config.amount(), config.initialAmount(), config.taskLocation(), false);
loginFlag = false;
}
break;
}
}
private void save()
{
config.amount(amount);
config.initialAmount(initialAmount);
config.taskName(taskName);
config.taskLocation(taskLocation);
config.expeditious(expeditiousChargeCount);
config.slaughter(slaughterChargeCount);
}
@Subscribe
public void onNpcSpawned(NpcSpawned npcSpawned)
{
NPC npc = npcSpawned.getNpc();
if (isTarget(npc))
{
highlightedTargets.add(npc);
}
}
@Subscribe
public void onNpcDespawned(NpcDespawned npcDespawned)
{
NPC npc = npcDespawned.getNpc();
taggedNpcs.remove(npc);
highlightedTargets.remove(npc);
}
@Subscribe
public void onGameTick(GameTick tick)
{
Widget npcDialog = client.getWidget(WidgetInfo.DIALOG_NPC_TEXT);
if (npcDialog != null)
{
String npcText = Text.sanitizeMultilineText(npcDialog.getText()); //remove color and linebreaks
final Matcher mAssign = NPC_ASSIGN_MESSAGE.matcher(npcText); // amount, name, (location)
final Matcher mAssignFirst = NPC_ASSIGN_FIRST_MESSAGE.matcher(npcText); // name, number
final Matcher mAssignBoss = NPC_ASSIGN_BOSS_MESSAGE.matcher(npcText); // name, number, points
final Matcher mCurrent = NPC_CURRENT_MESSAGE.matcher(npcText); // name, (location), amount
if (mAssign.find())
{
String name = mAssign.group("name");
int amount = Integer.parseInt(mAssign.group("amount"));
String location = mAssign.group("location");
setTask(name, amount, amount, location);
}
else if (mAssignFirst.find())
{
int amount = Integer.parseInt(mAssignFirst.group(2));
setTask(mAssignFirst.group(1), amount, amount);
}
else if (mAssignBoss.find())
{
int amount = Integer.parseInt(mAssignBoss.group(2));
setTask(mAssignBoss.group(1), amount, amount);
int points = Integer.parseInt(mAssignBoss.group(3).replaceAll(",", ""));
config.points(points);
}
else if (mCurrent.find())
{
String name = mCurrent.group("name");
int amount = Integer.parseInt(mCurrent.group("amount"));
String location = mCurrent.group("location");
setTask(name, amount, initialAmount, location);
}
}
Widget braceletBreakWidget = client.getWidget(WidgetInfo.DIALOG_SPRITE_TEXT);
if (braceletBreakWidget != null)
{
String braceletText = Text.removeTags(braceletBreakWidget.getText()); //remove color and linebreaks
if (braceletText.contains("bracelet of slaughter"))
{
slaughterChargeCount = SLAUGHTER_CHARGE;
config.slaughter(slaughterChargeCount);
}
else if (braceletText.contains("expeditious bracelet"))
{
expeditiousChargeCount = EXPEDITIOUS_CHARGE;
config.expeditious(expeditiousChargeCount);
}
}
Widget rewardsBarWidget = client.getWidget(WidgetInfo.SLAYER_REWARDS_TOPBAR);
if (rewardsBarWidget != null)
{
for (Widget w : rewardsBarWidget.getDynamicChildren())
{
Matcher mPoints = REWARD_POINTS.matcher(w.getText());
if (mPoints.find())
{
final int prevPoints = config.points();
int points = Integer.parseInt(mPoints.group(1).replaceAll(",", ""));
if (prevPoints != points)
{
config.points(points);
removeCounter();
addCounter();
}
break;
}
}
}
if (infoTimer != null && config.statTimeout() != 0)
{
Duration timeSinceInfobox = Duration.between(infoTimer, Instant.now());
Duration statTimeout = Duration.ofMinutes(config.statTimeout());
if (timeSinceInfobox.compareTo(statTimeout) >= 0)
{
removeCounter();
}
}
taggedNpcsDiedPrevTick = taggedNpcsDiedThisTick;
taggedNpcsDiedThisTick = 0;
}
@Subscribe
public void onChatMessage(ChatMessage event)
{
if (event.getType() != ChatMessageType.GAMEMESSAGE && event.getType() != ChatMessageType.SPAM)
{
return;
}
String chatMsg = Text.removeTags(event.getMessage()); //remove color and linebreaks
if (chatMsg.startsWith(CHAT_BRACELET_SLAUGHTER))
{
Matcher mSlaughter = CHAT_BRACELET_SLAUGHTER_REGEX.matcher(chatMsg);
amount++;
slaughterChargeCount = mSlaughter.find() ? Integer.parseInt(mSlaughter.group(1)) : SLAUGHTER_CHARGE;
config.slaughter(slaughterChargeCount);
}
if (chatMsg.startsWith(CHAT_BRACELET_EXPEDITIOUS))
{
Matcher mExpeditious = CHAT_BRACELET_EXPEDITIOUS_REGEX.matcher(chatMsg);
amount--;
expeditiousChargeCount = mExpeditious.find() ? Integer.parseInt(mExpeditious.group(1)) : EXPEDITIOUS_CHARGE;
config.expeditious(expeditiousChargeCount);
}
if (chatMsg.startsWith(CHAT_BRACELET_EXPEDITIOUS_CHARGE))
{
Matcher mExpeditious = CHAT_BRACELET_EXPEDITIOUS_CHARGE_REGEX.matcher(chatMsg);
if (!mExpeditious.find())
{
return;
}
expeditiousChargeCount = Integer.parseInt(mExpeditious.group(1));
config.expeditious(expeditiousChargeCount);
}
if (chatMsg.startsWith(CHAT_BRACELET_SLAUGHTER_CHARGE))
{
Matcher mSlaughter = CHAT_BRACELET_SLAUGHTER_CHARGE_REGEX.matcher(chatMsg);
if (!mSlaughter.find())
{
return;
}
slaughterChargeCount = Integer.parseInt(mSlaughter.group(1));
config.slaughter(slaughterChargeCount);
}
if (chatMsg.startsWith("You've completed") && (chatMsg.contains("Slayer master") || chatMsg.contains("Slayer Master")))
{
Matcher mComplete = CHAT_COMPLETE_MESSAGE.matcher(chatMsg);
List<String> matches = new ArrayList<>();
while (mComplete.find())
{
matches.add(mComplete.group(0).replaceAll(",", ""));
}
int streak = -1, points = -1;
switch (matches.size())
{
case 0:
streak = 1;
break;
case 1:
streak = Integer.parseInt(matches.get(0));
break;
case 3:
streak = Integer.parseInt(matches.get(0));
points = Integer.parseInt(matches.get(2));
break;
default:
log.warn("Unreachable default case for message ending in '; return to Slayer master'");
}
if (streak != -1)
{
config.streak(streak);
}
if (points != -1)
{
config.points(points);
}
setTask("", 0, 0);
return;
}
if (chatMsg.equals(CHAT_GEM_COMPLETE_MESSAGE) || chatMsg.equals(CHAT_CANCEL_MESSAGE) || chatMsg.equals(CHAT_CANCEL_MESSAGE_JAD) || chatMsg.equals(CHAT_CANCEL_MESSAGE_ZUK))
{
setTask("", 0, 0);
return;
}
if (config.showSuperiorNotification() && chatMsg.equals(CHAT_SUPERIOR_MESSAGE))
{
notifier.notify(CHAT_SUPERIOR_MESSAGE);
return;
}
Matcher mProgress = CHAT_GEM_PROGRESS_MESSAGE.matcher(chatMsg);
if (mProgress.find())
{
String name = mProgress.group("name");
int gemAmount = Integer.parseInt(mProgress.group("amount"));
String location = mProgress.group("location");
setTask(name, gemAmount, initialAmount, location);
return;
}
final Matcher bracerProgress = COMBAT_BRACELET_TASK_UPDATE_MESSAGE.matcher(chatMsg);
if (bracerProgress.find())
{
final int taskAmount = Integer.parseInt(bracerProgress.group(1));
setTask(taskName, taskAmount, initialAmount);
// Avoid race condition (combat brace message goes through first before XP drop)
amount++;
}
}
@Subscribe
public void onStatChanged(StatChanged statChanged)
{
if (statChanged.getSkill() != SLAYER)
{
return;
}
int slayerExp = statChanged.getXp();
if (slayerExp <= cachedXp)
{
return;
}
if (cachedXp == -1)
{
// this is the initial xp sent on login
cachedXp = slayerExp;
return;
}
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
private void onConfigChanged(ConfigChanged event)
{
if (!event.getGroup().equals("slayer") || !event.getKey().equals("infobox"))
{
return;
}
if (config.showInfobox())
{
clientThread.invoke(this::addCounter);
}
else
{
removeCounter();
}
}
@VisibleForTesting
void killed(int amt)
{
if (amount == 0)
{
return;
}
amount -= amt;
if (doubleTroubleExtraKill())
{
assert amt == 1;
amount--;
}
config.amount(amount); // save changed value
if (!config.showInfobox())
{
return;
}
// add and update counter, set timer
addCounter();
counter.setCount(amount);
infoTimer = Instant.now();
}
private boolean doubleTroubleExtraKill()
{
return WorldPoint.fromLocalInstance(client, client.getLocalPlayer().getLocalLocation()).getRegionID() == GROTESQUE_GUARDIANS_REGION &&
SlayerUnlock.GROTESQUE_GUARDIAN_DOUBLE_COUNT.isEnabled(client);
}
private boolean isTarget(NPC npc)
{
if (targetNames.isEmpty())
{
return false;
}
String name = npc.getName();
if (name == null)
{
return false;
}
name = name.toLowerCase();
for (String target : targetNames)
{
if (name.contains(target))
{
NPCComposition composition = npc.getTransformedComposition();
if (composition != null)
{
List<String> actions = Arrays.asList(composition.getActions());
if (actions.contains("Attack") || actions.contains("Pick")) //Pick action is for zygomite-fungi
{
return true;
}
}
}
}
return false;
}
private void rebuildTargetNames(Task task)
{
targetNames.clear();
if (task != null)
{
Arrays.stream(task.getTargetNames())
.map(String::toLowerCase)
.forEach(targetNames::add);
targetNames.add(taskName.toLowerCase().replaceAll("s$", ""));
}
}
private void rebuildTargetList()
{
highlightedTargets.clear();
for (NPC npc : client.getNpcs())
{
if (isTarget(npc))
{
highlightedTargets.add(npc);
}
}
}
private void setTask(String name, int amt, int initAmt)
{
setTask(name, amt, initAmt, null);
}
private void setTask(String name, int amt, int initAmt, String location)
{
setTask(name, amt, initAmt, location, true);
}
private void setTask(String name, int amt, int initAmt, String location, boolean addCounter)
{
taskName = name;
amount = amt;
initialAmount = Math.max(amt, initAmt);
taskLocation = location;
save();
removeCounter();
if (addCounter)
{
infoTimer = Instant.now();
addCounter();
}
Task task = Task.getTask(name);
rebuildTargetNames(task);
rebuildTargetList();
}
private void addCounter()
{
if (!config.showInfobox() || counter != null || Strings.isNullOrEmpty(taskName))
{
return;
}
Task task = Task.getTask(taskName);
int itemSpriteId = ItemID.ENCHANTED_GEM;
if (task != null)
{
itemSpriteId = task.getItemSpriteId();
}
BufferedImage taskImg = itemManager.getImage(itemSpriteId);
String taskTooltip = ColorUtil.wrapWithColorTag("%s", new Color(255, 119, 0)) + "</br>";
if (taskLocation != null && !taskLocation.isEmpty())
{
taskTooltip += taskLocation + "</br>";
}
taskTooltip += ColorUtil.wrapWithColorTag("Pts:", Color.YELLOW)
+ " %s</br>"
+ ColorUtil.wrapWithColorTag("Streak:", Color.YELLOW)
+ " %s";
if (initialAmount > 0)
{
taskTooltip += "</br>"
+ ColorUtil.wrapWithColorTag("Start:", Color.YELLOW)
+ " " + initialAmount;
}
counter = new TaskCounter(taskImg, this, amount);
counter.setTooltip(String.format(taskTooltip, capsString(taskName), config.points(), config.streak()));
infoBoxManager.addInfoBox(counter);
}
private void removeCounter()
{
if (counter == null)
{
return;
}
infoBoxManager.removeInfoBox(counter);
counter = null;
}
void taskLookup(ChatMessage chatMessage, String message)
{
if (!config.taskCommand())
{
return;
}
ChatMessageType type = chatMessage.getType();
final String player;
if (type.equals(ChatMessageType.PRIVATECHATOUT))
{
player = client.getLocalPlayer().getName();
}
else
{
player = Text.removeTags(chatMessage.getName())
.replace('\u00A0', ' ');
}
net.runelite.http.api.chat.Task task;
try
{
task = chatClient.getTask(player);
}
catch (IOException ex)
{
log.debug("unable to lookup slayer task", ex);
return;
}
if (TASK_STRING_VALIDATION.matcher(task.getTask()).find() || task.getTask().length() > TASK_STRING_MAX_LENGTH ||
TASK_STRING_VALIDATION.matcher(task.getLocation()).find() || task.getLocation().length() > TASK_STRING_MAX_LENGTH ||
Task.getTask(task.getTask()) == null || !Task.LOCATIONS.contains(task.getLocation()))
{
log.debug("Validation failed for task name or location: {}", task);
return;
}
int killed = task.getInitialAmount() - task.getAmount();
StringBuilder sb = new StringBuilder();
sb.append(task.getTask());
if (!Strings.isNullOrEmpty(task.getLocation()))
{
sb.append(" (").append(task.getLocation()).append(")");
}
sb.append(": ");
if (killed < 0)
{
sb.append(task.getAmount()).append(" left");
}
else
{
sb.append(killed).append('/').append(task.getInitialAmount()).append(" killed");
}
String response = new ChatMessageBuilder()
.append(ChatColorType.NORMAL)
.append("Slayer Task: ")
.append(ChatColorType.HIGHLIGHT)
.append(sb.toString())
.build();
final MessageNode messageNode = chatMessage.getMessageNode();
messageNode.setRuneLiteFormatMessage(response);
chatMessageManager.update(messageNode);
client.refreshChat();
}
private boolean taskSubmit(ChatInput chatInput, String value)
{
if (Strings.isNullOrEmpty(taskName))
{
return false;
}
final String playerName = client.getLocalPlayer().getName();
executor.execute(() ->
{
try
{
chatClient.submitTask(playerName, capsString(taskName), amount, initialAmount, taskLocation);
}
catch (Exception ex)
{
log.warn("unable to submit slayer task", ex);
}
finally
{
chatInput.resume();
}
});
return true;
}
//Utils
private String capsString(String str)
{
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2018, James Swindle <wilingua@gmail.com>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* Copyright (c) 2018, Shaun Dreclin <shaundreclin@gmail.com>
* 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.slayer;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.util.List;
import javax.inject.Inject;
import net.runelite.api.NPC;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
public class TargetClickboxOverlay extends Overlay
{
private final SlayerConfig config;
private final SlayerPlugin plugin;
@Inject
TargetClickboxOverlay(SlayerConfig config, SlayerPlugin plugin)
{
this.config = config;
this.plugin = plugin;
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_SCENE);
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!config.highlightTargets())
{
return null;
}
List<NPC> targets = plugin.getHighlightedTargets();
for (NPC target : targets)
{
renderTargetOverlay(graphics, target, config.getTargetColor());
}
return null;
}
private void renderTargetOverlay(Graphics2D graphics, NPC actor, Color color)
{
Shape objectClickbox = actor.getConvexHull();
if (objectClickbox != null)
{
graphics.setColor(color);
graphics.setStroke(new BasicStroke(2));
graphics.draw(objectClickbox);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(objectClickbox);
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2018, James Swindle <wilingua@gmail.com>
* Copyright (c) 2018, Adam <Adam@sigterm.info>
* Copyright (c) 2018, Shaun Dreclin <shaundreclin@gmail.com>
* 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.slayer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.util.List;
import javax.inject.Inject;
import net.runelite.api.NPC;
import net.runelite.api.Point;
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;
public class TargetMinimapOverlay extends Overlay
{
private final SlayerConfig config;
private final SlayerPlugin plugin;
@Inject
TargetMinimapOverlay(SlayerConfig config, SlayerPlugin plugin)
{
this.config = config;
this.plugin = plugin;
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_WIDGETS);
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!config.highlightTargets())
{
return null;
}
List<NPC> targets = plugin.getHighlightedTargets();
for (NPC target : targets)
{
renderTargetOverlay(graphics, target, config.getTargetColor());
}
return null;
}
private void renderTargetOverlay(Graphics2D graphics, NPC actor, Color color)
{
Point minimapLocation = actor.getMinimapLocation();
if (minimapLocation != null)
{
OverlayUtil.renderMinimapLocation(graphics, minimapLocation, color);
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2018, Sam "Berry" Beresford <seb1g13@soton.ac.uk>
* 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.slayer;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.NPC;
import net.runelite.api.Perspective;
import net.runelite.api.Point;
import net.runelite.api.coords.LocalPoint;
import net.runelite.client.game.ItemManager;
import net.runelite.client.game.NPCManager;
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 TargetWeaknessOverlay extends Overlay
{
private final Client client;
private final SlayerConfig config;
private final SlayerPlugin plugin;
private final ItemManager itemManager;
private final NPCManager npcManager;
@Inject
private TargetWeaknessOverlay(Client client, SlayerConfig config, SlayerPlugin plugin, ItemManager itemManager, NPCManager npcManager)
{
this.client = client;
this.config = config;
this.plugin = plugin;
this.itemManager = itemManager;
this.npcManager = npcManager;
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.UNDER_WIDGETS);
}
@Override
public Dimension render(Graphics2D graphics)
{
final List<NPC> targets = plugin.getHighlightedTargets();
if (targets.isEmpty() || !config.weaknessPrompt())
{
return null;
}
final Task curTask = Task.getTask(plugin.getTaskName());
if (curTask == null || curTask.getWeaknessThreshold() < 0 || curTask.getWeaknessItem() < 0)
{
return null;
}
final int threshold = curTask.getWeaknessThreshold();
final BufferedImage image = itemManager.getImage(curTask.getWeaknessItem());
if (image == null)
{
return null;
}
for (NPC target : targets)
{
final int currentHealth = calculateHealth(target);
if (currentHealth >= 0 && currentHealth <= threshold)
{
renderTargetItem(graphics, target, image);
}
}
return null;
}
private int calculateHealth(NPC target)
{
// Based on OpponentInfoOverlay HP calculation
if (target == null || target.getName() == null)
{
return -1;
}
final int healthScale = target.getHealthScale();
final int healthRatio = target.getHealthRatio();
final Integer maxHealth = npcManager.getHealth(target.getId());
if (healthRatio < 0 || healthScale <= 0 || maxHealth == null)
{
return -1;
}
return (int)((maxHealth * healthRatio / healthScale) + 0.5f);
}
private void renderTargetItem(Graphics2D graphics, NPC actor, BufferedImage image)
{
final LocalPoint actorPosition = actor.getLocalLocation();
final int offset = actor.getLogicalHeight() + 40;
if (actorPosition == null || image == null)
{
return;
}
final Point imageLoc = Perspective.getCanvasImageLocation(client, actorPosition, image, offset);
if (imageLoc != null)
{
OverlayUtil.renderImageLocation(graphics, imageLoc, image);
}
}
}

View File

@@ -0,0 +1,292 @@
/*
* Copyright (c) 2017, Tyler <https://github.com/tylerthardy>
* Copyright (c) 2018, Shaun Dreclin <shaundreclin@gmail.com>
* 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.slayer;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.Getter;
import net.runelite.api.ItemID;
@Getter
enum Task
{
//<editor-fold desc="Enums">
ABERRANT_SPECTRES("Aberrant spectres", ItemID.ABERRANT_SPECTRE, "Spectre"),
ABYSSAL_DEMONS("Abyssal demons", ItemID.ABYSSAL_DEMON),
ABYSSAL_SIRE("Abyssal Sire", ItemID.ABYSSAL_ORPHAN),
ADAMANT_DRAGONS("Adamant dragons", ItemID.ADAMANT_DRAGON_MASK),
ALCHEMICAL_HYDRA("Alchemical Hydra", ItemID.IKKLE_HYDRA),
ANKOU("Ankou", ItemID.ANKOU_MASK),
AVIANSIES("Aviansies", ItemID.ENSOULED_AVIANSIE_HEAD),
BANDITS("Bandits", ItemID.BANDIT, "Bandit", "Black Heather", "Donny the Lad", "Speedy Keith"),
BANSHEES("Banshees", ItemID.BANSHEE),
BARROWS_BROTHERS("Barrows Brothers", ItemID.KARILS_COIF),
BASILISKS("Basilisks", ItemID.BASILISK),
BATS("Bats", ItemID.GIRAL_BAT_2, "Death wing"),
BEARS("Bears", ItemID.ENSOULED_BEAR_HEAD),
BIRDS("Birds", ItemID.FEATHER, "Chicken", "Rooster", "Terrorbird", "Seagull", "Vulture"),
BLACK_DEMONS("Black demons", ItemID.BLACK_DEMON_MASK),
BLACK_DRAGONS("Black dragons", ItemID.BLACK_DRAGON_MASK, "Baby black dragon"),
BLACK_KNIGHTS("Black Knights", ItemID.BLACK_FULL_HELM, "Black Knight"),
BLOODVELD("Bloodveld", ItemID.BLOODVELD),
BLUE_DRAGONS("Blue dragons", ItemID.BLUE_DRAGON_MASK, "Baby blue dragon"),
BRINE_RATS("Brine rats", ItemID.BRINE_RAT),
BRONZE_DRAGONS("Bronze dragons", ItemID.BRONZE_DRAGON_MASK),
CALLISTO("Callisto", ItemID.CALLISTO_CUB),
CATABLEPON("Catablepon", ItemID.LEFT_SKULL_HALF),
CAVE_BUGS("Cave bugs", ItemID.SWAMP_CAVE_BUG),
CAVE_CRAWLERS("Cave crawlers", ItemID.CAVE_CRAWLER, "Chasm crawler"),
CAVE_HORRORS("Cave horrors", ItemID.CAVE_HORROR, "Cave abomination"),
CAVE_KRAKEN("Cave kraken", ItemID.CAVE_KRAKEN),
CAVE_SLIMES("Cave slimes", ItemID.SWAMP_CAVE_SLIME),
CERBERUS("Cerberus", ItemID.HELLPUPPY),
CHAOS_DRUIDS("Chaos druids", ItemID.ELDER_CHAOS_HOOD, "Elder Chaos druid", "Chaos druid"),
CHAOS_ELEMENTAL("Chaos Elemental", ItemID.PET_CHAOS_ELEMENTAL),
CHAOS_FANATIC("Chaos Fanatic", ItemID.ANCIENT_STAFF),
COCKATRICE("Cockatrice", ItemID.COCKATRICE, "Cockathrice"),
COWS("Cows", ItemID.COW_MASK),
CRAWLING_HANDS("Crawling hands", ItemID.CRAWLING_HAND, "Crushing hand"),
CRAZY_ARCHAEOLOGIST("Crazy Archaeologists", ItemID.FEDORA, "Crazy Archaeologist"),
CROCODILES("Crocodiles", ItemID.SWAMP_LIZARD),
DAGANNOTH("Dagannoth", ItemID.DAGANNOTH),
DAGANNOTH_KINGS("Dagannoth Kings", ItemID.PET_DAGANNOTH_PRIME),
DARK_BEASTS("Dark beasts", ItemID.DARK_BEAST, "Night beast"),
DARK_WARRIORS("Dark warriors", ItemID.BLACK_MED_HELM, "Dark warrior"),
DERANGED_ARCHAEOLOGIST("Deranged Archaeologist", ItemID.ARCHAEOLOGISTS_DIARY),
DOGS("Dogs", ItemID.GUARD_DOG, "Jackal"),
DRAKES("Drakes", ItemID.DRAKE),
DUST_DEVILS("Dust devils", ItemID.DUST_DEVIL, "Choke devil"),
DWARVES("Dwarves", ItemID.DWARVEN_HELMET, "Dwarf", "Black Guard"),
EARTH_WARRIORS("Earth warriors", ItemID.BRONZE_FULL_HELM_T),
ELVES("Elves", ItemID.ELF, "Elf", "Iorwerth Warrior", "Iorwerth Archer"),
ENTS("Ents", ItemID.NICE_TREE, "Ent"),
FEVER_SPIDERS("Fever spiders", ItemID.FEVER_SPIDER),
FIRE_GIANTS("Fire giants", ItemID.FIRE_BATTLESTAFF),
FLESH_CRAWLERS("Fleshcrawlers", ItemID.ENSOULED_SCORPION_HEAD, "Flesh crawler"),
FOSSIL_ISLAND_WYVERNS("Fossil island wyverns", ItemID.FOSSIL_ISLAND_WYVERN, "Ancient wyvern", "Long-tailed wyvern", "Spitting wyvern", "Taloned wyvern"),
GARGOYLES("Gargoyles", ItemID.GARGOYLE, 9, ItemID.ROCK_HAMMER),
GENERAL_GRAARDOR("General Graardor", ItemID.PET_GENERAL_GRAARDOR),
GHOSTS("Ghosts", ItemID.GHOSTSPEAK_AMULET, "Death wing", "Tortured soul"),
GHOULS("Ghouls", ItemID.ZOMBIE_HEAD),
GIANT_MOLE("Giant Mole", ItemID.BABY_MOLE),
GOBLINS("Goblins", ItemID.ENSOULED_GOBLIN_HEAD),
GREATER_DEMONS("Greater demons", ItemID.GREATER_DEMON_MASK),
GREEN_DRAGONS("Green dragons", ItemID.GREEN_DRAGON_MASK, "Baby green dragon", "Elvarg"),
GROTESQUE_GUARDIANS("Grotesque Guardians", ItemID.MIDNIGHT, 0, ItemID.ROCK_HAMMER, "Dusk", "Dawn"),
HARPIE_BUG_SWARMS("Harpie bug swarms", ItemID.SWARM),
HELLHOUNDS("Hellhounds", ItemID.HELLHOUND),
HILL_GIANTS("Hill giants", ItemID.ENSOULED_GIANT_HEAD, "Cyclops"),
HOBGOBLINS("Hobgoblins", ItemID.HOBGOBLIN_GUARD),
HYDRAS("Hydras", ItemID.HYDRA),
ICEFIENDS("Icefiends", ItemID.ICE_DIAMOND),
ICE_GIANTS("Ice giants", ItemID.ICE_DIAMOND),
ICE_WARRIORS("Ice warriors", ItemID.MITHRIL_FULL_HELM_T, "Icelord"),
INFERNAL_MAGES("Infernal mages", ItemID.INFERNAL_MAGE, "Malevolent mage"),
IRON_DRAGONS("Iron dragons", ItemID.IRON_DRAGON_MASK),
JAD("TzTok-Jad", ItemID.TZREKJAD, 25250),
JELLIES("Jellies", ItemID.JELLY, "Jelly"),
JUNGLE_HORROR("Jungle horrors", ItemID.ENSOULED_HORROR_HEAD),
KALPHITE("Kalphite", ItemID.KALPHITE_SOLDIER),
KALPHITE_QUEEN("Kalphite Queen", ItemID.KALPHITE_PRINCESS),
KILLERWATTS("Killerwatts", ItemID.KILLERWATT),
KING_BLACK_DRAGON("King Black Dragon", ItemID.PRINCE_BLACK_DRAGON),
KRAKEN("Cave Kraken Boss", ItemID.PET_KRAKEN, "Kraken"),
KREEARRA("Kree'arra", ItemID.PET_KREEARRA),
KRIL_TSUTSAROTH("K'ril Tsutsaroth", ItemID.PET_KRIL_TSUTSAROTH),
KURASK("Kurask", ItemID.KURASK),
LAVA_DRAGONS("Lava Dragons", ItemID.LAVA_SCALE, "Lava dragon"),
LESSER_DEMONS("Lesser demons", ItemID.LESSER_DEMON_MASK),
LIZARDMEN("Lizardmen", ItemID.LIZARDMAN_FANG, "Lizardman"),
LIZARDS("Lizards", ItemID.DESERT_LIZARD, "Desert lizard", "Sulphur lizard", "Small lizard", "Lizard"),
MAGIC_AXES("Magic axes", ItemID.IRON_BATTLEAXE, "Magic axe"),
MAMMOTHS("Mammoths", ItemID.ATTACKER_HORN, "Mammoth"),
MINIONS_OF_SCABARAS("Minions of scabaras", ItemID.GOLDEN_SCARAB, "Scarab swarm", "Locust rider", "Scarab mage"),
MINOTAURS("Minotaurs", ItemID.ENSOULED_MINOTAUR_HEAD),
MITHRIL_DRAGONS("Mithril dragons", ItemID.MITHRIL_DRAGON_MASK),
MOGRES("Mogres", ItemID.MOGRE),
MOLANISKS("Molanisks", ItemID.MOLANISK),
MONKEYS("Monkeys", ItemID.ENSOULED_MONKEY_HEAD, "Tortured gorilla"),
MOSS_GIANTS("Moss giants", ItemID.HILL_GIANT_CLUB),
MUTATED_ZYGOMITES("Mutated zygomites", ItemID.MUTATED_ZYGOMITE, 7, ItemID.FUNGICIDE_SPRAY_0, "Zygomite", "Fungi"),
NECHRYAEL("Nechryael", ItemID.NECHRYAEL, "Nechryarch"),
OGRES("Ogres", ItemID.ENSOULED_OGRE_HEAD),
OTHERWORLDLY_BEING("Otherworldly beings", ItemID.GHOSTLY_HOOD),
PIRATES("Pirates", ItemID.PIRATE_HAT, "Pirate"),
PYREFIENDS("Pyrefiends", ItemID.PYREFIEND, "Flaming pyrelord"),
RATS("Rats", ItemID.RATS_TAIL),
RED_DRAGONS("Red dragons", ItemID.BABY_RED_DRAGON, "Baby red dragon"),
REVENANTS("Revenants", ItemID.BRACELET_OF_ETHEREUM, "Revenant imp", "Revenant goblin", "Revenant pyrefiend", "Revenant hobgoblin", "Revenant cyclops", "Revenant hellhound", "Revenant demon", "Revenant ork", "Revenant dark beast", "Revenant knight", "Revenant dragon"),
ROCKSLUGS("Rockslugs", ItemID.ROCKSLUG, 4, ItemID.BAG_OF_SALT),
ROGUES("Rogues", ItemID.ROGUE_MASK, "Rogue"),
RUNE_DRAGONS("Rune dragons", ItemID.RUNE_DRAGON_MASK),
SARACHNIS("Sarachnis", ItemID.SRARACHA),
SCORPIA("Scorpia", ItemID.SCORPIAS_OFFSPRING),
SCORPIONS("Scorpions", ItemID.ENSOULED_SCORPION_HEAD),
SEA_SNAKES("Sea snakes", ItemID.SNAKE_CORPSE),
SHADES("Shades", ItemID.SHADE_ROBE_TOP, "Loar Shadow", "Loar Shade", "Phrin Shadow", "Phrin Shade", "Riyl Shadow", "Riyl Shade", "Asyn Shadow", "Asyn Shade", "Fiyr Shadow", "Fiyr Shade"),
SHADOW_WARRIORS("Shadow warriors", ItemID.BLACK_FULL_HELM),
SKELETAL_WYVERNS("Skeletal wyverns", ItemID.SKELETAL_WYVERN),
SKELETONS("Skeletons", ItemID.SKELETON_GUARD),
SMOKE_DEVILS("Smoke devils", ItemID.SMOKE_DEVIL),
SOURHOGS("Sourhogs", ItemID.SOURHOG_FOOT),
SPIDERS("Spiders", ItemID.HUGE_SPIDER),
SPIRITUAL_CREATURES("Spiritual creatures", ItemID.DRAGON_BOOTS, "Spiritual ranger", "Spiritual mage", "Spiritual warrior"),
STEEL_DRAGONS("Steel dragons", ItemID.STEEL_DRAGON),
SULPHUR_LIZARDS("Sulphur Lizards", ItemID.SULPHUR_LIZARD),
SUQAHS("Suqahs", ItemID.SUQAH_TOOTH),
TEMPLE_SPIDERS("Temple Spiders", ItemID.RED_SPIDERS_EGGS),
TERROR_DOGS("Terror dogs", ItemID.TERROR_DOG),
THERMONUCLEAR_SMOKE_DEVIL("Thermonuclear Smoke Devil", ItemID.PET_SMOKE_DEVIL),
TROLLS("Trolls", ItemID.TROLL_GUARD, "Dad", "Arrg"),
TUROTH("Turoth", ItemID.TUROTH),
TZHAAR("Tzhaar", ItemID.ENSOULED_TZHAAR_HEAD),
UNDEAD_DRUIDS("Undead Druids", ItemID.MASK_OF_RANUL),
VAMPYRES("Vampyres", ItemID.STAKE, "Vyrewatch", "Vampire"),
VENENATIS("Venenatis", ItemID.VENENATIS_SPIDERLING),
VETION("Vet'ion", ItemID.VETION_JR),
VORKATH("Vorkath", ItemID.VORKI),
WALL_BEASTS("Wall beasts", ItemID.SWAMP_WALLBEAST),
WATERFIENDS("Waterfiends", ItemID.WATER_ORB),
WEREWOLVES("Werewolves", ItemID.WOLFBANE, "Werewolf"),
WOLVES("Wolves", ItemID.GREY_WOLF_FUR, "Wolf"),
WYRMS("Wyrms", ItemID.WYRM),
ZILYANA("Commander Zilyana", ItemID.PET_ZILYANA),
ZOMBIES("Zombies", ItemID.ZOMBIE_HEAD, "Undead"),
ZUK("TzKal-Zuk", ItemID.TZREKZUK, 101890),
ZULRAH("Zulrah", ItemID.PET_SNAKELING);
//</editor-fold>
private static final Map<String, Task> tasks;
static final List<String> LOCATIONS = ImmutableList.of(
"", // no location is a valid location
"Abyss",
"Ancient Cavern",
"Asgarnian Ice Dungeon",
"Battlefront",
"Brimhaven Dungeon",
"Brine Rat Cavern",
"Catacombs of Kourend",
"Chasm of Fire",
"Clan Wars",
"Death Plateau",
"Evil Chicken's Lair",
"Fossil Island",
"Forthos Dungeon",
"Fremennik Slayer Dungeon",
"God Wars Dungeon",
"Iorwerth Dungeon",
"Jormungand's Prison",
"Kalphite Lair",
"Karuulm Slayer Dungeon",
"Keldagrim",
"Kraken Cove",
"Lighthouse",
"Lithkren Vault",
"Lizardman Canyon",
"Lizardman Settlement",
"Meiyerditch Laboratories",
"Molch",
"Mount Quidamortem",
"Mourner Tunnels",
"Myths' Guild Dungeon",
"Ogre Enclave",
"Slayer Tower",
"Smoke Devil Dungeon",
"Smoke Dungeon",
"Stronghold of Security",
"Stronghold Slayer Dungeon",
"task-only Kalphite Cave",
"Taverley Dungeon",
"Troll Stronghold",
"Waterbirth Island",
"Waterfall Dungeon",
"Wilderness",
"Witchaven Dungeon",
"Zanaris"
);
private final String name;
private final int itemSpriteId;
private final String[] targetNames;
private final int weaknessThreshold;
private final int weaknessItem;
private final int expectedKillExp;
static
{
ImmutableMap.Builder<String, Task> builder = new ImmutableMap.Builder<>();
for (Task task : values())
{
builder.put(task.getName().toLowerCase(), task);
}
tasks = builder.build();
}
Task(String name, int itemSpriteId, String... targetNames)
{
Preconditions.checkArgument(itemSpriteId >= 0);
this.name = name;
this.itemSpriteId = itemSpriteId;
this.weaknessThreshold = -1;
this.weaknessItem = -1;
this.targetNames = targetNames;
this.expectedKillExp = 0;
}
Task(String name, int itemSpriteId, int weaknessThreshold, int weaknessItem, String... targetNames)
{
Preconditions.checkArgument(itemSpriteId >= 0);
this.name = name;
this.itemSpriteId = itemSpriteId;
this.weaknessThreshold = weaknessThreshold;
this.weaknessItem = weaknessItem;
this.targetNames = targetNames;
this.expectedKillExp = 0;
}
Task(String name, int itemSpriteId, int expectedKillExp)
{
Preconditions.checkArgument(itemSpriteId >= 0);
this.name = name;
this.itemSpriteId = itemSpriteId;
this.weaknessThreshold = -1;
this.weaknessItem = -1;
this.targetNames = new String[0];
this.expectedKillExp = expectedKillExp;
}
@Nullable
static Task getTask(String taskName)
{
return tasks.get(taskName.toLowerCase());
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2017, Tyler <https://github.com/tylerthardy>
* 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.slayer;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.ui.overlay.infobox.Counter;
import java.awt.image.BufferedImage;
class TaskCounter extends Counter
{
TaskCounter(BufferedImage img, Plugin plugin, int amount)
{
super(img, plugin, amount);
}
}

View File

@@ -148,7 +148,7 @@ public abstract class RSNPCMixin implements RSNPC
@Inject
@Override
public NPCComposition getTransformedDefinition()
public NPCComposition getTransformedComposition()
{
RSNPCComposition composition = getComposition();
if (composition != null && composition.getConfigs() != null)