From 7816c405bf7e31ef1fdf8da3e429a2cfca9e0dae Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 17 Apr 2026 19:16:41 -0700 Subject: [PATCH] feat(LeaguesToolkit): add Toci's Gem Store + Transmutation features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toci's Gem Store — three modes: - Buy & Bank: fast stockpile uncut gems via briefcase - Buy, Cut & Sell: buy, cut with chisel, sell cut back to Toci - Buy, Cut & Bank: buy, cut, bank via briefcase Walks to Toci automatically, mass-clicks buy/sell at 100-250ms, handles chisel + SPACE dialog for cutting, briefcase Last-destination for banking, Rs2Walker for return trips. Transmutation — casts Alchemic Divergence/Convergence on noted items: - Dropdown item selection for start/target (all categories) - Auto-detects category, walks chain tier by tier - Non-blocking tick-based monitoring with shop-aware timeout - Re-casts if auto-recast interrupted (45s normal, 3min if shop open) Categories: Ores, Fish, Gems, Runes, Logs, Bones, Hides, Ashes, Compost. Also updates info panel with full feature descriptions. Bumps version to 1.2.0. Work in progress. --- .../microbot/leaguestoolkit/GemCutter.java | 390 ++++++++++++++++++ .../leaguestoolkit/GemCutterMode.java | 19 + .../leaguestoolkit/GemCutterState.java | 11 + .../microbot/leaguestoolkit/GemType.java | 27 ++ .../leaguestoolkit/LeaguesToolkitConfig.java | 124 +++++- .../leaguestoolkit/LeaguesToolkitPlugin.java | 2 +- .../leaguestoolkit/LeaguesToolkitScript.java | 34 ++ .../leaguestoolkit/TransmuteCategory.java | 69 ++++ .../leaguestoolkit/TransmuteDirection.java | 6 + .../leaguestoolkit/TransmuteItem.java | 114 +++++ .../microbot/leaguestoolkit/Transmuter.java | 229 ++++++++++ 11 files changed, 1022 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterMode.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteCategory.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteDirection.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteItem.java create mode 100644 src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/Transmuter.java diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java new file mode 100644 index 0000000000..19d0002d56 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutter.java @@ -0,0 +1,390 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; + +import java.awt.event.KeyEvent; + +import static net.runelite.client.plugins.microbot.util.Global.sleep; +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@Slf4j +public class GemCutter { + + private static final WorldPoint TOCI_LOCATION = new WorldPoint(1428, 2975, 0); + private static final String TOCI_NPC_NAME = "Toci"; + private static final String CHISEL_NAME = "Chisel"; + private static final int COINS_ID = 995; + private static final String BRIEFCASE_NAME = "Banker's briefcase"; + + @Getter + private GemCutterState state = GemCutterState.WALKING_TO_SHOP; + @Getter + private String status = "Idle"; + + public void reset() { + state = GemCutterState.WALKING_TO_SHOP; + status = "Idle"; + } + + public boolean tick(LeaguesToolkitConfig config) { + GemType gem = config.gemType(); + if (gem == null) { + status = "No gem selected"; + log.info("[GemCutter] tick: no gem selected"); + return false; + } + + log.info("[GemCutter] tick: state={}, cutGems={}, mode={}", state, shouldCut(config), config.gemCutterMode()); + + // Need to bank for coins if idle with low funds + if (state == GemCutterState.WALKING_TO_SHOP + && Rs2Inventory.itemQuantity(COINS_ID) < config.gemCutterMinCoins() + && !Rs2Inventory.hasItem(gem.getUncutName()) + && !Rs2Inventory.hasItem(gem.getCutName())) { + state = GemCutterState.BANKING; + } + + switch (state) { + case BANKING: + return handleBanking(config); + case WALKING_TO_SHOP: + return handleWalkingToShop(); + case BUYING: + return handleBuying(gem, config); + case CUTTING: + return handleCutting(gem, config); + case SELLING: + return handleSelling(gem, config); + case BRIEFCASE_BANKING: + return handleBriefcaseBanking(gem, config); + case TELEPORTING_BACK: + return handleTeleportingBack(); + } + return true; + } + + private boolean handleBanking(LeaguesToolkitConfig config) { + if (Rs2Shop.isOpen()) { + Rs2Shop.closeShop(); + return true; + } + + if (Rs2Equipment.isWearing(BRIEFCASE_NAME)) { + if (!Rs2Bank.isOpen()) { + status = "Using equipped briefcase to bank"; + Rs2Equipment.interact(BRIEFCASE_NAME, "Last-destination"); + sleepUntil(Rs2Bank::isOpen, 5000); + return true; + } + } else if (Rs2Inventory.hasItem(BRIEFCASE_NAME)) { + if (!Rs2Bank.isOpen()) { + status = "Using briefcase to bank"; + Rs2Inventory.interact(BRIEFCASE_NAME, "Bank"); + sleepUntil(Rs2Bank::isOpen, 5000); + return true; + } + } else { + if (!Rs2Bank.isOpen()) { + status = "Walking to bank"; + if (!Rs2Bank.walkToBankAndUseBank()) return true; + } + } + + if (!Rs2Bank.isOpen()) return true; + + status = "Withdrawing coins"; + if (!Rs2Bank.hasItem(COINS_ID)) { + status = "No coins in bank — stopping"; + Rs2Bank.closeBank(); + return false; + } + + Rs2Bank.withdrawAll(COINS_ID); + Rs2Bank.closeBank(); + state = GemCutterState.WALKING_TO_SHOP; + return true; + } + + private boolean handleWalkingToShop() { + log.info("[GemCutter] handleWalkingToShop entered"); + + if (Rs2Shop.isOpen()) { + log.info("[GemCutter] Shop already open — transitioning to BUYING"); + status = "Shop already open"; + state = GemCutterState.BUYING; + return true; + } + + WorldPoint playerPos = Rs2Player.getWorldLocation(); + if (playerPos == null) { + log.info("[GemCutter] Player position is null"); + status = "Waiting for player position..."; + return true; + } + + int distance = playerPos.distanceTo(TOCI_LOCATION); + log.info("[GemCutter] Player at {}, Toci at {}, distance={}", playerPos, TOCI_LOCATION, distance); + + // Far away — walk first, don't try to open shop + if (distance > 15) { + status = "Walking to Toci (" + distance + " tiles away)"; + log.info("[GemCutter] Walking to Toci..."); + Rs2Walker.walkTo(TOCI_LOCATION, 4); + sleep(3000, 5000); + return true; + } + + // Close enough — try to open shop + status = "Near Toci — opening shop"; + log.info("[GemCutter] Close enough, opening shop"); + boolean opened = Rs2Shop.openShop(TOCI_NPC_NAME); + log.info("[GemCutter] openShop returned {}", opened); + if (opened) { + sleepUntil(Rs2Shop::isOpen, 5000); + if (Rs2Shop.isOpen()) { + state = GemCutterState.BUYING; + return true; + } + } + return true; + } + + private boolean shouldCut(LeaguesToolkitConfig config) { + GemCutterMode mode = config.gemCutterMode(); + return mode == GemCutterMode.BUY_CUT_SELL || mode == GemCutterMode.BUY_CUT_BANK; + } + + private boolean shouldUseBriefcase(LeaguesToolkitConfig config) { + GemCutterMode mode = config.gemCutterMode(); + return mode == GemCutterMode.BUY_AND_BANK || mode == GemCutterMode.BUY_CUT_BANK; + } + + private GemCutterState nextStateAfterCutting(LeaguesToolkitConfig config) { + return shouldUseBriefcase(config) + ? GemCutterState.BRIEFCASE_BANKING + : GemCutterState.SELLING; + } + + private boolean handleBuying(GemType gem, LeaguesToolkitConfig config) { + if (!Rs2Shop.isOpen()) { + status = "Opening Toci's shop"; + if (!Rs2Shop.openShop(TOCI_NPC_NAME)) { + status = "Could not open shop"; + return true; + } + sleepUntil(Rs2Shop::isOpen, 3000); + return true; + } + + int uncutCount = Rs2Inventory.count(gem.getUncutName()); + + if (Rs2Inventory.isFull()) { + status = "Inventory full"; + Rs2Shop.closeShop(); + state = shouldCut(config) ? GemCutterState.CUTTING : nextStateAfterCutting(config); + return true; + } + + if (!Rs2Shop.hasStock(gem.getUncutName())) { + if (uncutCount > 0) { + status = "Shop out of stock"; + Rs2Shop.closeShop(); + state = shouldCut(config) ? GemCutterState.CUTTING : nextStateAfterCutting(config); + return true; + } + status = "Shop out of " + gem.getUncutName() + " — waiting"; + sleep(1500, 2500); + return true; + } + + // Mass-click buy at 100-250ms intervals + status = "Rapid-buying " + gem.getUncutName(); + int safetyMax = 32; + int missedInRow = 0; + for (int i = 0; i < safetyMax; i++) { + if (!Rs2Shop.isOpen()) break; + int before = Rs2Inventory.count(gem.getUncutName()); + Rs2Shop.buyItem(gem.getUncutName(), "1"); + sleep(Rs2Random.between(100, 250)); + if (Rs2Inventory.count(gem.getUncutName()) > before) { + missedInRow = 0; + } else { + missedInRow++; + if (missedInRow >= 2) break; + } + } + return true; + } + + private boolean handleCutting(GemType gem, LeaguesToolkitConfig config) { + // Skip cutting if disabled OR no chisel — go straight to sell/bank + if (!shouldCut(config) || !Rs2Inventory.hasItem(CHISEL_NAME)) { + log.info("[GemCutter] Skipping CUTTING (cutGems={}, hasChisel={}), going to next state", + shouldCut(config), Rs2Inventory.hasItem(CHISEL_NAME)); + state = nextStateAfterCutting(config); + return true; + } + + if (!Rs2Inventory.hasItem(gem.getUncutName())) { + status = "All gems cut"; + if (shouldUseBriefcase(config)) { + state = GemCutterState.BRIEFCASE_BANKING; + } else { + state = GemCutterState.SELLING; + } + return true; + } + + status = "Starting to cut " + gem.getCutName(); + Rs2Inventory.use(CHISEL_NAME); + sleep(300, 500); + Rs2Inventory.use(gem.getUncutName()); + + sleep(600, 900); + Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); + + sleep(2000, 3000); + status = "Cutting " + gem.getCutName() + "..."; + sleepUntil(() -> !Microbot.isGainingExp || !Rs2Inventory.hasItem(gem.getUncutName()), 60000); + + return true; + } + + // === SELL MODE === + + private boolean handleSelling(GemType gem, LeaguesToolkitConfig config) { + if (!Rs2Inventory.hasItem(gem.getCutName())) { + status = "All cut gems sold — looping"; + if (Rs2Inventory.itemQuantity(COINS_ID) < config.gemCutterMinCoins()) { + if (Rs2Shop.isOpen()) Rs2Shop.closeShop(); + state = GemCutterState.BANKING; + } else { + // Keep shop open for next buy cycle + state = GemCutterState.BUYING; + } + return true; + } + + if (!Rs2Shop.isOpen()) { + status = "Reopening shop to sell"; + if (!Rs2Shop.openShop(TOCI_NPC_NAME)) { + status = "Could not reopen shop"; + return true; + } + sleepUntil(Rs2Shop::isOpen, 3000); + return true; + } + + // Mass-click sell from bottom of inventory + int initialCount = Rs2Inventory.count(gem.getCutName()); + status = "Rapid-selling " + gem.getCutName() + " x" + initialCount; + + int safetyMax = initialCount + 5; + int missedInRow = 0; + for (int i = 0; i < safetyMax; i++) { + if (!Rs2Shop.isOpen()) break; + if (!Rs2Inventory.hasItem(gem.getCutName())) break; + + Rs2ItemModel last = Rs2Inventory.items(item -> + gem.getCutName().equalsIgnoreCase(item.getName())) + .reduce((a, b) -> b) + .orElse(null); + if (last == null) break; + int before = Rs2Inventory.count(gem.getCutName()); + + Rs2Inventory.slotInteract(last.getSlot(), "Sell 1"); + sleep(Rs2Random.between(100, 250)); + + if (Rs2Inventory.count(gem.getCutName()) < before) { + missedInRow = 0; + } else { + missedInRow++; + if (missedInRow >= 2) break; + } + } + + return true; + } + + // === BRIEFCASE BANKING MODE === + + private boolean handleBriefcaseBanking(GemType gem, LeaguesToolkitConfig config) { + log.info("[GemCutter] handleBriefcaseBanking: bankOpen={}, wearing={}, hasInInv={}", + Rs2Bank.isOpen(), + Rs2Equipment.isWearing(BRIEFCASE_NAME), + Rs2Inventory.hasItem(BRIEFCASE_NAME)); + + // Step 1: Teleport to bank via briefcase, then open bank + if (!Rs2Bank.isOpen()) { + // First teleport to the bank + status = "Teleporting to bank via briefcase"; + if (Rs2Equipment.isWearing(BRIEFCASE_NAME)) { + log.info("[GemCutter] Clicking equipped briefcase 'Last-destination'"); + Rs2Equipment.interact(BRIEFCASE_NAME, "Last-destination"); + } else { + status = "No briefcase equipped — walking to bank"; + if (!Rs2Bank.walkToBankAndUseBank()) return true; + sleepUntil(Rs2Bank::isOpen, 10000); + return true; + } + + // Wait for teleport to finish + sleep(2000, 3000); + sleepUntil(() -> !Rs2Player.isAnimating() && !Rs2Player.isMoving(), 8000); + sleep(500, 1000); + + // Now open the bank normally + status = "Opening bank"; + log.info("[GemCutter] Teleported, now opening bank"); + Rs2Bank.openBank(); + sleepUntil(Rs2Bank::isOpen, 5000); + log.info("[GemCutter] Bank open: {}", Rs2Bank.isOpen()); + return true; + } + + // Step 2: Deposit gems (cut or uncut depending on config) + status = "Depositing gems"; + if (shouldCut(config) && Rs2Inventory.hasItem(gem.getCutName())) { + Rs2Bank.depositAll(gem.getCutName()); + sleep(300, 500); + } + if (!shouldCut(config) && Rs2Inventory.hasItem(gem.getUncutName())) { + Rs2Bank.depositAll(gem.getUncutName()); + sleep(300, 500); + } + + // Step 3: Withdraw coins if low + if (Rs2Inventory.itemQuantity(COINS_ID) < config.gemCutterMinCoins() && Rs2Bank.hasItem(COINS_ID)) { + Rs2Bank.withdrawAll(COINS_ID); + sleep(300, 500); + } + + Rs2Bank.closeBank(); + sleepUntil(() -> !Rs2Bank.isOpen(), 3000); + + state = GemCutterState.TELEPORTING_BACK; + return true; + } + + private boolean handleTeleportingBack() { + // Walk back to Toci using the web walker + status = "Walking back to Toci"; + log.info("[GemCutter] TELEPORTING_BACK → transitioning to WALKING_TO_SHOP"); + state = GemCutterState.WALKING_TO_SHOP; + return true; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterMode.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterMode.java new file mode 100644 index 0000000000..7334b38e7d --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterMode.java @@ -0,0 +1,19 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum GemCutterMode { + BUY_AND_BANK("Buy & Bank (fast stockpile uncut gems via briefcase)"), + BUY_CUT_SELL("Buy, Cut & Sell (buy uncut, cut, sell cut back to Toci)"), + BUY_CUT_BANK("Buy, Cut & Bank (buy uncut, cut, bank via briefcase)"); + + private final String description; + + @Override + public String toString() { + return description; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java new file mode 100644 index 0000000000..02c7aabaa1 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemCutterState.java @@ -0,0 +1,11 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +public enum GemCutterState { + BANKING, + WALKING_TO_SHOP, + BUYING, + CUTTING, + SELLING, + BRIEFCASE_BANKING, + TELEPORTING_BACK +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java new file mode 100644 index 0000000000..9e29e52f1e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/GemType.java @@ -0,0 +1,27 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Skill; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +@Getter +@RequiredArgsConstructor +public enum GemType { + SAPPHIRE("Uncut sapphire", "Sapphire", 20), + EMERALD("Uncut emerald", "Emerald", 27), + RUBY("Uncut ruby", "Ruby", 34); + + private final String uncutName; + private final String cutName; + private final int craftingLevel; + + public boolean hasRequiredLevel() { + return Rs2Player.getSkillRequirement(Skill.CRAFTING, craftingLevel); + } + + @Override + public String toString() { + return cutName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java index 0e6dbc4fb3..093a61c5eb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitConfig.java @@ -10,8 +10,19 @@ @ConfigGroup("LeaguesToolkit") @ConfigInformation("

Leagues Toolkit

" + "

Version: " + LeaguesToolkitPlugin.version + "

" + - "

A grab-bag of Leagues-focused utilities. Start with Anti-AFK to keep long, " + - "auto-banking skilling sessions from getting logged out.

") + "

Anti-AFK: Presses a random arrow key before the idle timer kicks in. " + + "Great for long AFK sessions with auto-bank relics (e.g. Endless Harvest).

" + + "

Toci's Gem Store: Walks to Toci in Aldarin, buys uncut gems, " + + "and either sells cut gems back or banks them via the Banker's Briefcase. Three modes:

" + + "" + + "

Transmutation: Casts Alchemic Divergence or Convergence on noted items " + + "to upgrade/downgrade through tiers (e.g. Iron ore all the way to Runite ore). " + + "Have the starting items noted in your inventory before enabling. " + + "Requires the Transmutation relic and the transmutation ledger.

") public interface LeaguesToolkitConfig extends Config { @ConfigSection( @@ -66,4 +77,113 @@ default int antiAfkBufferMin() { default int antiAfkBufferMax() { return 1500; } + + @ConfigSection( + name = "Toci's Gem Store", + description = "Automated gem buying, cutting, and selling/banking at Toci in Aldarin", + position = 1, + closedByDefault = true + ) + String gemCutterSection = "gemCutterSection"; + + @ConfigItem( + keyName = "enableGemCutter", + name = "Enable", + description = "Walks to Toci's Gem Store in Aldarin and runs the selected mode. Requires coins in inventory.", + position = 0, + section = gemCutterSection + ) + default boolean enableGemCutter() { + return false; + } + + @ConfigItem( + keyName = "gemCutterMode", + name = "Mode", + description = "Buy & Bank: fast stockpile uncut gems (briefcase required). " + + "Buy, Cut & Sell: buy uncut, cut with chisel, sell cut back to Toci. " + + "Buy, Cut & Bank: buy uncut, cut, bank via briefcase.", + position = 1, + section = gemCutterSection + ) + default GemCutterMode gemCutterMode() { + return GemCutterMode.BUY_AND_BANK; + } + + @ConfigItem( + keyName = "gemType", + name = "Gem", + description = "Which gem to buy/cut. Cut modes require a chisel and the crafting level.", + position = 2, + section = gemCutterSection + ) + default GemType gemType() { + return GemType.RUBY; + } + + @Range(min = 1000, max = 1_000_000) + @ConfigItem( + keyName = "gemCutterMinCoins", + name = "Min coins to keep", + description = "When coins drop below this, withdraw more from the bank", + position = 3, + section = gemCutterSection + ) + default int gemCutterMinCoins() { + return 10_000; + } + + @ConfigSection( + name = "Transmutation", + description = "Casts Alchemic Divergence/Convergence to upgrade or downgrade noted items through tiers", + position = 2, + closedByDefault = true + ) + String transmuteSection = "transmuteSection"; + + @ConfigItem( + keyName = "enableTransmute", + name = "Enable transmutation", + description = "Have the starting noted items in your inventory before enabling. " + + "The script casts the spell on each tier until it reaches the target. " + + "Requires the Transmutation relic and the transmutation ledger equipped or in inventory.", + position = 0, + section = transmuteSection + ) + default boolean enableTransmute() { + return false; + } + + @ConfigItem( + keyName = "transmuteStartItem", + name = "Starting item", + description = "The item you currently have noted in your inventory. Must be in the same category as the target.", + position = 1, + section = transmuteSection + ) + default TransmuteItem transmuteStartItem() { + return TransmuteItem.IRON_ORE; + } + + @ConfigItem( + keyName = "transmuteTargetItem", + name = "Target item", + description = "The final item you want. Must be in the same category as the starting item.", + position = 2, + section = transmuteSection + ) + default TransmuteItem transmuteTargetItem() { + return TransmuteItem.RUNITE_ORE; + } + + @ConfigItem( + keyName = "transmuteDirection", + name = "Direction", + description = "Upgrade (Alchemic Divergence / High Alch) or Downgrade (Alchemic Convergence / Low Alch)", + position = 4, + section = transmuteSection + ) + default TransmuteDirection transmuteDirection() { + return TransmuteDirection.UPGRADE; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java index bbec460109..c1f55f5ea3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitPlugin.java @@ -20,7 +20,7 @@ ) @Slf4j public class LeaguesToolkitPlugin extends Plugin { - public static final String version = "1.0.0"; + public static final String version = "1.2.0"; @Inject private LeaguesToolkitConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java index fafd6f3760..c7eabc88a3 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/LeaguesToolkitScript.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.leaguestoolkit; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; @@ -18,6 +19,14 @@ public class LeaguesToolkitScript extends Script { KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN }; + @Getter + private final GemCutter gemCutter = new GemCutter(); + @Getter + private final Transmuter transmuter = new Transmuter(); + + private boolean gemCutterWasEnabled = false; + private boolean transmuteWasEnabled = false; + public boolean run(LeaguesToolkitConfig config) { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { @@ -27,6 +36,30 @@ public boolean run(LeaguesToolkitConfig config) { if (config.enableAntiAfk()) { runAntiAfk(config); } + + if (config.enableGemCutter()) { + if (!gemCutterWasEnabled) { + gemCutter.reset(); + gemCutterWasEnabled = true; + log.info("[LeaguesToolkit] Gem cutter enabled — state: {}", gemCutter.getState()); + } + gemCutter.tick(config); + } else { + gemCutterWasEnabled = false; + } + + if (config.enableTransmute()) { + if (!transmuteWasEnabled) { + transmuter.reset(); + transmuteWasEnabled = true; + } + if (!transmuter.tick(config)) { + // Transmuter finished or errored — keep running plugin but stop transmuting + log.info("Transmuter stopped: {}", transmuter.getStatus()); + } + } else { + transmuteWasEnabled = false; + } } catch (Exception ex) { log.error("LeaguesToolkitScript loop error", ex); } @@ -58,5 +91,6 @@ private void runAntiAfk(LeaguesToolkitConfig config) { @Override public void shutdown() { super.shutdown(); + transmuter.reset(); } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteCategory.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteCategory.java new file mode 100644 index 0000000000..2dd474bffa --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteCategory.java @@ -0,0 +1,69 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public enum TransmuteCategory { + ORES("Ores", Arrays.asList( + "Tin ore", "Copper ore", "Iron ore", "Coal", "Mithril ore", "Adamantite ore", "Runite ore" + )), + FISH("Fish", Arrays.asList( + "Raw shrimps", "Raw sardine", "Raw herring", "Raw mackerel", "Raw trout", + "Raw cod", "Raw pike", "Raw salmon", "Raw tuna", "Raw lobster", + "Raw bass", "Raw swordfish", "Raw karambwan", "Raw shark", "Raw anglerfish" + )), + GEMS("Gems", Arrays.asList( + "Uncut sapphire", "Uncut emerald", "Uncut ruby", "Uncut diamond", + "Uncut opal", "Uncut jade", "Uncut red topaz", "Uncut dragonstone" + )), + RUNES("Runes", Arrays.asList( + "Air rune", "Water rune", "Earth rune", "Fire rune", + "Chaos rune", "Nature rune", "Cosmic rune", "Law rune", + "Death rune", "Astral rune", "Blood rune", "Soul rune", "Wrath rune" + )), + ASHES("Ashes", Arrays.asList( + "Ashes", "Volcanic ash", "Fiendish ashes", "Vile ashes", + "Malicious ashes", "Abyssal ashes", "Infernal ashes" + )), + COMPOST("Compost", Arrays.asList( + "Compost", "Supercompost", "Ultracompost" + )), + LOGS("Logs", Arrays.asList( + "Logs", "Oak logs", "Willow logs", "Teak logs", "Maple logs", + "Mahogany logs", "Yew logs", "Magic logs", "Redwood logs" + )), + BONES("Bones", Arrays.asList( + "Bones", "Bat bones", "Big bones", "Wyrmling bones", "Baby dragon bones", + "Wyrm bones", "Dragon bones", "Drake bones", "Lava dragon bones", + "Hydra bones", "Dagannoth bones", "Superior dragon bones" + )), + HIDES("Hides", Arrays.asList( + "Cowhide", "Snakeskin", "Green dragonhide", "Blue dragonhide", + "Red dragonhide", "Black dragonhide" + )); + + private final String displayName; + private final List chain; + + public int indexOf(String itemName) { + for (int i = 0; i < chain.size(); i++) { + if (chain.get(i).equalsIgnoreCase(itemName)) return i; + } + return -1; + } + + public String getItem(int index) { + if (index < 0 || index >= chain.size()) return null; + return chain.get(index); + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteDirection.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteDirection.java new file mode 100644 index 0000000000..46cdf9f42c --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteDirection.java @@ -0,0 +1,6 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +public enum TransmuteDirection { + UPGRADE, + DOWNGRADE +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteItem.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteItem.java new file mode 100644 index 0000000000..47bfc43d40 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/TransmuteItem.java @@ -0,0 +1,114 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TransmuteItem { + // Ores + TIN_ORE("Tin ore", TransmuteCategory.ORES), + COPPER_ORE("Copper ore", TransmuteCategory.ORES), + IRON_ORE("Iron ore", TransmuteCategory.ORES), + COAL("Coal", TransmuteCategory.ORES), + MITHRIL_ORE("Mithril ore", TransmuteCategory.ORES), + ADAMANTITE_ORE("Adamantite ore", TransmuteCategory.ORES), + RUNITE_ORE("Runite ore", TransmuteCategory.ORES), + + // Fish + RAW_SHRIMPS("Raw shrimps", TransmuteCategory.FISH), + RAW_SARDINE("Raw sardine", TransmuteCategory.FISH), + RAW_HERRING("Raw herring", TransmuteCategory.FISH), + RAW_MACKEREL("Raw mackerel", TransmuteCategory.FISH), + RAW_TROUT("Raw trout", TransmuteCategory.FISH), + RAW_COD("Raw cod", TransmuteCategory.FISH), + RAW_PIKE("Raw pike", TransmuteCategory.FISH), + RAW_SALMON("Raw salmon", TransmuteCategory.FISH), + RAW_TUNA("Raw tuna", TransmuteCategory.FISH), + RAW_LOBSTER("Raw lobster", TransmuteCategory.FISH), + RAW_BASS("Raw bass", TransmuteCategory.FISH), + RAW_SWORDFISH("Raw swordfish", TransmuteCategory.FISH), + RAW_KARAMBWAN("Raw karambwan", TransmuteCategory.FISH), + RAW_SHARK("Raw shark", TransmuteCategory.FISH), + RAW_ANGLERFISH("Raw anglerfish", TransmuteCategory.FISH), + + // Gems (transmute order: sapphire → emerald → ruby → diamond → opal → jade → red topaz → dragonstone) + UNCUT_SAPPHIRE("Uncut sapphire", TransmuteCategory.GEMS), + UNCUT_EMERALD("Uncut emerald", TransmuteCategory.GEMS), + UNCUT_RUBY("Uncut ruby", TransmuteCategory.GEMS), + UNCUT_DIAMOND("Uncut diamond", TransmuteCategory.GEMS), + UNCUT_OPAL("Uncut opal", TransmuteCategory.GEMS), + UNCUT_JADE("Uncut jade", TransmuteCategory.GEMS), + UNCUT_RED_TOPAZ("Uncut red topaz", TransmuteCategory.GEMS), + UNCUT_DRAGONSTONE("Uncut dragonstone", TransmuteCategory.GEMS), + + // Runes + AIR_RUNE("Air rune", TransmuteCategory.RUNES), + WATER_RUNE("Water rune", TransmuteCategory.RUNES), + EARTH_RUNE("Earth rune", TransmuteCategory.RUNES), + FIRE_RUNE("Fire rune", TransmuteCategory.RUNES), + CHAOS_RUNE("Chaos rune", TransmuteCategory.RUNES), + NATURE_RUNE("Nature rune", TransmuteCategory.RUNES), + COSMIC_RUNE("Cosmic rune", TransmuteCategory.RUNES), + LAW_RUNE("Law rune", TransmuteCategory.RUNES), + DEATH_RUNE("Death rune", TransmuteCategory.RUNES), + ASTRAL_RUNE("Astral rune", TransmuteCategory.RUNES), + BLOOD_RUNE("Blood rune", TransmuteCategory.RUNES), + SOUL_RUNE("Soul rune", TransmuteCategory.RUNES), + WRATH_RUNE("Wrath rune", TransmuteCategory.RUNES), + + // Logs + LOGS("Logs", TransmuteCategory.LOGS), + OAK_LOGS("Oak logs", TransmuteCategory.LOGS), + WILLOW_LOGS("Willow logs", TransmuteCategory.LOGS), + TEAK_LOGS("Teak logs", TransmuteCategory.LOGS), + MAPLE_LOGS("Maple logs", TransmuteCategory.LOGS), + MAHOGANY_LOGS("Mahogany logs", TransmuteCategory.LOGS), + YEW_LOGS("Yew logs", TransmuteCategory.LOGS), + MAGIC_LOGS("Magic logs", TransmuteCategory.LOGS), + REDWOOD_LOGS("Redwood logs", TransmuteCategory.LOGS), + + // Bones + BONES("Bones", TransmuteCategory.BONES), + BAT_BONES("Bat bones", TransmuteCategory.BONES), + BIG_BONES("Big bones", TransmuteCategory.BONES), + WYRMLING_BONES("Wyrmling bones", TransmuteCategory.BONES), + BABY_DRAGON_BONES("Baby dragon bones", TransmuteCategory.BONES), + WYRM_BONES("Wyrm bones", TransmuteCategory.BONES), + DRAGON_BONES("Dragon bones", TransmuteCategory.BONES), + DRAKE_BONES("Drake bones", TransmuteCategory.BONES), + LAVA_DRAGON_BONES("Lava dragon bones", TransmuteCategory.BONES), + HYDRA_BONES("Hydra bones", TransmuteCategory.BONES), + DAGANNOTH_BONES("Dagannoth bones", TransmuteCategory.BONES), + SUPERIOR_DRAGON_BONES("Superior dragon bones", TransmuteCategory.BONES), + + // Hides + COWHIDE("Cowhide", TransmuteCategory.HIDES), + SNAKESKIN("Snakeskin", TransmuteCategory.HIDES), + GREEN_DRAGONHIDE("Green dragonhide", TransmuteCategory.HIDES), + BLUE_DRAGONHIDE("Blue dragonhide", TransmuteCategory.HIDES), + RED_DRAGONHIDE("Red dragonhide", TransmuteCategory.HIDES), + BLACK_DRAGONHIDE("Black dragonhide", TransmuteCategory.HIDES), + + // Ashes + ASHES("Ashes", TransmuteCategory.ASHES), + VOLCANIC_ASH("Volcanic ash", TransmuteCategory.ASHES), + FIENDISH_ASHES("Fiendish ashes", TransmuteCategory.ASHES), + VILE_ASHES("Vile ashes", TransmuteCategory.ASHES), + MALICIOUS_ASHES("Malicious ashes", TransmuteCategory.ASHES), + ABYSSAL_ASHES("Abyssal ashes", TransmuteCategory.ASHES), + INFERNAL_ASHES("Infernal ashes", TransmuteCategory.ASHES), + + // Compost + COMPOST("Compost", TransmuteCategory.COMPOST), + SUPERCOMPOST("Supercompost", TransmuteCategory.COMPOST), + ULTRACOMPOST("Ultracompost", TransmuteCategory.COMPOST); + + private final String itemName; + private final TransmuteCategory category; + + @Override + public String toString() { + return itemName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/Transmuter.java b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/Transmuter.java new file mode 100644 index 0000000000..bec017a625 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/leaguestoolkit/Transmuter.java @@ -0,0 +1,229 @@ +package net.runelite.client.plugins.microbot.leaguestoolkit; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; + +import java.util.List; + +import static net.runelite.client.plugins.microbot.util.Global.sleep; +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@Slf4j +public class Transmuter { + + @Getter + private String status = "Idle"; + @Getter + private boolean running = false; + + // Track state across ticks instead of blocking + private boolean casting = false; + private long lastCastTime = 0; + private int lastItemCount = 0; + private String currentItemName = null; + + // If item count hasn't changed for this long AND shop is closed, re-cast. + // Auto-recast processes 10 items per batch with gaps between, so be patient. + private static final long RECAST_TIMEOUT_MS = 45_000; + // When shop is open, auto-recast pauses entirely — use a much longer timeout + private static final long RECAST_TIMEOUT_SHOP_OPEN_MS = 180_000; + + public void reset() { + status = "Idle"; + running = false; + casting = false; + lastCastTime = 0; + lastItemCount = 0; + currentItemName = null; + } + + /** + * Run one tick of the transmute loop. + * Returns false if the feature should be disabled (done or error). + */ + public boolean tick(LeaguesToolkitConfig config) { + running = true; + TransmuteItem startEnum = config.transmuteStartItem(); + TransmuteItem targetEnum = config.transmuteTargetItem(); + TransmuteDirection direction = config.transmuteDirection(); + + if (startEnum == null || targetEnum == null) { + status = "Config incomplete"; + return false; + } + + if (startEnum.getCategory() != targetEnum.getCategory()) { + status = "Start and target must be in the same category (" + + startEnum.getCategory().getDisplayName() + " vs " + + targetEnum.getCategory().getDisplayName() + ")"; + return false; + } + + TransmuteCategory category = startEnum.getCategory(); + String startItem = startEnum.getItemName(); + String targetItem = targetEnum.getItemName(); + List chain = category.getChain(); + int startIdx = category.indexOf(startItem); + int targetIdx = category.indexOf(targetItem); + + if (startIdx == -1 || targetIdx == -1) { + status = "Item not found in " + category.getDisplayName() + " chain"; + return false; + } + + // Determine direction based on chain positions — the user picks upgrade/downgrade + // to select the spell, but we walk the chain in whichever direction goes from start to target + int step = (targetIdx > startIdx) ? 1 : -1; + int currentIdx = findCurrentTier(chain, startIdx, targetIdx, step); + + // Check if we're done: target exists AND no intermediate items remain + if (Rs2Inventory.hasItem(targetItem)) { + boolean intermediatesRemain = false; + for (int i = startIdx; i != targetIdx; i += step) { + if (i < 0 || i >= chain.size()) break; + if (Rs2Inventory.hasItem(chain.get(i))) { + intermediatesRemain = true; + break; + } + } + if (!intermediatesRemain) { + status = "Done — all items are now " + targetItem; + running = false; + casting = false; + return false; + } + // Target exists but intermediates remain — keep going + } + + if (currentIdx == -1) { + // Log what we searched for to debug + log.info("[Transmuter] No items found. Searched chain indices {} to {} (step {}):", startIdx, targetIdx, step); + for (int i = startIdx; i != targetIdx + step; i += step) { + if (i < 0 || i >= chain.size()) break; + String name = chain.get(i); + boolean has = Rs2Inventory.hasItem(name); + log.info("[Transmuter] {} = {}", name, has); + } + status = "No transmutable items found in inventory"; + running = false; + casting = false; + return false; + } + + String currentItem = chain.get(currentIdx); + String nextItem = chain.get(currentIdx + step); + + // Are we mid-cast and the item changed? Advance. + if (casting && currentItemName != null && !currentItemName.equals(currentItem)) { + log.info("Tier advanced: {} → {}", currentItemName, currentItem); + casting = false; + currentItemName = null; + } + + // Check if auto-recast is still running + if (casting) { + int currentCount = Rs2Inventory.count(currentItem); + + if (currentCount == 0) { + // All items transmuted — next tick will pick up new tier + casting = false; + currentItemName = null; + status = "Tier complete → " + nextItem; + return true; + } + + // Check if count is still decreasing (auto-recast active) + if (currentCount < lastItemCount) { + lastItemCount = currentCount; + lastCastTime = System.currentTimeMillis(); + status = "Auto-recasting " + currentItem + " → " + nextItem + " (" + currentCount + " left)"; + return true; + } + + // Count hasn't changed — check if we've timed out (recast interrupted) + // Shop windows pause auto-recast, so use a longer timeout when shop is open + long elapsed = System.currentTimeMillis() - lastCastTime; + long timeout = Rs2Shop.isOpen() ? RECAST_TIMEOUT_SHOP_OPEN_MS : RECAST_TIMEOUT_MS; + + if (elapsed < timeout) { + String reason = Rs2Shop.isOpen() ? " (shop open — recast paused)" : ""; + status = "Waiting for recast... " + currentItem + " (" + currentCount + " left)" + reason; + return true; + } + + // Timed out — recast got interrupted, try again + log.info("Auto-recast interrupted for {} (shop open: {}), re-casting", currentItem, Rs2Shop.isOpen()); + casting = false; + } + + // Cast the spell on the current tier item + // In Leagues, High Alch = "Alchemic Divergence", Low Alch = "Alchemic Convergence" + String spellName = direction == TransmuteDirection.UPGRADE + ? "Alchemic Divergence" + : "Alchemic Convergence"; + status = "Casting " + spellName + " on " + currentItem + " → " + nextItem; + + Rs2ItemModel item = Rs2Inventory.get(currentItem); + if (item == null) { + status = "Lost " + currentItem + " from inventory"; + return true; + } + + // Switch to magic tab and click the spell by name + Rs2Tab.switchToMagicTab(); + sleepUntil(() -> Microbot.getClientThread().runOnClientThreadOptional( + () -> Rs2Tab.getCurrentTab() == InterfaceTab.MAGIC).orElse(false)); + sleep(200, 400); + + if (!Rs2Widget.clickWidget(spellName)) { + status = "Could not find " + spellName + " spell — do you have the Transmutation relic?"; + return true; + } + + // Wait for inventory tab to appear, then click the item + sleepUntil(() -> Microbot.getClientThread().runOnClientThreadOptional( + () -> Rs2Tab.getCurrentTab() == InterfaceTab.INVENTORY).orElse(false)); + sleep(300, 500); + Rs2Inventory.interact(item, "Cast"); + sleep(600, 900); + + // Switch back to inventory tab so we can monitor item count changes + Rs2Tab.switchToInventoryTab(); + sleepUntil(() -> Microbot.getClientThread().runOnClientThreadOptional( + () -> Rs2Tab.getCurrentTab() == InterfaceTab.INVENTORY).orElse(false)); + + // Mark as casting — subsequent ticks will monitor progress + casting = true; + currentItemName = currentItem; + lastItemCount = Rs2Inventory.count(currentItem); + lastCastTime = System.currentTimeMillis(); + status = "Auto-recasting " + currentItem + " → " + nextItem + " (" + lastItemCount + " remaining)"; + + return true; + } + + /** + * Finds which tier item is currently in the inventory. + * Searches the entire chain (not just start→target) in case a previous + * transmutation went in an unexpected direction. + */ + private int findCurrentTier(List chain, int startIdx, int targetIdx, int step) { + // First: search from start toward target (expected path) + for (int i = startIdx; i != targetIdx + step; i += step) { + if (i < 0 || i >= chain.size()) break; + if (Rs2Inventory.hasItem(chain.get(i))) return i; + } + // Fallback: search the entire chain for any matching item + for (int i = 0; i < chain.size(); i++) { + if (Rs2Inventory.hasItem(chain.get(i))) return i; + } + return -1; + } +}