diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigButton.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigButton.java new file mode 100644 index 00000000000..abf140f2148 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigButton.java @@ -0,0 +1,27 @@ +package net.runelite.client.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Simple configuration action button that appears inside plugin configuration panels. + */ +@RequiredArgsConstructor +public class ConfigButton +{ + @Getter + private final String label; + + private final Runnable onClick; + + /** + * Invoke the configured action if one was supplied. + */ + public void press() + { + if (onClick != null) + { + onClick.run(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 36a3eef4c99..6231a8e56e2 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -1199,6 +1199,11 @@ public void setDefaultConfiguration(T proxy, boolean override continue; } + if (method.getReturnType() == ConfigButton.class) + { + continue; + } + if (!method.isDefault()) { if (override) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java index 5fddd269a79..fd81f6f23fd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java @@ -65,6 +65,7 @@ import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.List; import java.util.*; @@ -355,6 +356,10 @@ else if (cid.getType() == Notification.class) { item.add(createNotification(cd, cid), BorderLayout.EAST); } + else if (cid.getType() == ConfigButton.class) + { + item.add(createConfigButton(cid), BorderLayout.EAST); + } else if (cid.getType() instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); @@ -602,7 +607,7 @@ private JPanel createDimension(ConfigDescriptor cd, ConfigItemDescriptor cid) return dimensionPanel; } - private JComboBox> createComboBox(ConfigDescriptor cd, ConfigItemDescriptor cid) + private JComboBox> createComboBox(ConfigDescriptor cd, ConfigItemDescriptor cid) { Class type = (Class) cid.getType(); @@ -692,14 +697,14 @@ private JPanel createNotification(ConfigDescriptor cd, ConfigItemDescriptor cid) return panel; } - private JList> createList(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); - Class type = (Class) parameterizedType.getActualTypeArguments()[0]; - Set set = configManager.getConfiguration(cd.getGroup().value(), null, - cid.getItem().keyName(), parameterizedType); + private JList> createList(ConfigDescriptor cd, ConfigItemDescriptor cid) + { + ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); + Class type = (Class) parameterizedType.getActualTypeArguments()[0]; + Set set = configManager.getConfiguration(cd.getGroup().value(), null, + cid.getItem().keyName(), parameterizedType); - JList> list = new JList>(type.getEnumConstants()); // NOPMD: UseDiamondOperator + JList> list = new JList>(type.getEnumConstants()); // NOPMD: UseDiamondOperator list.setCellRenderer(listCellRenderer); list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); list.setLayoutOrientation(JList.VERTICAL); @@ -717,8 +722,73 @@ public void focusLost(FocusEvent e) } }); - return list; - } + return list; + } + + private JButton createConfigButton(ConfigItemDescriptor cid) + { + JButton button = new JButton(); + button.setFocusable(false); + button.addActionListener(e -> + { + ConfigButton configButton = getConfigButtonValue(cid); + if (configButton != null) + { + configButton.press(); + } + }); + + ConfigButton configButton = getConfigButtonValue(cid); + if (configButton != null) + { + button.setText(configButton.getLabel()); + } + else + { + button.setText("Open"); + button.setEnabled(false); + } + + return button; + } + + private ConfigButton getConfigButtonValue(ConfigItemDescriptor cid) + { + if (pluginConfig == null || pluginConfig.getConfig() == null) + { + return null; + } + + Config config = pluginConfig.getConfig(); + Class configInterface = config.getClass().getInterfaces()[0]; + + for (Method method : configInterface.getDeclaredMethods()) + { + ConfigItem item = method.getDeclaredAnnotation(ConfigItem.class); + if (item == null || method.getParameterCount() != 0) + { + continue; + } + + if (!item.keyName().equals(cid.getItem().keyName())) + { + continue; + } + + try + { + Object value = method.invoke(config); + return value instanceof ConfigButton ? (ConfigButton) value : null; + } + catch (ReflectiveOperationException ex) + { + log.warn("Unable to invoke config button {}", method.getName(), ex); + return null; + } + } + + return null; + } private void changeConfiguration(Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java index 28cdf25561a..a696fd75762 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java @@ -4,9 +4,13 @@ import lombok.AllArgsConstructor; import lombok.Getter; import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigButton; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigSection; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeyAssignments; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeyConfigAccess; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeyOption; @ConfigGroup(MicrobotConfig.configGroup) public interface MicrobotConfig extends Config @@ -46,18 +50,104 @@ default boolean disableWorldSwitcherConfirmation() return true; } - @ConfigSection( - name = "Logging", - description = "Game chat logging configuration", - position = 1 - ) - String loggingSection = "loggingSection"; - @ConfigSection( - name = "Caching", - description = "Caching ingame data", - position = 2 - ) - String cacheSection = "cacheSection"; + @ConfigSection( + name = "Logging", + description = "Game chat logging configuration", + position = 1 + ) + String loggingSection = "loggingSection"; + @ConfigSection( + name = "Caching", + description = "Caching ingame data", + position = 2 + ) + String cacheSection = "cacheSection"; + @ConfigSection( + name = "Prayer Hotkeys", + description = "Configure the five Microbot prayer hotkey slots", + position = 3 + ) + String prayerHotkeysSection = "prayerHotkeysSection"; + + String keyPrayerHotkeyConfigure = "configurePrayerHotkeys"; + + @ConfigItem( + keyName = keyPrayerHotkeyConfigure, + name = "Configure hotkeys", + description = "Open the in-game selector to assign prayers to hotkey slots.", + position = 0, + section = prayerHotkeysSection + ) + default ConfigButton openPrayerHotkeySelector() + { + return new ConfigButton("Open selector", PrayerHotkeyConfigAccess::openSelector); + } + + String keyPrayerHotkeySlot1 = PrayerHotkeyAssignments.SLOT_KEY_PREFIX + "1"; + String keyPrayerHotkeySlot2 = PrayerHotkeyAssignments.SLOT_KEY_PREFIX + "2"; + String keyPrayerHotkeySlot3 = PrayerHotkeyAssignments.SLOT_KEY_PREFIX + "3"; + String keyPrayerHotkeySlot4 = PrayerHotkeyAssignments.SLOT_KEY_PREFIX + "4"; + String keyPrayerHotkeySlot5 = PrayerHotkeyAssignments.SLOT_KEY_PREFIX + "5"; + + @ConfigItem( + keyName = keyPrayerHotkeySlot1, + name = "Hotkey 1", + description = "Prayer triggered when hotkey slot 1 is clicked.", + position = 1, + section = prayerHotkeysSection + ) + default PrayerHotkeyOption prayerHotkeySlot1() + { + return PrayerHotkeyOption.NONE; + } + + @ConfigItem( + keyName = keyPrayerHotkeySlot2, + name = "Hotkey 2", + description = "Prayer triggered when hotkey slot 2 is clicked.", + position = 2, + section = prayerHotkeysSection + ) + default PrayerHotkeyOption prayerHotkeySlot2() + { + return PrayerHotkeyOption.NONE; + } + + @ConfigItem( + keyName = keyPrayerHotkeySlot3, + name = "Hotkey 3", + description = "Prayer triggered when hotkey slot 3 is clicked.", + position = 3, + section = prayerHotkeysSection + ) + default PrayerHotkeyOption prayerHotkeySlot3() + { + return PrayerHotkeyOption.NONE; + } + + @ConfigItem( + keyName = keyPrayerHotkeySlot4, + name = "Hotkey 4", + description = "Prayer triggered when hotkey slot 4 is clicked.", + position = 4, + section = prayerHotkeysSection + ) + default PrayerHotkeyOption prayerHotkeySlot4() + { + return PrayerHotkeyOption.NONE; + } + + @ConfigItem( + keyName = keyPrayerHotkeySlot5, + name = "Hotkey 5", + description = "Prayer triggered when hotkey slot 5 is clicked.", + position = 5, + section = prayerHotkeysSection + ) + default PrayerHotkeyOption prayerHotkeySlot5() + { + return PrayerHotkeyOption.NONE; + } String keyEnableGameChatLogging = "enableGameChatLogging"; @ConfigItem( @@ -111,23 +201,23 @@ default boolean onlyMicrobotLogging() { return false; } - String keyEnableMenuEntryLogging = "enableMenuEntryLogging"; + String keyEnableMenuEntryLogging = "enableMenuEntryLogging"; - @ConfigItem( - keyName = keyEnableMenuEntryLogging, - name = "Enable Menu Entry Logging", + @ConfigItem( + keyName = keyEnableMenuEntryLogging, + name = "Enable Menu Entry Logging", description = "Enable or disable logging menu entry clicked", position = 4, section = loggingSection ) - default boolean enableMenuEntryLogging() { - return false; - } + default boolean enableMenuEntryLogging() { + return false; + } - String keyEnableCache = "enableRs2Cache"; - @ConfigItem( - keyName = keyEnableCache, - name = "Enable Microbot Cache", + String keyEnableCache = "enableRs2Cache"; + @ConfigItem( + keyName = keyEnableCache, + name = "Enable Microbot Cache", description = "This will cache ingame entities (npcs, objects,...) to improve performance", position = 0, section = cacheSection diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index f3901214b88..9a561bc81a3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -28,6 +28,10 @@ import net.runelite.client.plugins.microbot.util.inventory.Rs2RunePouch; import net.runelite.client.plugins.microbot.util.overlay.GembagOverlay; import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeyAssignments; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeyConfigAccess; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeyOverlay; +import net.runelite.client.plugins.microbot.util.prayer.PrayerHotkeySelector; import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection; import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; @@ -96,11 +100,17 @@ MicrobotConfig provideConfig(ConfigManager configManager) @Inject private OverlayManager overlayManager; @Inject - private MicrobotOverlay microbotOverlay; - @Inject - private GembagOverlay gembagOverlay; - @Inject - private PouchOverlay pouchOverlay; + private MicrobotOverlay microbotOverlay; + @Inject + private GembagOverlay gembagOverlay; + @Inject + private PouchOverlay pouchOverlay; + @Inject + private PrayerHotkeyOverlay prayerHotkeyOverlay; + @Inject + private PrayerHotkeySelector prayerHotkeySelector; + @Inject + private PrayerHotkeyAssignments prayerHotkeyAssignments; @Inject private EventBus eventBus; private GameChatAppender gameChatAppender; @@ -181,24 +191,32 @@ protected void startUp() throws AWTException initializeCacheSystem(); } - if (overlayManager != null) - { - overlayManager.add(microbotOverlay); - overlayManager.add(gembagOverlay); - overlayManager.add(pouchOverlay); - microbotOverlay.cacheButton.hookMouseListener(); - } - } - - protected void shutDown() - { - overlayManager.remove(microbotOverlay); - overlayManager.remove(gembagOverlay); - overlayManager.remove(pouchOverlay); - microbotOverlay.cacheButton.unhookMouseListener(); - clientToolbar.removeNavigation(navButton); - if (gameChatAppender.isStarted()) gameChatAppender.stop(); - microbotVersionChecker.shutdown(); + if (overlayManager != null) + { + overlayManager.add(microbotOverlay); + overlayManager.add(gembagOverlay); + overlayManager.add(pouchOverlay); + overlayManager.add(prayerHotkeyOverlay); + microbotOverlay.cacheButton.hookMouseListener(); + prayerHotkeyOverlay.hookMouseListener(); + } + + PrayerHotkeyConfigAccess.setOpenSelectorAction(prayerHotkeySelector::toggle); + } + + protected void shutDown() + { + overlayManager.remove(microbotOverlay); + overlayManager.remove(gembagOverlay); + overlayManager.remove(pouchOverlay); + overlayManager.remove(prayerHotkeyOverlay); + microbotOverlay.cacheButton.unhookMouseListener(); + prayerHotkeyOverlay.unhookMouseListener(); + prayerHotkeySelector.close(); + PrayerHotkeyConfigAccess.clearOpenSelectorAction(); + clientToolbar.removeNavigation(navButton); + if (gameChatAppender.isStarted()) gameChatAppender.stop(); + microbotVersionChecker.shutdown(); // Shutdown the cache system shutdownCacheSystem(); @@ -404,11 +422,16 @@ private void onChatMessage(ChatMessage event) } @Subscribe - public void onConfigChanged(ConfigChanged ev) - { - if (ev.getGroup().equals(MicrobotConfig.configGroup)) { - switch (ev.getKey()) { - case MicrobotConfig.keyEnableGameChatLogging: + public void onConfigChanged(ConfigChanged ev) + { + if (ev.getGroup().equals(MicrobotConfig.configGroup)) { + if (ev.getKey() != null && ev.getKey().startsWith(PrayerHotkeyAssignments.SLOT_KEY_PREFIX)) + { + prayerHotkeyAssignments.reload(); + return; + } + switch (ev.getKey()) { + case MicrobotConfig.keyEnableGameChatLogging: case MicrobotConfig.keyGameChatLogPattern: case MicrobotConfig.keyGameChatLogLevel: case MicrobotConfig.keyOnlyMicrobotLogging: @@ -456,22 +479,21 @@ public void onConfigChanged(ConfigChanged ev) } @Subscribe - public void onWidgetLoaded(WidgetLoaded event) - { - Rs2RunePouch.onWidgetLoaded(event); - - // Mark that widget layout has changed for cache invalidation - widgetLayoutChanged = true; - log.debug("Widget {} loaded, layout changed", event.getGroupId()); - } - - @Subscribe - public void onWidgetClosed(WidgetClosed event) - { - // Mark that widget layout has changed for cache invalidation - widgetLayoutChanged = true; - log.debug("Widget {} closed, layout changed", event.getGroupId()); - } + public void onWidgetLoaded(WidgetLoaded event) + { + Rs2RunePouch.onWidgetLoaded(event); + // Mark that widget layout has changed for cache invalidation + widgetLayoutChanged = true; + log.debug("Widget {} loaded, layout changed", event.getGroupId()); + } + + @Subscribe + public void onWidgetClosed(WidgetClosed event) + { + // Mark that widget layout has changed for cache invalidation + widgetLayoutChanged = true; + log.debug("Widget {} closed, layout changed", event.getGroupId()); + } @Subscribe public void onHitsplatApplied(HitsplatApplied event) @@ -527,11 +549,11 @@ public void onOverlayMenuClicked(OverlayMenuClicked overlayMenuClicked) } @Subscribe - public void onGameTick(GameTick event) - { - // Cache loading is now handled properly during login/profile changes - // No need to call loadInitialCacheFromCurrentConfig on every tick - } + public void onGameTick(GameTick event) + { + // Cache loading is now handled properly during login/profile changes + // No need to call loadInitialCacheFromCurrentConfig on every tick + } @Subscribe(priority = 100) private void onClientShutdown(ClientShutdown e) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyAssignments.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyAssignments.java new file mode 100644 index 00000000000..adb74d4bc03 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyAssignments.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.microbot.util.prayer; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Arrays; + +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.microbot.MicrobotConfig; + +@Singleton +public class PrayerHotkeyAssignments +{ + public static final int SLOT_COUNT = 5; + public static final String SLOT_KEY_PREFIX = "prayerHotkeySlot"; + + private final ConfigManager configManager; + private final PrayerHotkeyOption[] slots = new PrayerHotkeyOption[SLOT_COUNT]; + + @Inject + PrayerHotkeyAssignments(ConfigManager configManager) + { + this.configManager = configManager; + Arrays.fill(slots, PrayerHotkeyOption.NONE); + reload(); + } + + public void reload() + { + for (int i = 0; i < SLOT_COUNT; i++) + { + slots[i] = readSlot(i); + } + } + + public PrayerHotkeyOption getSlot(int index) + { + if (!isValidIndex(index)) + { + return PrayerHotkeyOption.NONE; + } + + return slots[index]; + } + + public void setSlot(int index, PrayerHotkeyOption option) + { + if (!isValidIndex(index)) + { + return; + } + + PrayerHotkeyOption value = option == null ? PrayerHotkeyOption.NONE : option; + slots[index] = value; + + if (value == PrayerHotkeyOption.NONE) + { + configManager.unsetConfiguration(MicrobotConfig.configGroup, getKey(index)); + } + else + { + configManager.setConfiguration(MicrobotConfig.configGroup, getKey(index), value.name()); + } + } + + public void clearSlot(int index) + { + setSlot(index, PrayerHotkeyOption.NONE); + } + + private PrayerHotkeyOption readSlot(int index) + { + String stored = configManager.getConfiguration(MicrobotConfig.configGroup, getKey(index)); + if (stored == null || stored.isEmpty()) + { + return PrayerHotkeyOption.NONE; + } + + try + { + return PrayerHotkeyOption.valueOf(stored); + } + catch (IllegalArgumentException ex) + { + return PrayerHotkeyOption.NONE; + } + } + + public static String getKey(int index) + { + return SLOT_KEY_PREFIX + (index + 1); + } + + private boolean isValidIndex(int index) + { + return index >= 0 && index < SLOT_COUNT; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyConfigAccess.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyConfigAccess.java new file mode 100644 index 00000000000..d8cb865dd0e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyConfigAccess.java @@ -0,0 +1,27 @@ +package net.runelite.client.plugins.microbot.util.prayer; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class PrayerHotkeyConfigAccess +{ + private static Runnable openSelectorAction; + + public void setOpenSelectorAction(Runnable action) + { + openSelectorAction = action; + } + + public void clearOpenSelectorAction() + { + openSelectorAction = null; + } + + public void openSelector() + { + if (openSelectorAction != null) + { + openSelectorAction.run(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyOption.java new file mode 100644 index 00000000000..55a9ab82deb --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyOption.java @@ -0,0 +1,108 @@ +package net.runelite.client.plugins.microbot.util.prayer; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Prayer; +import net.runelite.api.gameval.SpriteID; +import net.runelite.client.util.Text; + +@Getter +@RequiredArgsConstructor +public enum PrayerHotkeyOption +{ + NONE(null, null, -1, -1), + + THICK_SKIN(Rs2PrayerEnum.THICK_SKIN, Prayer.THICK_SKIN, + SpriteID.Prayeron.THICK_SKIN, SpriteID.Prayeroff.THICK_SKIN_DISABLED), + BURST_OF_STRENGTH(Rs2PrayerEnum.BURST_STRENGTH, Prayer.BURST_OF_STRENGTH, + SpriteID.Prayeron.BURST_OF_STRENGTH, SpriteID.Prayeroff.BURST_OF_STRENGTH_DISABLED), + CLARITY_OF_THOUGHT(Rs2PrayerEnum.CLARITY_THOUGHT, Prayer.CLARITY_OF_THOUGHT, + SpriteID.Prayeron.CLARITY_OF_THOUGHT, SpriteID.Prayeroff.CLARITY_OF_THOUGHT_DISABLED), + SHARP_EYE(Rs2PrayerEnum.SHARP_EYE, Prayer.SHARP_EYE, + SpriteID.Prayeron.SHARP_EYE, SpriteID.Prayeroff.SHARP_EYE_DISABLED), + MYSTIC_WILL(Rs2PrayerEnum.MYSTIC_WILL, Prayer.MYSTIC_WILL, + SpriteID.Prayeron.MYSTIC_WILL, SpriteID.Prayeroff.MYSTIC_WILL_DISABLED), + ROCK_SKIN(Rs2PrayerEnum.ROCK_SKIN, Prayer.ROCK_SKIN, + SpriteID.Prayeron.ROCK_SKIN, SpriteID.Prayeroff.ROCK_SKIN_DISABLED), + SUPERHUMAN_STRENGTH(Rs2PrayerEnum.SUPERHUMAN_STRENGTH, Prayer.SUPERHUMAN_STRENGTH, + SpriteID.Prayeron.SUPERHUMAN_STRENGTH, SpriteID.Prayeroff.SUPERHUMAN_STRENGTH_DISABLED), + IMPROVED_REFLEXES(Rs2PrayerEnum.IMPROVED_REFLEXES, Prayer.IMPROVED_REFLEXES, + SpriteID.Prayeron.IMPROVED_REFLEXES, SpriteID.Prayeroff.IMPROVED_REFLEXES_DISABLED), + RAPID_RESTORE(Rs2PrayerEnum.RAPID_RESTORE, Prayer.RAPID_RESTORE, + SpriteID.Prayeron.RAPID_RESTORE, SpriteID.Prayeroff.RAPID_RESTORE_DISABLED), + RAPID_HEAL(Rs2PrayerEnum.RAPID_HEAL, Prayer.RAPID_HEAL, + SpriteID.Prayeron.RAPID_HEAL, SpriteID.Prayeroff.RAPID_HEAL_DISABLED), + PROTECT_ITEM(Rs2PrayerEnum.PROTECT_ITEM, Prayer.PROTECT_ITEM, + SpriteID.Prayeron.PROTECT_ITEM, SpriteID.Prayeroff.PROTECT_ITEM_DISABLED), + HAWK_EYE(Rs2PrayerEnum.HAWK_EYE, Prayer.HAWK_EYE, + SpriteID.Prayeron.HAWK_EYE, SpriteID.Prayeroff.HAWK_EYE_DISABLED), + MYSTIC_LORE(Rs2PrayerEnum.MYSTIC_LORE, Prayer.MYSTIC_LORE, + SpriteID.Prayeron.MYSTIC_LORE, SpriteID.Prayeroff.MYSTIC_LORE_DISABLED), + STEEL_SKIN(Rs2PrayerEnum.STEEL_SKIN, Prayer.STEEL_SKIN, + SpriteID.Prayeron.STEEL_SKIN, SpriteID.Prayeroff.STEEL_SKIN_DISABLED), + ULTIMATE_STRENGTH(Rs2PrayerEnum.ULTIMATE_STRENGTH, Prayer.ULTIMATE_STRENGTH, + SpriteID.Prayeron.ULTIMATE_STRENGTH, SpriteID.Prayeroff.ULTIMATE_STRENGTH_DISABLED), + INCREDIBLE_REFLEXES(Rs2PrayerEnum.INCREDIBLE_REFLEXES, Prayer.INCREDIBLE_REFLEXES, + SpriteID.Prayeron.INCREDIBLE_REFLEXES, SpriteID.Prayeroff.INCREDIBLE_REFLEXES_DISABLED), + PROTECT_FROM_MAGIC(Rs2PrayerEnum.PROTECT_MAGIC, Prayer.PROTECT_FROM_MAGIC, + SpriteID.Prayeron.PROTECT_FROM_MAGIC, SpriteID.Prayeroff.PROTECT_FROM_MAGIC_DISABLED), + PROTECT_FROM_MISSILES(Rs2PrayerEnum.PROTECT_RANGE, Prayer.PROTECT_FROM_MISSILES, + SpriteID.Prayeron.PROTECT_FROM_MISSILES, SpriteID.Prayeroff.PROTECT_FROM_MISSILES_DISABLED), + PROTECT_FROM_MELEE(Rs2PrayerEnum.PROTECT_MELEE, Prayer.PROTECT_FROM_MELEE, + SpriteID.Prayeron.PROTECT_FROM_MELEE, SpriteID.Prayeroff.PROTECT_FROM_MELEE_DISABLED), + EAGLE_EYE(Rs2PrayerEnum.EAGLE_EYE, Prayer.EAGLE_EYE, + SpriteID.Prayeron.EAGLE_EYE, SpriteID.Prayeroff.EAGLE_EYE_DISABLED), + DEAD_EYE(Rs2PrayerEnum.DEAD_EYE, Prayer.DEADEYE, + SpriteID.Prayeron.DEADEYE, SpriteID.Prayeroff.DEADEYE_DISABLED), + MYSTIC_MIGHT(Rs2PrayerEnum.MYSTIC_MIGHT, Prayer.MYSTIC_MIGHT, + SpriteID.Prayeron.MYSTIC_MIGHT, SpriteID.Prayeroff.MYSTIC_MIGHT_DISABLED), + MYSTIC_VIGOUR(Rs2PrayerEnum.MYSTIC_VIGOUR, Prayer.MYSTIC_VIGOUR, + SpriteID.Prayeron.MYSTIC_VIGOUR, SpriteID.Prayeroff.MYSTIC_VIGOUR_DISABLED), + RETRIBUTION(Rs2PrayerEnum.RETRIBUTION, Prayer.RETRIBUTION, + SpriteID.Prayeron.RETRIBUTION, SpriteID.Prayeroff.RETRIBUTION_DISABLED), + REDEMPTION(Rs2PrayerEnum.REDEMPTION, Prayer.REDEMPTION, + SpriteID.Prayeron.REDEMPTION, SpriteID.Prayeroff.REDEMPTION_DISABLED), + SMITE(Rs2PrayerEnum.SMITE, Prayer.SMITE, + SpriteID.Prayeron.SMITE, SpriteID.Prayeroff.SMITE_DISABLED), + PRESERVE(Rs2PrayerEnum.PRESERVE, Prayer.PRESERVE, + SpriteID.Prayeron.PRESERVE, SpriteID.Prayeroff.PRESERVE_DISABLED), + CHIVALRY(Rs2PrayerEnum.CHIVALRY, Prayer.CHIVALRY, + SpriteID.Prayeron.CHIVALRY, SpriteID.Prayeroff.CHIVALRY_DISABLED), + PIETY(Rs2PrayerEnum.PIETY, Prayer.PIETY, + SpriteID.Prayeron.PIETY, SpriteID.Prayeroff.PIETY_DISABLED), + RIGOUR(Rs2PrayerEnum.RIGOUR, Prayer.RIGOUR, + SpriteID.Prayeron.RIGOUR, SpriteID.Prayeroff.RIGOUR_DISABLED), + AUGURY(Rs2PrayerEnum.AUGURY, Prayer.AUGURY, + SpriteID.Prayeron.AUGURY, SpriteID.Prayeroff.AUGURY_DISABLED); + + private final Rs2PrayerEnum prayerEnum; + private final Prayer prayer; + private final int activeSpriteId; + private final int inactiveSpriteId; + + public int getSpriteId(boolean active) + { + return active ? activeSpriteId : inactiveSpriteId; + } + + public String getDisplayName() + { + if (this == NONE) + { + return "Empty"; + } + + if (prayer != null) + { + return Text.titleCase(prayer); + } + + return Text.titleCase(this); + } + + @Override + public String toString() + { + return getDisplayName(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyOverlay.java new file mode 100644 index 00000000000..e36b05746c0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeyOverlay.java @@ -0,0 +1,301 @@ +package net.runelite.client.plugins.microbot.util.prayer; + +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.SpritePixels; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.input.MouseAdapter; +import net.runelite.client.input.MouseListener; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.MicrobotPlugin; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.SwingUtilities; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.image.BufferedImage; + +@Singleton +public class PrayerHotkeyOverlay extends Overlay +{ + private static final int SLOT_COUNT = PrayerHotkeyAssignments.SLOT_COUNT; + private static final int SLOT_SIZE = 34; + private static final int SLOT_GAP = 4; + private static final int SLOT_CORNER_RADIUS = 6; + private static final int CHAT_MARGIN = 6; + private static final Font LABEL_FONT = new Font("Arial", Font.BOLD, 11); + private static final Stroke BORDER_STROKE = new BasicStroke(1.2f); + + private final Client client; + private final SpriteManager spriteManager; + private final PrayerHotkeyAssignments assignments; + private final Rectangle[] slotBounds = new Rectangle[SLOT_COUNT]; + + private final MouseListener mouseListener = new MouseAdapter() + { + @Override + public java.awt.event.MouseEvent mouseMoved(java.awt.event.MouseEvent mouseEvent) + { + if (!shouldHandleInput()) + { + hoveredIndex = -1; + return mouseEvent; + } + + hoveredIndex = findSlotIndex(mouseEvent.getPoint()); + return mouseEvent; + } + + @Override + public java.awt.event.MouseEvent mousePressed(java.awt.event.MouseEvent mouseEvent) + { + if (!shouldHandleInput()) + { + return mouseEvent; + } + + if (mouseEvent.getSource().toString().equals("Microbot")) + { + return mouseEvent; + } + + int slotIndex = findSlotIndex(mouseEvent.getPoint()); + if (slotIndex == -1) + { + return mouseEvent; + } + + PrayerHotkeyOption option = getOptionForSlot(slotIndex); + + if (SwingUtilities.isLeftMouseButton(mouseEvent)) + { + if (option == PrayerHotkeyOption.NONE) + { + return mouseEvent; + } + + mouseEvent.consume(); + Microbot.getClientThread().invokeLater(() -> togglePrayer(option)); + } + else if (SwingUtilities.isRightMouseButton(mouseEvent)) + { + mouseEvent.consume(); + clearSlot(slotIndex); + } + + return mouseEvent; + } + }; + + private boolean listenersHooked; + @Getter + private int hoveredIndex = -1; + private boolean renderedLastFrame; + + @Inject + PrayerHotkeyOverlay(MicrobotPlugin plugin, Client client, SpriteManager spriteManager, PrayerHotkeyAssignments assignments) + { + super(plugin); + setPosition(OverlayPosition.DYNAMIC); + setPriority(PRIORITY_HIGH); + setLayer(OverlayLayer.ABOVE_WIDGETS); + drawAfterInterface(WidgetInfo.CHATBOX.getGroupId()); + + this.client = client; + this.spriteManager = spriteManager; + this.assignments = assignments; + } + + @Override + public Dimension render(Graphics2D graphics) + { + renderedLastFrame = false; + + if (client == null || client.getGameState() != GameState.LOGGED_IN) + { + hoveredIndex = -1; + return null; + } + + Widget chatWidget = client.getWidget(WidgetInfo.CHATBOX_TRANSPARENT_BACKGROUND); + if (chatWidget == null) + { + chatWidget = client.getWidget(WidgetInfo.CHATBOX); + if (chatWidget == null) + { + hoveredIndex = -1; + return null; + } + } + + Rectangle chatBounds = chatWidget.getBounds(); + if (chatBounds == null) + { + hoveredIndex = -1; + return null; + } + + renderedLastFrame = true; + + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int startX = chatBounds.x + SLOT_GAP; + int startY = chatBounds.y - SLOT_SIZE - CHAT_MARGIN; + + for (int i = 0; i < SLOT_COUNT; i++) + { + Rectangle bounds = slotBounds[i]; + if (bounds == null) + { + bounds = new Rectangle(); + slotBounds[i] = bounds; + } + + int x = startX + i * (SLOT_SIZE + SLOT_GAP); + int y = startY; + bounds.setBounds(x, y, SLOT_SIZE, SLOT_SIZE); + + drawSlot(graphics, bounds, i); + } + + return null; + } + + public void hookMouseListener() + { + if (!listenersHooked) + { + Microbot.getMouseManager().registerMouseListener(mouseListener); + listenersHooked = true; + } + } + + public void unhookMouseListener() + { + if (listenersHooked) + { + Microbot.getMouseManager().unregisterMouseListener(mouseListener); + listenersHooked = false; + hoveredIndex = -1; + } + } + + private void drawSlot(Graphics2D graphics, Rectangle bounds, int index) + { + PrayerHotkeyOption option = getOptionForSlot(index); + boolean isHovered = hoveredIndex == index; + boolean isActive = option != PrayerHotkeyOption.NONE && client.isPrayerActive(option.getPrayer()); + + Color background = isActive ? new Color(15, 115, 12, 180) : new Color(27, 27, 27, 185); + if (isHovered) + { + background = background.brighter(); + } + + graphics.setColor(background); + graphics.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, SLOT_CORNER_RADIUS, SLOT_CORNER_RADIUS); + + graphics.setColor(new Color(0, 0, 0, 200)); + graphics.setStroke(BORDER_STROKE); + graphics.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, SLOT_CORNER_RADIUS, SLOT_CORNER_RADIUS); + + if (option != PrayerHotkeyOption.NONE) + { + int spriteId = option.getSpriteId(isActive); + if (spriteId >= 0) + { + SpritePixels sprite = spriteManager.getSprite(spriteId, 0); + if (sprite != null) + { + BufferedImage image = sprite.toBufferedImage(); + int imgX = bounds.x + (bounds.width - image.getWidth()) / 2; + int imgY = bounds.y + (bounds.height - image.getHeight()) / 2; + graphics.drawImage(image, imgX, imgY, null); + } + } + } + else + { + graphics.setColor(new Color(255, 255, 255, 60)); + int cx = bounds.x + bounds.width / 2; + int cy = bounds.y + bounds.height / 2; + graphics.drawLine(cx - 6, cy, cx + 6, cy); + graphics.drawLine(cx, cy - 6, cx, cy + 6); + } + + graphics.setFont(LABEL_FONT); + FontMetrics metrics = graphics.getFontMetrics(); + String label = "H" + (index + 1); + int textWidth = metrics.stringWidth(label); + int textX = bounds.x + (bounds.width - textWidth) / 2; + int textY = bounds.y + bounds.height - 4; + graphics.setColor(new Color(255, 255, 255, 180)); + graphics.drawString(label, textX, textY); + } + + private PrayerHotkeyOption getOptionForSlot(int slotIndex) + { + return assignments.getSlot(slotIndex); + } + + private void togglePrayer(PrayerHotkeyOption option) + { + if (option == null || option == PrayerHotkeyOption.NONE) + { + return; + } + + Client currentClient = Microbot.getClient(); + if (currentClient == null) + { + return; + } + + boolean shouldEnable = !currentClient.isPrayerActive(option.getPrayer()); + Rs2Prayer.toggle(option.getPrayerEnum(), shouldEnable); + } + + private void clearSlot(int slotIndex) + { + assignments.clearSlot(slotIndex); + } + + private int findSlotIndex(Point canvasPoint) + { + if (slotBounds == null) + { + return -1; + } + + for (int i = 0; i < slotBounds.length; i++) + { + Rectangle bounds = slotBounds[i]; + if (bounds != null && bounds.contains(canvasPoint)) + { + return i; + } + } + + return -1; + } + + private boolean shouldHandleInput() + { + return listenersHooked && renderedLastFrame && client != null && client.getGameState() == GameState.LOGGED_IN; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeySelector.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeySelector.java new file mode 100644 index 00000000000..5a57c2e2e8f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/PrayerHotkeySelector.java @@ -0,0 +1,541 @@ +package net.runelite.client.plugins.microbot.util.prayer; + +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.ScriptEvent; +import net.runelite.api.widgets.JavaScriptCallback; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.api.widgets.WidgetPositionMode; +import net.runelite.api.widgets.WidgetSizeMode; +import net.runelite.api.widgets.WidgetTextAlignment; +import net.runelite.api.widgets.WidgetType; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.microbot.Microbot; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.awt.Color; + +import static net.runelite.api.FontID.BOLD_12; +import static net.runelite.api.FontID.PLAIN_11; + +@Singleton +public class PrayerHotkeySelector +{ + private static final int PANEL_WIDTH = 224; + private static final int PANEL_HEIGHT = 200; + private static final int CHAT_OFFSET = 8; + private static final int PADDING = 8; + private static final int SLOT_SIZE = 30; + private static final int SLOT_GAP = 8; + private static final int GRID_COLUMNS = 7; + private static final int GRID_ICON_SIZE = 26; + private static final int GRID_GAP = 4; + private static final int SLOT_TEXT_HEIGHT = 14; + private static final int PANEL_OPACITY = 190; + private static final int SLOT_OPACITY = 160; + private static final int SLOT_OPACITY_SELECTED = 210; + private static final int PANEL_BACKGROUND = new Color(20, 20, 20).getRGB(); + private static final int SLOT_BACKGROUND = new Color(36, 36, 36).getRGB(); + private static final int SLOT_BACKGROUND_SELECTED = new Color(52, 96, 158).getRGB(); + private static final int LABEL_COLOR = new Color(200, 200, 200).getRGB(); + private static final int LABEL_COLOR_SELECTED = Color.WHITE.getRGB(); + private static final int PLUS_COLOR = new Color(255, 255, 255, 190).getRGB(); + private static final int INSTRUCTION_COLOR = new Color(210, 210, 210).getRGB(); + + private final Client client; + private final EventBus eventBus; + private final PrayerHotkeyAssignments assignments; + + private boolean active; + private Widget container; + private Widget[] slotBackgrounds; + private Widget[] slotIcons; + private Widget[] slotPlusLabels; + private Widget[] slotLabels; + private Widget selectionLabel; + private int selectedSlot; + + @Inject + PrayerHotkeySelector(Client client, EventBus eventBus, PrayerHotkeyAssignments assignments) + { + this.client = client; + this.eventBus = eventBus; + this.assignments = assignments; + } + + public void open() + { + if (active) + { + return; + } + + active = true; + eventBus.register(this); + selectedSlot = Math.min(selectedSlot, PrayerHotkeyAssignments.SLOT_COUNT - 1); + assignments.reload(); + + Microbot.getClientThread().invokeLater(() -> + { + if (!ensureBuilt()) + { + return false; + } + + updateSlotWidgets(); + updateSlotHighlights(); + updateSelectionLabel(); + return true; + }); + } + + public void close() + { + if (!active) + { + return; + } + + active = false; + eventBus.unregister(this); + + Microbot.getClientThread().invokeLater(() -> + { + destroy(); + return true; + }); + } + + public void toggle() + { + if (active) + { + close(); + } + else + { + open(); + } + } + + public boolean isOpen() + { + return active; + } + + @Subscribe + public void onGameTick(GameTick tick) + { + if (!active) + { + return; + } + + if (ensureBuilt()) + { + updateSlotWidgets(); + updateSlotHighlights(); + updateSelectionLabel(); + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged stateChanged) + { + if (!active) + { + return; + } + + GameState state = stateChanged.getGameState(); + if (state == GameState.LOGGED_IN) + { + assignments.reload(); + ensureBuilt(); + updateSlotWidgets(); + updateSlotHighlights(); + updateSelectionLabel(); + } + else if (state == GameState.LOGIN_SCREEN || state == GameState.HOPPING) + { + destroy(); + } + } + + private boolean ensureBuilt() + { + if (!active) + { + return true; + } + + Widget chatWidget = getChatWidget(); + if (chatWidget == null || chatWidget.isHidden()) + { + destroy(); + return false; + } + + if (container != null && container.getParent() == chatWidget && !container.isHidden()) + { + return true; + } + + destroy(); + buildInterface(chatWidget); + return true; + } + + private void buildInterface(Widget chatWidget) + { + container = chatWidget.createChild(-1, WidgetType.RECTANGLE); + container.setWidthMode(WidgetSizeMode.ABSOLUTE); + container.setHeightMode(WidgetSizeMode.ABSOLUTE); + container.setOriginalWidth(PANEL_WIDTH); + container.setOriginalHeight(PANEL_HEIGHT); + container.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + container.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + container.setOriginalX(CHAT_OFFSET); + container.setOriginalY(-PANEL_HEIGHT - CHAT_OFFSET); + container.setTextColor(PANEL_BACKGROUND); + container.setOpacity(PANEL_OPACITY); + container.setFilled(true); + container.setNoClickThrough(true); + container.setBorderType(1); + container.setName("Prayer hotkey setup"); + container.revalidate(); + + Widget title = container.createChild(-1, WidgetType.TEXT); + title.setFontId(BOLD_12); + title.setText("Prayer Hotkeys"); + title.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + title.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + title.setWidthMode(WidgetSizeMode.ABSOLUTE); + title.setHeightMode(WidgetSizeMode.ABSOLUTE); + title.setOriginalX(PADDING); + title.setOriginalY(PADDING); + title.setOriginalWidth(PANEL_WIDTH - PADDING * 2); + title.setOriginalHeight(16); + title.setXTextAlignment(WidgetTextAlignment.LEFT); + title.setYTextAlignment(WidgetTextAlignment.CENTER); + title.setTextColor(Color.WHITE.getRGB()); + title.revalidate(); + + slotBackgrounds = new Widget[PrayerHotkeyAssignments.SLOT_COUNT]; + slotIcons = new Widget[PrayerHotkeyAssignments.SLOT_COUNT]; + slotPlusLabels = new Widget[PrayerHotkeyAssignments.SLOT_COUNT]; + slotLabels = new Widget[PrayerHotkeyAssignments.SLOT_COUNT]; + + int slotsWidth = PrayerHotkeyAssignments.SLOT_COUNT * SLOT_SIZE + + (PrayerHotkeyAssignments.SLOT_COUNT - 1) * SLOT_GAP; + int availableWidth = PANEL_WIDTH - PADDING * 2; + int slotsStartX = PADDING + Math.max(0, (availableWidth - slotsWidth) / 2); + int slotY = PADDING + 20; + + for (int i = 0; i < PrayerHotkeyAssignments.SLOT_COUNT; i++) + { + final int slotIndex = i; + int slotX = slotsStartX + i * (SLOT_SIZE + SLOT_GAP); + + Widget background = container.createChild(-1, WidgetType.RECTANGLE); + background.setWidthMode(WidgetSizeMode.ABSOLUTE); + background.setHeightMode(WidgetSizeMode.ABSOLUTE); + background.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + background.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + background.setOriginalX(slotX); + background.setOriginalY(slotY); + background.setOriginalWidth(SLOT_SIZE); + background.setOriginalHeight(SLOT_SIZE); + background.setTextColor(SLOT_BACKGROUND); + background.setOpacity(SLOT_OPACITY); + background.setFilled(true); + background.setBorderType(1); + background.setAction(0, "Select"); + background.setAction(1, "Clear"); + background.setHasListener(true); + background.setOnOpListener((JavaScriptCallback) event -> handleSlotClick(event, slotIndex)); + background.setName("Hotkey " + (slotIndex + 1)); + background.revalidate(); + slotBackgrounds[i] = background; + + Widget icon = container.createChild(-1, WidgetType.GRAPHIC); + icon.setWidthMode(WidgetSizeMode.ABSOLUTE); + icon.setHeightMode(WidgetSizeMode.ABSOLUTE); + icon.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + icon.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + icon.setOriginalX(slotX + 2); + icon.setOriginalY(slotY + 2); + icon.setOriginalWidth(SLOT_SIZE - 4); + icon.setOriginalHeight(SLOT_SIZE - 4); + icon.setSpriteId(-1); + icon.revalidate(); + slotIcons[i] = icon; + + Widget plus = container.createChild(-1, WidgetType.TEXT); + plus.setFontId(BOLD_12); + plus.setText("+"); + plus.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + plus.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + plus.setWidthMode(WidgetSizeMode.ABSOLUTE); + plus.setHeightMode(WidgetSizeMode.ABSOLUTE); + plus.setOriginalX(slotX); + plus.setOriginalY(slotY + 6); + plus.setOriginalWidth(SLOT_SIZE); + plus.setOriginalHeight(SLOT_SIZE - 12); + plus.setXTextAlignment(WidgetTextAlignment.CENTER); + plus.setYTextAlignment(WidgetTextAlignment.CENTER); + plus.setTextColor(PLUS_COLOR); + plus.revalidate(); + slotPlusLabels[i] = plus; + + Widget label = container.createChild(-1, WidgetType.TEXT); + label.setFontId(PLAIN_11); + label.setText("H" + (i + 1)); + label.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + label.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + label.setWidthMode(WidgetSizeMode.ABSOLUTE); + label.setHeightMode(WidgetSizeMode.ABSOLUTE); + label.setOriginalX(slotX); + label.setOriginalY(slotY + SLOT_SIZE + 2); + label.setOriginalWidth(SLOT_SIZE); + label.setOriginalHeight(SLOT_TEXT_HEIGHT); + label.setXTextAlignment(WidgetTextAlignment.CENTER); + label.setYTextAlignment(WidgetTextAlignment.CENTER); + label.setTextColor(LABEL_COLOR); + label.revalidate(); + slotLabels[i] = label; + } + + selectionLabel = container.createChild(-1, WidgetType.TEXT); + selectionLabel.setFontId(PLAIN_11); + selectionLabel.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + selectionLabel.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + selectionLabel.setWidthMode(WidgetSizeMode.ABSOLUTE); + selectionLabel.setHeightMode(WidgetSizeMode.ABSOLUTE); + selectionLabel.setOriginalX(PADDING); + selectionLabel.setOriginalY(slotY + SLOT_SIZE + SLOT_TEXT_HEIGHT + 8); + selectionLabel.setOriginalWidth(PANEL_WIDTH - PADDING * 2); + selectionLabel.setOriginalHeight(SLOT_TEXT_HEIGHT); + selectionLabel.setXTextAlignment(WidgetTextAlignment.LEFT); + selectionLabel.setYTextAlignment(WidgetTextAlignment.CENTER); + selectionLabel.setTextColor(Color.WHITE.getRGB()); + selectionLabel.revalidate(); + + Widget instructions = container.createChild(-1, WidgetType.TEXT); + instructions.setFontId(PLAIN_11); + instructions.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + instructions.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + instructions.setWidthMode(WidgetSizeMode.ABSOLUTE); + instructions.setHeightMode(WidgetSizeMode.ABSOLUTE); + instructions.setOriginalX(PADDING); + instructions.setOriginalY(selectionLabel.getOriginalY() + SLOT_TEXT_HEIGHT + 6); + instructions.setOriginalWidth(PANEL_WIDTH - PADDING * 2); + instructions.setOriginalHeight(28); + instructions.setXTextAlignment(WidgetTextAlignment.LEFT); + instructions.setYTextAlignment(WidgetTextAlignment.TOP); + instructions.setTextColor(INSTRUCTION_COLOR); + instructions.setText("Left-click a prayer to assign. Right-click a hotkey to clear."); + instructions.revalidate(); + + int gridStartY = instructions.getOriginalY() + SLOT_TEXT_HEIGHT + 6; + int gridStartX = PADDING; + int index = 0; + for (PrayerHotkeyOption option : PrayerHotkeyOption.values()) + { + if (option == PrayerHotkeyOption.NONE) + { + continue; + } + + final PrayerHotkeyOption assignOption = option; + int row = index / GRID_COLUMNS; + int column = index % GRID_COLUMNS; + + Widget prayerWidget = container.createChild(-1, WidgetType.GRAPHIC); + prayerWidget.setWidthMode(WidgetSizeMode.ABSOLUTE); + prayerWidget.setHeightMode(WidgetSizeMode.ABSOLUTE); + prayerWidget.setXPositionMode(WidgetPositionMode.ABSOLUTE_LEFT); + prayerWidget.setYPositionMode(WidgetPositionMode.ABSOLUTE_TOP); + prayerWidget.setOriginalX(gridStartX + column * (GRID_ICON_SIZE + GRID_GAP)); + prayerWidget.setOriginalY(gridStartY + row * (GRID_ICON_SIZE + GRID_GAP)); + prayerWidget.setOriginalWidth(GRID_ICON_SIZE); + prayerWidget.setOriginalHeight(GRID_ICON_SIZE); + prayerWidget.setSpriteId(assignOption.getSpriteId(true)); + prayerWidget.setAction(0, "Assign"); + prayerWidget.setHasListener(true); + prayerWidget.setOnOpListener((JavaScriptCallback) event -> handlePrayerSelection(assignOption, event)); + prayerWidget.setName(assignOption.getDisplayName()); + prayerWidget.setNoClickThrough(true); + prayerWidget.revalidate(); + + index++; + } + + updateSlotWidgets(); + updateSlotHighlights(); + updateSelectionLabel(); + } + + private void handleSlotClick(ScriptEvent event, int slotIndex) + { + if (event.getOp() == 2) + { + assignments.clearSlot(slotIndex); + updateSlotWidgets(); + if (slotIndex == selectedSlot) + { + updateSelectionLabel(); + } + return; + } + + setSelectedSlot(slotIndex); + } + + private void handlePrayerSelection(PrayerHotkeyOption option, ScriptEvent event) + { + if (event.getOp() != 1) + { + return; + } + + assignments.setSlot(selectedSlot, option); + updateSlotWidgets(); + updateSelectionLabel(); + } + + private void setSelectedSlot(int slotIndex) + { + if (slotIndex < 0 || slotIndex >= PrayerHotkeyAssignments.SLOT_COUNT) + { + return; + } + + selectedSlot = slotIndex; + updateSlotHighlights(); + updateSelectionLabel(); + } + + private void updateSlotWidgets() + { + if (slotIcons == null) + { + return; + } + + for (int i = 0; i < slotIcons.length; i++) + { + Widget icon = slotIcons[i]; + Widget plus = slotPlusLabels != null && i < slotPlusLabels.length ? slotPlusLabels[i] : null; + if (icon == null) + { + continue; + } + + PrayerHotkeyOption option = assignments.getSlot(i); + if (option != null && option != PrayerHotkeyOption.NONE) + { + icon.setHidden(false); + icon.setSpriteId(option.getSpriteId(true)); + icon.setName(option.getDisplayName()); + if (plus != null) + { + plus.setHidden(true); + } + } + else + { + icon.setHidden(true); + icon.setSpriteId(-1); + icon.setName("Empty"); + if (plus != null) + { + plus.setHidden(false); + } + } + } + } + + private void updateSlotHighlights() + { + if (slotBackgrounds == null) + { + return; + } + + for (int i = 0; i < slotBackgrounds.length; i++) + { + Widget background = slotBackgrounds[i]; + Widget label = slotLabels != null && i < slotLabels.length ? slotLabels[i] : null; + if (background != null) + { + background.setTextColor(i == selectedSlot ? SLOT_BACKGROUND_SELECTED : SLOT_BACKGROUND); + background.setOpacity(i == selectedSlot ? SLOT_OPACITY_SELECTED : SLOT_OPACITY); + } + if (label != null) + { + label.setTextColor(i == selectedSlot ? LABEL_COLOR_SELECTED : LABEL_COLOR); + } + } + } + + private void updateSelectionLabel() + { + if (selectionLabel == null) + { + return; + } + + PrayerHotkeyOption option = assignments.getSlot(selectedSlot); + selectionLabel.setText(String.format("Hotkey %d: %s", selectedSlot + 1, option.getDisplayName())); + } + + private Widget getChatWidget() + { + Widget widget = client.getWidget(WidgetInfo.CHATBOX_PARENT); + if (isLayer(widget)) + { + return widget; + } + + widget = client.getWidget(WidgetInfo.RESIZABLE_VIEWPORT_CHATBOX_PARENT); + if (isLayer(widget)) + { + return widget; + } + + widget = client.getWidget(WidgetInfo.RESIZABLE_VIEWPORT_BOTTOM_LINE_CHATBOX_PARENT); + if (isLayer(widget)) + { + return widget; + } + + return null; + } + + private boolean isLayer(Widget widget) + { + return widget != null && widget.getType() == WidgetType.LAYER; + } + + private void destroy() + { + if (container != null) + { + container.deleteAllChildren(); + container.setHidden(true); + container = null; + } + + slotBackgrounds = null; + slotIcons = null; + slotPlusLabels = null; + slotLabels = null; + selectionLabel = null; + } +}