diff --git a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java index 143c2a8fbb..b7bd5eed0d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java +++ b/src/main/java/net/runelite/client/plugins/microbot/PluginConstants.java @@ -34,6 +34,7 @@ private PluginConstants() public static final String NATE = "[N] "; public static final String SYN = "[Syn] "; public static final String BIGL = "[BL] "; + public static final String PERT = "[P] "; public static final String DV = "[DV] "; public static final boolean DEFAULT_ENABLED = false; diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java new file mode 100644 index 0000000000..f387be7e69 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastConfig.java @@ -0,0 +1,203 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Keybind; + +@ConfigGroup("leftclickcast") +public interface LeftClickCastConfig extends Config +{ + @ConfigItem( + keyName = "enabled", + name = "Enabled", + description = "Replace the left-click Attack option on NPCs with Cast Spell", + position = 0 + ) + default boolean enabled() + { + return true; + } + + // Retained so existing stored config is not invalidated. Read once at startUp for migration into slot1Spell. + @ConfigItem( + keyName = "spell", + name = "Spell", + description = "Legacy single-spell setting — migrated into Slot 1 on startup.", + position = 1 + ) + default PertTargetSpell spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "requireMagicWeapon", + name = "Require magic weapon", + description = "When enabled, the Cast entry is only inserted while a staff, bladed staff, powered staff, or powered wand is equipped. Disable to cast regardless of equipped weapon.", + position = 2 + ) + default boolean requireMagicWeapon() + { + return true; + } + + @ConfigSection( + name = "Spell Slots", + description = "Up to five spells that can be bound to hotkeys for mid-fight swapping.", + position = 10 + ) + String spellSlotsSection = "spellSlots"; + + @ConfigSection( + name = "Hotkeys", + description = "Hotkey bindings that switch the active spell slot.", + position = 11 + ) + String hotkeysSection = "hotkeys"; + + @ConfigItem( + keyName = "slot1Spell", + name = "Slot 1 Spell", + description = "Spell for slot 1 (the startup-active slot).", + section = spellSlotsSection, + position = 0 + ) + default PertTargetSpell slot1Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot2Spell", + name = "Slot 2 Spell", + description = "Spell for slot 2.", + section = spellSlotsSection, + position = 1 + ) + default PertTargetSpell slot2Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot3Spell", + name = "Slot 3 Spell", + description = "Spell for slot 3.", + section = spellSlotsSection, + position = 2 + ) + default PertTargetSpell slot3Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot4Spell", + name = "Slot 4 Spell", + description = "Spell for slot 4.", + section = spellSlotsSection, + position = 3 + ) + default PertTargetSpell slot4Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "slot5Spell", + name = "Slot 5 Spell", + description = "Spell for slot 5.", + section = spellSlotsSection, + position = 4 + ) + default PertTargetSpell slot5Spell() + { + return PertTargetSpell.FIRE_STRIKE; + } + + @ConfigItem( + keyName = "enabledToggleHotkey", + name = "Enable/Disable Hotkey", + description = "Hotkey that toggles the plugin on and off.", + section = hotkeysSection, + position = 0 + ) + default Keybind enabledToggleHotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot1Hotkey", + name = "Slot 1 Hotkey", + description = "Hotkey that activates slot 1.", + section = hotkeysSection, + position = 1 + ) + default Keybind slot1Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot2Hotkey", + name = "Slot 2 Hotkey", + description = "Hotkey that activates slot 2.", + section = hotkeysSection, + position = 2 + ) + default Keybind slot2Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot3Hotkey", + name = "Slot 3 Hotkey", + description = "Hotkey that activates slot 3.", + section = hotkeysSection, + position = 3 + ) + default Keybind slot3Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot4Hotkey", + name = "Slot 4 Hotkey", + description = "Hotkey that activates slot 4.", + section = hotkeysSection, + position = 4 + ) + default Keybind slot4Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "slot5Hotkey", + name = "Slot 5 Hotkey", + description = "Hotkey that activates slot 5.", + section = hotkeysSection, + position = 5 + ) + default Keybind slot5Hotkey() + { + return Keybind.NOT_SET; + } + + @ConfigItem( + keyName = "chatFeedback", + name = "Chat feedback", + description = "Post a game chat message on plugin events (active slot change, enable/disable toggle).", + section = hotkeysSection, + position = 6 + ) + default boolean chatFeedback() + { + return true; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java new file mode 100644 index 0000000000..0a0ed5f989 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/LeftClickCastPlugin.java @@ -0,0 +1,391 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import com.google.inject.Provides; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import net.runelite.api.Actor; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.EnumComposition; +import net.runelite.api.EnumID; +import net.runelite.api.MenuAction; +import net.runelite.api.MenuEntry; +import net.runelite.api.Menu; +import net.runelite.api.NPC; +import net.runelite.api.ParamID; +import net.runelite.api.Player; +import net.runelite.api.StructComposition; +import net.runelite.api.events.PostMenuSort; +import net.runelite.api.gameval.VarbitID; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.Keybind; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.events.ExternalPluginsChanged; +import net.runelite.client.input.KeyManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.api.widgets.Widget; +import net.runelite.client.plugins.microbot.PluginConstants; +import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; +import net.runelite.client.util.HotkeyListener; + +@PluginDescriptor( + name = PluginConstants.PERT + "Left-Click Cast", + description = "Replaces left-click Attack on NPCs with a preconfigured Cast Spell action.", + tags = {"magic", "combat", "spell", "left-click", "cast", "pvm", "pvp"}, + authors = {"Pert"}, + version = LeftClickCastPlugin.version, + minClientVersion = "2.0.13", + enabledByDefault = PluginConstants.DEFAULT_ENABLED, + isExternal = PluginConstants.IS_EXTERNAL +) +public class LeftClickCastPlugin extends Plugin +{ + static final String version = "1.3.0"; + + private static final int SLOT_COUNT = 5; + + @Inject + private Client client; + + @Inject + private LeftClickCastConfig config; + + @Inject + private KeyManager keyManager; + + @Inject + private ChatMessageManager chatMessageManager; + + @Inject + private ConfigManager configManager; + + @Inject + private EventBus eventBus; + + private volatile int activeSlot = 0; + + private final HotkeyListener[] hotkeyListeners = new HotkeyListener[SLOT_COUNT]; + + private HotkeyListener enabledToggleListener; + + @Provides + LeftClickCastConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(LeftClickCastConfig.class); + } + + @Override + protected void startUp() + { + // MicrobotConfigPanel renders boolean checkboxes from raw stored values; missing keys read as false + // even when the @ConfigItem default is true. Materialize defaults so the UI and the proxy agree. + configManager.setDefaultConfiguration(config, false); + activeSlot = 0; + for (int i = 0; i < SLOT_COUNT; i++) + { + final int slotIndex = i; + HotkeyListener listener = new HotkeyListener(() -> slotHotkeyFor(slotIndex)) + { + @Override + public void hotkeyPressed() + { + onSlotHotkey(slotIndex); + } + }; + hotkeyListeners[i] = listener; + keyManager.registerKeyListener(listener); + } + enabledToggleListener = new HotkeyListener(() -> config.enabledToggleHotkey()) + { + @Override + public void hotkeyPressed() + { + onEnabledToggleHotkey(); + } + }; + keyManager.registerKeyListener(enabledToggleListener); + migrateLegacySpellKey(); + } + + @Override + protected void shutDown() + { + for (int i = 0; i < hotkeyListeners.length; i++) + { + HotkeyListener listener = hotkeyListeners[i]; + if (listener != null) + { + keyManager.unregisterKeyListener(listener); + hotkeyListeners[i] = null; + } + } + if (enabledToggleListener != null) + { + keyManager.unregisterKeyListener(enabledToggleListener); + enabledToggleListener = null; + } + } + + @Subscribe + public void onPostMenuSort(PostMenuSort event) + { + // Don't mutate while the right-click menu is open — entries are frozen at open-time. + if (client.isMenuOpen()) + { + return; + } + if (!config.enabled()) + { + return; + } + PertTargetSpell spell = slotSpellFor(activeSlot); + if (spell == null) + { + return; + } + if (config.requireMagicWeapon() && !isMagicWeaponEquipped()) + { + return; + } + + Menu menu = client.getMenu(); + MenuEntry[] entries = menu.getMenuEntries(); + + // Find the top-most NPC or Player Attack entry (the game's already-sorted left-click candidate). + int attackIdx = -1; + Actor targetActor = null; + for (int i = entries.length - 1; i >= 0; i--) + { + MenuEntry e = entries[i]; + if (!"Attack".equals(e.getOption())) + { + continue; + } + if (e.getNpc() != null) + { + attackIdx = i; + targetActor = e.getNpc(); + break; + } + if (e.getPlayer() != null) + { + attackIdx = i; + targetActor = e.getPlayer(); + break; + } + } + if (attackIdx < 0) + { + return; + } + + MenuEntry attack = entries[attackIdx]; + final Actor dispatchTarget = targetActor; + final PertTargetSpell dispatchSpell = spell; + attack.setOption("Cast " + dispatchSpell.getDisplayName()); + attack.setType(MenuAction.RUNELITE); + attack.onClick(e -> castOnTargetFast(dispatchSpell, dispatchTarget)); + + // Move to the tail of the array — that slot is the left-click action in RuneLite's menu model. + if (attackIdx != entries.length - 1) + { + entries[attackIdx] = entries[entries.length - 1]; + entries[entries.length - 1] = attack; + menu.setMenuEntries(entries); + } + } + + private Keybind slotHotkeyFor(int index) + { + switch (index) + { + case 0: + return config.slot1Hotkey(); + case 1: + return config.slot2Hotkey(); + case 2: + return config.slot3Hotkey(); + case 3: + return config.slot4Hotkey(); + case 4: + return config.slot5Hotkey(); + default: + return Keybind.NOT_SET; + } + } + + private PertTargetSpell slotSpellFor(int index) + { + switch (index) + { + case 0: + return config.slot1Spell(); + case 1: + return config.slot2Spell(); + case 2: + return config.slot3Spell(); + case 3: + return config.slot4Spell(); + case 4: + return config.slot5Spell(); + default: + return config.slot1Spell(); + } + } + + private void onSlotHotkey(int index) + { + activeSlot = index; + if (config.chatFeedback()) + { + PertTargetSpell spell = slotSpellFor(index); + String display = spell != null ? spell.getDisplayName() : "(no spell)"; + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .value("Left-Click Cast: now casting " + display) + .build()); + } + } + + private void onEnabledToggleHotkey() + { + boolean newValue = !config.enabled(); + configManager.setConfiguration("leftclickcast", "enabled", newValue); + // MicrobotConfigPanel doesn't subscribe to ConfigChanged for individual checkbox refresh, but it does + // rebuild on ExternalPluginsChanged. Posting that here makes the open config panel re-read this and + // every other config item, so the "Enabled" checkbox visually flips to match the keybind toggle. + eventBus.post(new ExternalPluginsChanged()); + // Chat feedback is emitted by onConfigChanged so checkbox clicks and hotkey presses share one path. + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (!"leftclickcast".equals(event.getGroup()) || !"enabled".equals(event.getKey())) + { + return; + } + if (!config.chatFeedback()) + { + return; + } + boolean enabled = "true".equals(event.getNewValue()); + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.GAMEMESSAGE) + .value("Left-Click Cast: " + (enabled ? "enabled" : "disabled")) + .build()); + } + + // Fast-path cast: fire two synchronous client.menuAction packets back-to-back so the server processes + // the spell selection and the spell-on-target dispatch on the same game tick. Falls back to + // Rs2Magic.castOn (tab switch + sleeps + clicks) if the spellbook widget isn't loaded yet or the + // spell isn't on the current spellbook. + private void castOnTargetFast(PertTargetSpell spell, Actor target) + { + if (target == null) + { + return; + } + MagicAction magic = spell.getMagicAction(); + Widget magicRoot = client.getWidget(218, 0); + boolean widgetReady = magicRoot != null && magicRoot.getStaticChildren() != null; + if (widgetReady) + { + try + { + int spellWidgetId = magic.getWidgetId(); + // Packet 1: select the spell client-side (WIDGET_TARGET on the spell widget). + client.menuAction(-1, spellWidgetId, MenuAction.WIDGET_TARGET, 1, -1, "Cast", magic.getName()); + // Packet 2: dispatch the selected spell on the target, same tick. + if (target instanceof NPC) + { + NPC npc = (NPC) target; + client.menuAction(0, 0, MenuAction.WIDGET_TARGET_ON_NPC, npc.getIndex(), -1, "Use", npc.getName()); + } + else if (target instanceof Player) + { + Player p = (Player) target; + client.menuAction(0, 0, MenuAction.WIDGET_TARGET_ON_PLAYER, p.getId(), -1, "Use", p.getName()); + } + return; + } + catch (Exception ignored) + { + // Spell not on the active spellbook (e.g., modern while on ancients) — fall through. + } + } + else + { + // Spellbook widget not yet loaded this session; nudge it open so the next click is fast. + Rs2Tab.switchTo(InterfaceTab.MAGIC); + } + // Slow fallback path. Rs2Magic.castOn uses sleepUntil which is a no-op on the client thread, so dispatch async. + final Actor dispatch = target instanceof NPC ? new Rs2NpcModel((NPC) target) : target; + CompletableFuture.runAsync(() -> Rs2Magic.castOn(magic, dispatch)); + } + + // Best-effort: if the user had previously set the legacy `spell` key to a non-default value and + // slot1Spell is still at its default, copy the legacy value into slot1Spell so existing configs keep working. + private void migrateLegacySpellKey() + { + try + { + PertTargetSpell legacy = configManager.getConfiguration( + "leftclickcast", "spell", PertTargetSpell.class); + if (legacy == null || legacy == PertTargetSpell.FIRE_STRIKE) + { + return; + } + if (config.slot1Spell() != PertTargetSpell.FIRE_STRIKE) + { + return; + } + configManager.setConfiguration("leftclickcast", "slot1Spell", legacy); + } + catch (Exception ignored) + { + // Migration is best-effort; ignore any deserialization or storage errors. + } + } + + // A weapon counts as "magic" when its style struct exposes Casting or Defensive Casting. + // Mirrors the core AttackStylesPlugin logic (EnumID.WEAPON_STYLES + ParamID.ATTACK_STYLE_NAME). + private boolean isMagicWeaponEquipped() + { + int weaponType = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY); + EnumComposition weaponStyles = client.getEnum(EnumID.WEAPON_STYLES); + if (weaponStyles == null) + { + return false; + } + int styleEnumId = weaponStyles.getIntValue(weaponType); + if (styleEnumId == -1) + { + return false; + } + int[] styleStructs = client.getEnum(styleEnumId).getIntVals(); + for (int structId : styleStructs) + { + StructComposition sc = client.getStructComposition(structId); + if (sc == null) + { + continue; + } + String name = sc.getStringValue(ParamID.ATTACK_STYLE_NAME); + if ("Casting".equalsIgnoreCase(name) || "Defensive Casting".equalsIgnoreCase(name)) + { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java new file mode 100644 index 0000000000..521072613b --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leftclickcast/PertTargetSpell.java @@ -0,0 +1,96 @@ +package net.runelite.client.plugins.microbot.leftclickcast; + +import lombok.Getter; +import net.runelite.client.plugins.skillcalculator.skills.MagicAction; + +@Getter +public enum PertTargetSpell +{ + // Modern autocastable combat lines (Strike -> Surge) + WIND_STRIKE("Wind Strike", MagicAction.WIND_STRIKE), + WATER_STRIKE("Water Strike", MagicAction.WATER_STRIKE), + EARTH_STRIKE("Earth Strike", MagicAction.EARTH_STRIKE), + FIRE_STRIKE("Fire Strike", MagicAction.FIRE_STRIKE), + WIND_BOLT("Wind Bolt", MagicAction.WIND_BOLT), + WATER_BOLT("Water Bolt", MagicAction.WATER_BOLT), + EARTH_BOLT("Earth Bolt", MagicAction.EARTH_BOLT), + FIRE_BOLT("Fire Bolt", MagicAction.FIRE_BOLT), + WIND_BLAST("Wind Blast", MagicAction.WIND_BLAST), + WATER_BLAST("Water Blast", MagicAction.WATER_BLAST), + EARTH_BLAST("Earth Blast", MagicAction.EARTH_BLAST), + FIRE_BLAST("Fire Blast", MagicAction.FIRE_BLAST), + WIND_WAVE("Wind Wave", MagicAction.WIND_WAVE), + WATER_WAVE("Water Wave", MagicAction.WATER_WAVE), + EARTH_WAVE("Earth Wave", MagicAction.EARTH_WAVE), + FIRE_WAVE("Fire Wave", MagicAction.FIRE_WAVE), + WIND_SURGE("Wind Surge", MagicAction.WIND_SURGE), + WATER_SURGE("Water Surge", MagicAction.WATER_SURGE), + EARTH_SURGE("Earth Surge", MagicAction.EARTH_SURGE), + FIRE_SURGE("Fire Surge", MagicAction.FIRE_SURGE), + + // Ancient autocastable combat lines (Rush/Burst/Blitz/Barrage for each element) + SMOKE_RUSH("Smoke Rush", MagicAction.SMOKE_RUSH), + SHADOW_RUSH("Shadow Rush", MagicAction.SHADOW_RUSH), + BLOOD_RUSH("Blood Rush", MagicAction.BLOOD_RUSH), + ICE_RUSH("Ice Rush", MagicAction.ICE_RUSH), + SMOKE_BURST("Smoke Burst", MagicAction.SMOKE_BURST), + SHADOW_BURST("Shadow Burst", MagicAction.SHADOW_BURST), + BLOOD_BURST("Blood Burst", MagicAction.BLOOD_BURST), + ICE_BURST("Ice Burst", MagicAction.ICE_BURST), + SMOKE_BLITZ("Smoke Blitz", MagicAction.SMOKE_BLITZ), + SHADOW_BLITZ("Shadow Blitz", MagicAction.SHADOW_BLITZ), + BLOOD_BLITZ("Blood Blitz", MagicAction.BLOOD_BLITZ), + ICE_BLITZ("Ice Blitz", MagicAction.ICE_BLITZ), + SMOKE_BARRAGE("Smoke Barrage", MagicAction.SMOKE_BARRAGE), + SHADOW_BARRAGE("Shadow Barrage", MagicAction.SHADOW_BARRAGE), + BLOOD_BARRAGE("Blood Barrage", MagicAction.BLOOD_BARRAGE), + ICE_BARRAGE("Ice Barrage", MagicAction.ICE_BARRAGE), + + // Non-autocastable combat spells + CRUMBLE_UNDEAD("Crumble Undead", MagicAction.CRUMBLE_UNDEAD), + IBAN_BLAST("Iban Blast", MagicAction.IBAN_BLAST), + MAGIC_DART("Magic Dart", MagicAction.MAGIC_DART), + SARADOMIN_STRIKE("Saradomin Strike", MagicAction.SARADOMIN_STRIKE), + CLAWS_OF_GUTHIX("Claws of Guthix", MagicAction.CLAWS_OF_GUTHIX), + FLAMES_OF_ZAMORAK("Flames of Zamorak", MagicAction.FLAMES_OF_ZAMORAK), + + // Arceuus offensive target spells + GHOSTLY_GRASP("Ghostly Grasp", MagicAction.GHOSTLY_GRASP), + SKELETAL_GRASP("Skeletal Grasp", MagicAction.SKELETAL_GRASP), + UNDEAD_GRASP("Undead Grasp", MagicAction.UNDEAD_GRASP), + INFERIOR_DEMONBANE("Inferior Demonbane", MagicAction.INFERIOR_DEMONBANE), + SUPERIOR_DEMONBANE("Superior Demonbane", MagicAction.SUPERIOR_DEMONBANE), + DARK_DEMONBANE("Dark Demonbane", MagicAction.DARK_DEMONBANE), + LESSER_CORRUPTION("Lesser Corruption", MagicAction.LESSER_CORRUPTION), + GREATER_CORRUPTION("Greater Corruption", MagicAction.GREATER_CORRUPTION), + + // Utility target spells + CONFUSE("Confuse", MagicAction.CONFUSE), + WEAKEN("Weaken", MagicAction.WEAKEN), + CURSE("Curse", MagicAction.CURSE), + BIND("Bind", MagicAction.BIND), + SNARE("Snare", MagicAction.SNARE), + ENTANGLE("Entangle", MagicAction.ENTANGLE), + VULNERABILITY("Vulnerability", MagicAction.VULNERABILITY), + ENFEEBLE("Enfeeble", MagicAction.ENFEEBLE), + STUN("Stun", MagicAction.STUN), + TELE_BLOCK("Tele Block", MagicAction.TELE_BLOCK), + TELEOTHER_LUMBRIDGE("Tele Other Lumbridge", MagicAction.TELEOTHER_LUMBRIDGE), + TELEOTHER_FALADOR("Tele Other Falador", MagicAction.TELEOTHER_FALADOR), + TELEOTHER_CAMELOT("Tele Other Camelot", MagicAction.TELEOTHER_CAMELOT); + + private final String displayName; + private final MagicAction magicAction; + + PertTargetSpell(String displayName, MagicAction magicAction) + { + this.displayName = displayName; + this.magicAction = magicAction; + } + + @Override + public String toString() + { + return displayName; + } +} diff --git a/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md new file mode 100644 index 0000000000..be60ed19ae --- /dev/null +++ b/src/main/resources/net/runelite/client/plugins/microbot/leftclickcast/docs/README.md @@ -0,0 +1,54 @@ +# Left-Click Cast + +Replaces the left-click **Attack** option on attackable NPCs and on players (wilderness / PvP) with a preconfigured **Cast Spell** action. The plugin stays invisible when you swap to a melee or ranged weapon, so leaving it enabled is safe. + +## How it works + +When an "Attack" menu entry is added for an NPC, the plugin inserts a new menu entry for the selected spell and places it above Attack, making the spell the left-click action. + +All casting is dispatched through the Microbot client's existing `Rs2Magic.castOn(MagicAction, Actor)` — the plugin does not re-implement rune checks, spellbook switching, or targeting. + +## Configuration + +| Option | Default | Description | +| --- | --- | --- | +| **Enabled** | `true` | Master switch. When off, no menu entries are inserted. | +| **Spell** | `Fire Strike` | Legacy single-spell setting. On startup the plugin migrates this into **Slot 1 Spell** if Slot 1 is still at its default — keeps existing configs working without any manual action. | +| **Require magic weapon** | `true` | When on, the Cast entry only shows while a staff, bladed staff, powered staff, or powered wand is equipped (detected via varbit `EQUIPPED_WEAPON_TYPE`). Disable to cast regardless of weapon. | + +### Spell slots and hotkeys + +The plugin exposes five independently configurable spell slots, grouped under two sections in the config panel: + +| Section | Options | +| --- | --- | +| **Spell Slots** | `Slot 1 Spell` … `Slot 5 Spell` — each picks any spell from the full supported-spells dropdown. All five default to `Fire Strike`. | +| **Hotkeys** | `Slot 1 Hotkey` … `Slot 5 Hotkey` — RuneLite-standard hotkey pickers, all unbound by default. `Chat feedback on slot change` — toggles the chat message posted when a hotkey switches slots (default on). | + +**How slot switching works:** + +- **Slot 1 is always active at startup.** Enabling the plugin (or restarting the client) resets the active slot to Slot 1. The active slot is runtime-only — it is never written to config. +- **Press a bound slot hotkey** (while the game window is focused and no text field is active) to make that slot the active slot. The next menu-sort uses the new slot's spell. +- **Unbound hotkeys are inert.** A slot whose hotkey is `Not set` cannot be activated by keypress. RuneLite's hotkey plumbing suppresses hotkeys while you're typing in a chat or search widget, so hotkey letters won't accidentally swap slots during text entry. +- **Slot 1 needs no hotkey.** Because it's the startup default, leave its hotkey unbound unless you want to explicitly return to it from another slot. +- **Chat feedback** (when enabled) prints `Left-Click Cast: now casting ` on every slot change. Toggle it off if it's noisy during combat rotations. + +## Limitations + +- **No rune auto-management.** If you run out of runes, the cast fails cleanly and you see the normal "You do not have enough ..." chat message. +- **No auto-spellbook switching.** If the selected spell is not on your current spellbook, the cast fails silently. Switch spellbooks manually. +- **The dropdown is the source of truth.** Spells not listed in the dropdown are not supported by this plugin. +- **Staff-only default.** With `Require magic weapon` enabled (default), non-magic weapon types produce normal Attack behavior. Disable the toggle if you want to cast from melee/ranged weapons as well. +- **Cooperative menu composition.** If another plugin inserts menu entries after this one on the same tick, its entry becomes the top entry instead — a known limitation of RuneLite's menu model. + +## Supported spells + +**Modern combat (Strike → Surge):** Wind Strike, Water Strike, Earth Strike, Fire Strike, Wind Bolt, Water Bolt, Earth Bolt, Fire Bolt, Wind Blast, Water Blast, Earth Blast, Fire Blast, Wind Wave, Water Wave, Earth Wave, Fire Wave, Wind Surge, Water Surge, Earth Surge, Fire Surge. + +**Ancient combat:** Smoke / Shadow / Blood / Ice — Rush, Burst, Blitz, Barrage. + +**Non-autocastable combat:** Crumble Undead, Iban Blast, Magic Dart, Saradomin Strike, Claws of Guthix, Flames of Zamorak. + +**Arceuus offensive:** Ghostly Grasp, Skeletal Grasp, Undead Grasp, Inferior Demonbane, Superior Demonbane, Dark Demonbane, Lesser Corruption, Greater Corruption. + +**Utility target spells:** Confuse, Weaken, Curse, Bind, Snare, Entangle, Vulnerability, Enfeeble, Stun, Tele Block, Tele Other (Lumbridge / Falador / Camelot). diff --git a/src/test/java/net/runelite/client/Microbot.java b/src/test/java/net/runelite/client/Microbot.java index 0bbce8830b..8933287b08 100644 --- a/src/test/java/net/runelite/client/Microbot.java +++ b/src/test/java/net/runelite/client/Microbot.java @@ -10,6 +10,7 @@ import net.runelite.client.plugins.microbot.astralrc.AstralRunesPlugin; import net.runelite.client.plugins.microbot.autofishing.AutoFishingPlugin; import net.runelite.client.plugins.microbot.example.ExamplePlugin; +import net.runelite.client.plugins.microbot.leftclickcast.LeftClickCastPlugin; import net.runelite.client.plugins.microbot.sailing.MSailingPlugin; import net.runelite.client.plugins.microbot.thieving.ThievingPlugin; import net.runelite.client.plugins.microbot.woodcutting.AutoWoodcuttingPlugin; @@ -20,7 +21,8 @@ public class Microbot private static final Class[] debugPlugins = { AIOFighterPlugin.class, - AgentServerPlugin.class + AgentServerPlugin.class, + LeftClickCastPlugin.class }; public static void main(String[] args) throws Exception