diff --git a/src/main/java/net/runelite/client/plugins/microbot/smithingplus/AutoSmithingPlusConfig.java b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/AutoSmithingPlusConfig.java new file mode 100644 index 0000000000..fbaa2a834f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/AutoSmithingPlusConfig.java @@ -0,0 +1,226 @@ +package net.runelite.client.plugins.microbot.smithingplus; + +import net.runelite.client.config.*; +import net.runelite.client.plugins.microbot.smithingplus.data.AnvilItem; +import net.runelite.client.plugins.microbot.smithingplus.data.AnvilLocationOption; +import net.runelite.client.plugins.microbot.smithingplus.data.BankLocationOption; +import net.runelite.client.plugins.microbot.smithingplus.data.Bars; + +@ConfigGroup("SmithingPlus") +@ConfigInformation("
1. Bar: the bar tier to smith. The bars must already be in your bank.
" + + "" + + "2. Item: what to make at the anvil. Each item uses a fixed number of bars, and bigger items give more XP per hour. Some picks are members only and will refuse to start on free worlds.
" + + "" + + "3. Progressive mode: ignore the Item pick and smith the best item your Smithing level can make at the chosen bar tier. It skips members items on free worlds.
" + + "" + + "4. Anvil: the anvil to walk to and anchor at. Auto nearest picks the closest. The Lumbridge rusted anvil takes bronze bars only.
" + + "" + + "5. Distance to stray: how far the bot may wander from the anvil tile. This is also the radius used to count nearby players for world hopping.
" + + "" + + "6. Max players in area: hop worlds when more players than this are within Distance to stray. Set 0 to never hop.
" + + "" + + "7. League mode: presses an arrow key now and then to reset the idle logout timer.
" + + "" + + "8. Speed mode: disables Microbot antiban for a faster but more detectable bot. Use throwaway accounts only.
" + + "" + + "9. Stop conditions: the bot shuts down when any limit you set is reached. Stop after minutes caps runtime, Stop after XP caps Smithing XP gained, and Target level stops once your Smithing level reaches it. Each value of 0 means no limit. The bot deposits its inventory before stopping.
" + + "" + + "10. Preferred bank: override the closest by distance choice with a specific bank.
" + + "" + + "11. Items to bank and keep: two comma separated lists matched against item names. Items to bank wins. Leave it empty to deposit everything except your keep list. Keep defaults hold the hammer and any bar you are smithing.
") +public interface AutoSmithingPlusConfig extends Config { + + @ConfigSection(name = "General", description = "General settings", position = 0) + String generalSection = "general"; + + @ConfigSection(name = "Banking", description = "Banking settings", position = 1) + String bankingSection = "bankingSection"; + + // --- General section --- + + @ConfigItem( + keyName = "selectedBar", + name = "Bar", + description = "Which bar tier to smith. Bars must be in your bank.", + position = 0, + section = generalSection + ) + default Bars selectedBar() { + return Bars.BRONZE; + } + + @ConfigItem( + keyName = "selectedItem", + name = "Item", + description = "Which item to make. Bronze claws are MEMBERS-ONLY (Cabin Fever quest); F2P picks will refuse to start.", + position = 1, + section = generalSection + ) + default AnvilItem selectedItem() { + return AnvilItem.DAGGER; + } + + @ConfigItem( + keyName = "progressiveSmith", + name = "Progressive mode", + description = "Ignore the Item pick and auto-smith the best item your Smithing level can make at the chosen bar tier (skips members items on F2P). Mirrors AutoSmeltingPlus's progressive mode.", + position = 2, + section = generalSection + ) + default boolean progressiveSmith() { + return false; + } + + @ConfigItem( + keyName = "hammerOnToolBelt", + name = "Hammer on tool belt", + description = "Tick this if your hammer lives on the tool belt instead of the inventory. The bot then stops requiring (and withdrawing) a loose hammer. Leave unticked to use a normal inventory hammer.", + position = 3, + section = generalSection + ) + default boolean hammerOnToolBelt() { + return false; + } + + @ConfigItem( + keyName = "anvilLocation", + name = "Anvil", + description = "Walk to and anchor at this anvil. AUTO_NEAREST = require start-near-anvil. Lumbridge Rusted Anvil is bronze-only.", + position = 2, + section = generalSection + ) + default AnvilLocationOption anvilLocation() { + return AnvilLocationOption.AUTO_NEAREST; + } + + @ConfigItem( + keyName = "distanceToStray", + name = "Distance to stray", + description = "How far the bot can wander from the anvil tile. Also the player-detection radius for autohop.", + position = 3, + section = generalSection + ) + default int distanceToStray() { + return 20; + } + + @ConfigItem( + keyName = "maxPlayersInArea", + name = "Max players in area", + description = "Hop worlds if more players than this are within Distance to Stray. 0 = disable.", + position = 4, + section = generalSection + ) + default int maxPlayersInArea() { + return 0; + } + + @ConfigItem( + keyName = "leagueMode", + name = "League mode (anti-AFK)", + description = "Periodically presses an arrow key to reset the idle timer.", + position = 5, + section = generalSection + ) + default boolean leagueMode() { + return false; + } + + @ConfigItem( + keyName = "speedMode", + name = "Speed mode (less antiban)", + description = "Disables Microbot's antiban. Faster bot, more pattern-detectable. Throwaway only.", + position = 6, + section = generalSection + ) + default boolean speedMode() { + return false; + } + + @ConfigItem( + keyName = "stopAfterMinutes", + name = "Stop after (minutes)", + description = "Auto-shutdown after this many minutes of runtime. 0 = no limit.", + position = 7, + section = generalSection + ) + default int stopAfterMinutes() { + return 0; + } + + @ConfigItem( + keyName = "stopAfterXp", + name = "Stop after (XP gained)", + description = "Auto-shutdown after gaining this much Smithing XP. 0 = no limit.", + position = 8, + section = generalSection + ) + default int stopAfterXp() { + return 0; + } + + /** + * When Smithing level reaches this value, the script runs one final deposit pass + * then shuts down. 0 disables the check. + */ + @ConfigItem( + keyName = "targetLevel", + name = "Target level", + description = "Stop when Smithing reaches this level. Deposits inventory first. 0 = disabled.", + position = 9, + section = generalSection + ) + default int targetLevel() { + return 0; + } + + // Pause is an overlay button that toggles the shared Microbot.pauseAllScripts flag. + // See AutoSmithingPlusOverlay. + + // --- Banking section --- + + @ConfigItem( + keyName = "bankLocation", + name = "Preferred bank", + description = "AUTO_NEAREST = closest by raw distance.", + position = 1, + section = bankingSection + ) + default BankLocationOption bankLocation() { + return BankLocationOption.AUTO_NEAREST; + } + + /** + * Comma separated inclusion list matched as substrings against item names. + * Leave empty to fall back to the keep list instead. + */ + @ConfigItem( + keyName = "itemsToBank", + name = "Items to bank (comma-separated)", + description = "Items whose name contains any of these substrings get deposited. Leave empty to deposit everything except Items to keep.", + position = 2, + section = bankingSection + ) + default String itemsToBank() { + return ""; + } + + /** + * Defaults preserve the smithing setup (hammer plus selected bar) so the bot never + * deposits the tools it needs to keep working. + */ + @ConfigItem( + keyName = "itemsToKeep", + name = "Items to keep (comma-separated)", + description = "Items to never deposit. Default keeps hammer + any bar tier you're smithing.", + position = 3, + section = bankingSection + ) + default String itemsToKeep() { + return "hammer,bar"; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/smithingplus/AutoSmithingPlusOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/AutoSmithingPlusOverlay.java new file mode 100644 index 0000000000..c3a0c70a77 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/AutoSmithingPlusOverlay.java @@ -0,0 +1,240 @@ +package net.runelite.client.plugins.microbot.smithingplus; + +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.smithingplus.data.AnvilItem; +import net.runelite.client.plugins.microbot.smithingplus.data.Bars; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.ButtonComponent; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; +import net.runelite.http.api.item.ItemPrice; + +import javax.inject.Inject; +import java.awt.*; +import java.text.NumberFormat; +import java.time.Duration; +import java.util.List; + +/** + * Overlay showing runtime, Smithing XP gained, XP/hr, smith cycles, current level + delta, + * GP/hr estimate, and status. + */ +public class AutoSmithingPlusOverlay extends OverlayPanel { + private static final Color TITLE_COLOR = new Color(0, 170, 0); + private static final Color HEADER_COLOR = new Color(140, 220, 140); + private static final Color NORMAL_TEXT_COLOR = Color.WHITE; + private static final Color HIGHLIGHT_COLOR = new Color(255, 235, 145); + + // Inventory capacity. A loose hammer occupies one slot, leaving 27 for bars; with the hammer on + // the tool belt all 28 slots hold bars. Used to convert smith cycles to items for the GP/hr line. + private static final int INVENTORY_SLOTS = 28; + + private final AutoSmithingPlusPlugin plugin; + private final Client client; + private final AutoSmithingPlusConfig config; + + // Pause button toggles Microbot.pauseAllScripts (global flag). Public final so the plugin's + // startUp() can call hookMouseListener(). + public final ButtonComponent pauseButton; + + @Inject + AutoSmithingPlusOverlay(AutoSmithingPlusPlugin plugin, Client client, AutoSmithingPlusConfig config) { + super(plugin); + this.plugin = plugin; + this.client = client; + this.config = config; + setPosition(OverlayPosition.TOP_LEFT); + setNaughty(); + + pauseButton = new ButtonComponent("Pause"); + pauseButton.setPreferredSize(new Dimension(100, 25)); + pauseButton.setParentOverlay(this); + pauseButton.setFont(FontManager.getRunescapeBoldFont()); + pauseButton.setOnClick(() -> { + Microbot.pauseAllScripts.set(!Microbot.pauseAllScripts.get()); + if (Microbot.pauseAllScripts.get()) { + // Kill in-flight walker. Without this, Rs2Walker keeps walking on its own executor + // after the script main loop pauses. + Rs2Walker.setTarget(null); + } + }); + } + + @Override + public Dimension render(Graphics2D graphics) { + try { + panelComponent.setPreferredSize(new Dimension(240, 300)); + // No clear: preserves the click-target registry for the Pause button. + + panelComponent.getChildren().add(TitleComponent.builder() + .text("AutoSmithingPlus v" + AutoSmithingPlusPlugin.version) + .color(TITLE_COLOR) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Status:") + .right(Microbot.status == null ? "Idle" : Microbot.status) + .rightColor(HIGHLIGHT_COLOR) + .build()); + + panelComponent.getChildren().add(LineComponent.builder().left("").build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Statistics") + .leftColor(HEADER_COLOR) + .build()); + + AutoSmithingPlusScript script = plugin.getScript(); + if (script != null && script.getStartTimeMillis() > 0) { + int currentLevel = client.getRealSkillLevel(Skill.SMITHING); + int currentXp = client.getSkillExperience(Skill.SMITHING); + int xpGained = currentXp - script.getStartSkillXp(); + long runtimeMillis = System.currentTimeMillis() - script.getStartTimeMillis(); + long xpPerHour = (runtimeMillis > 1000) ? (xpGained * 3600000L / runtimeMillis) : 0; + + int levelDelta = currentLevel - script.getStartSkillLevel(); + String levelStr = currentLevel + (levelDelta > 0 ? " (+" + levelDelta + ")" : ""); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Smithing level:") + .right(levelStr) + .rightColor(NORMAL_TEXT_COLOR) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("XP gained:") + .right(NumberFormat.getInstance().format(xpGained)) + .rightColor(NORMAL_TEXT_COLOR) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("XP/hr:") + .right(NumberFormat.getInstance().format(xpPerHour)) + .rightColor(NORMAL_TEXT_COLOR) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Smith cycles:") + .right(String.valueOf(script.getActionsCompleted())) + .rightColor(NORMAL_TEXT_COLOR) + .build()); + + // GP/hr: NET profit per item = product GE price minus the bars it consumes. The + // counter is smith cycles; each "Smith All" works a full inventory of bars, so + // items per cycle is roughly freeBarSlots / bars-per-item. Can be negative when + // bars cost more than the product. Guards runtime 0 and price 0; the product price + // is 0 for items whose name doesn't follow the "Tier base" pattern. The "~" marks + // it an estimate (cycle->item conversion). + long gpPerHour = 0; + Bars activeBar = script.getActiveBar(); + AnvilItem activeItem = script.getActiveItem(); + if (activeBar != null && activeItem != null && runtimeMillis > 1000 + && activeItem.getRequiredBars() > 0) { + int productPrice = productPrice(activeBar, activeItem); + int barPrice = Microbot.getItemManager().getItemPrice(activeBar.getId()); + if (productPrice > 0) { + long netPerItem = (long) productPrice - (long) barPrice * activeItem.getRequiredBars(); + long itemsSmithed = (long) script.getActionsCompleted() + * (freeBarSlots() / activeItem.getRequiredBars()); + gpPerHour = netPerItem * itemsSmithed * 3600000L / runtimeMillis; + } + } + panelComponent.getChildren().add(LineComponent.builder() + .left("GP/hr:") + .right("~" + NumberFormat.getInstance().format(gpPerHour)) + .rightColor(NORMAL_TEXT_COLOR) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Runtime:") + .right(formatDuration(Duration.ofMillis(runtimeMillis))) + .rightColor(NORMAL_TEXT_COLOR) + .build()); + + // target-level progress line. + if (config.targetLevel() > 0) { + int toGo = Math.max(0, config.targetLevel() - currentLevel); + panelComponent.getChildren().add(LineComponent.builder() + .left("Target:") + .right(config.targetLevel() + (toGo > 0 ? " (" + toGo + " to go)" : " (reached)")) + .rightColor(HIGHLIGHT_COLOR) + .build()); + } + + } else { + panelComponent.getChildren().add(LineComponent.builder() + .left("(not running)") + .leftColor(NORMAL_TEXT_COLOR) + .build()); + } + + // Pause button added unconditionally to win the click-bounds registration race + // against the first render. + pauseButton.setText(Microbot.pauseAllScripts.get() ? "Resume" : "Pause"); + panelComponent.getChildren().add(pauseButton); + + } catch (Exception ex) { + Microbot.logStackTrace(this.getClass().getSimpleName(), ex); + } + return super.render(graphics); + } + + /** Inventory slots available for bars: all 28 with the hammer on the tool belt, else 27. */ + private int freeBarSlots() { + return config.hammerOnToolBelt() ? INVENTORY_SLOTS : INVENTORY_SLOTS - 1; + } + + private String formatDuration(Duration duration) { + long hours = duration.toHours(); + long minutes = duration.toMinutesPart(); + long seconds = duration.toSecondsPart(); + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } + + // Cache the resolved product item id so the name search runs only when the active bar/item + // changes, not every render frame. search() scans the whole item database; getItemPrice by id + // is a cheap lookup. + private Bars cachedProductBar; + private AnvilItem cachedProductItem; + private int cachedProductId = 0; + + /** + * GE price of the product smithed from {@code bar} at {@code item}. AnvilItem carries no + * product item id (it is widget-child driven), so we build the in-game name from the bar's + * product prefix + the item's base name (e.g. "Bronze dagger") and resolve it once via the + * item manager's name search (exact match), then cache the id. Returns 0 when the name can't be + * built or no item matches, so the overlay shows GP/hr 0 rather than mispricing. + */ + private int productPrice(Bars bar, AnvilItem item) { + if (bar != cachedProductBar || item != cachedProductItem) { + cachedProductBar = bar; + cachedProductItem = item; + cachedProductId = resolveProductId(bar, item); + } + return cachedProductId > 0 ? Microbot.getItemManager().getItemPrice(cachedProductId) : 0; + } + + private int resolveProductId(Bars bar, AnvilItem item) { + String prefix = bar.getProductPrefix(); + String base = item.getProductBaseName(); + if (prefix == null || base == null) { + return 0; + } + String fullName = prefix + " " + base; + ListThe anvil interaction is widget-driven: clicking an anvil with a hammer + bar in inventory + * opens the smithing widget (container ID {@code 312}). Each item occupies a child slot; the + * {@link #getChildId()} maps the item to that slot. Clicking child[childId] with the active bar + * tier (Bronze/Iron/Steel/etc.) selected smiths that item. + * + *
Item list, child IDs and bar counts are forked from the upstream {@code varrockanvil} plugin; + * level requirements and members flags are cross-referenced against the + * OSRS Wiki Anvil page. + * + *
Some entries are bar-tier-specific aliases: {@code BRONZE_WIRE} shares its child slot with + * Iron spit and Steel studs, so picking the wrong bar tier produces a different item in that slot. + */ +@Getter +@RequiredArgsConstructor +public enum AnvilItem { + DAGGER("Dagger", 9, 1), + SWORD("Sword", 10, 1), + SCIMITAR("Scimitar", 11, 2), + LONG_SWORD("Long sword", 12, 2), + TWO_HAND_SWORD("2-hand sword", 13, 3), + AXE("Axe", 14, 1), + MACE("Mace", 15, 1), + WARHAMMER("Warhammer", 16, 3), + BATTLE_AXE("Battle axe", 17, 3), + CLAWS("Claws", 18, 2), + CHAIN_BODY("Chain body", 19, 3), + PLATE_LEGS("Plate legs", 20, 3), + PLATE_SKIRT("Plate skirt", 21, 3), + PLATE_BODY("Plate body", 22, 5), + NAILS("Nails", 23, 1), + MEDIUM_HELM("Medium helm", 24, 1), + FULL_HELM("Full helm", 25, 2), + SQUARE_SHIELD("Square shield", 26, 2), + KITE_SHIELD("Kite shield", 27, 3), + OIL_LAMP("Oil lamp (bronze only)", 28, 1), + DART_TIPS("Dart tips", 29, 1), + ARROWTIPS("Arrowtips", 30, 1), + KNIVES("Knives", 31, 1), + BRONZE_WIRE("Wire / spit / studs", 32, 1), + BULLSEYE_LAMP("Bullseye lamp (steel)", 28, 1), + BOLTS("Bolts (unf)", 34, 1); + + private final String itemName; + private final int childId; + private final int requiredBars; + + public String getName() { + return itemName; + } + + @Override + public String toString() { + return itemName; + } + + /** + * The product's in-game name minus the bar-tier prefix (e.g. "dagger" -> "Bronze dagger", + * "med helm" -> "Bronze med helm"). Combined with {@link Bars#getProductPrefix()} to look up + * the finished item's GE price for the overlay GP/hr line. Returns null for items whose + * product name does not follow the simple "Tier base" pattern (wire/spit/studs, lamps, bolts, + * dart tips, arrowtips, nails, knives); the overlay then shows GP/hr 0 for those rather than + * pricing the wrong item. + */ + public String getProductBaseName() { + switch (this) { + case DAGGER: return "dagger"; + case SWORD: return "sword"; + case SCIMITAR: return "scimitar"; + case LONG_SWORD: return "longsword"; + case TWO_HAND_SWORD: return "2h sword"; + case AXE: return "axe"; + case MACE: return "mace"; + case WARHAMMER: return "warhammer"; + case BATTLE_AXE: return "battleaxe"; + case CLAWS: return "claws"; + case CHAIN_BODY: return "chainbody"; + case PLATE_LEGS: return "platelegs"; + case PLATE_SKIRT: return "plateskirt"; + case PLATE_BODY: return "platebody"; + case MEDIUM_HELM: return "med helm"; + case FULL_HELM: return "full helm"; + case SQUARE_SHIELD: return "sq shield"; + case KITE_SHIELD: return "kiteshield"; + // Non-standard product names (multi-output, tier-locked, or unusual naming) - priced 0. + default: return null; + } + } + + /** + * Members-only filter. Bronze claws (and all higher-tier claws) require completion of the Cabin + * Fever members quest, so the smithing widget doesn't show that slot on F2P. Picking a + * members-only item on F2P would stall the bot, so the pre-flight check uses this to + * refuse-start. Extend this method if more members-only items surface. + */ + public static boolean isMembersOnly(AnvilItem item) { + if (item == null) return false; + switch (item) { + // Members-only anvil products: an F2P account cannot smith these (the widget slot is + // absent), so the Script's pre-flight refuses to start rather than stalling on a no-op + // click. NAILS, DART_TIPS, ARROWTIPS and KNIVES are F2P-smithable in OSRS and are + // deliberately not listed here. + case CLAWS: + case OIL_LAMP: + case BULLSEYE_LAMP: + case BRONZE_WIRE: // shared slot also covers Iron spit / Steel studs, both members + case BOLTS: + return true; + default: + return false; + } + } + + /** + * Smithing level required to smith this item at the given bar tier, or -1 if not makeable at + * that tier. Indexed Bronze/Iron/Steel/Mithril/Adamant/Rune. Drives the Script's level + * pre-flight and progressive mode. The Script's stall-detection stays as a reactive backstop + * for any residual table error or a drifted widget id. + */ + public int getRequiredLevel(Bars bar) { + if (bar == null) return -1; + int[] levels = levelsByBar(this); + int idx = bar.ordinal(); + return (idx >= 0 && idx < levels.length) ? levels[idx] : -1; + } + + // Bronze, Iron, Steel, Mithril, Adamant, Rune. -1 = not makeable at that tier. + private static int[] levelsByBar(AnvilItem item) { + switch (item) { + case DAGGER: return new int[]{ 1, 15, 30, 50, 70, 85}; + case SWORD: return new int[]{ 4, 19, 34, 54, 74, 89}; + case SCIMITAR: return new int[]{ 5, 20, 35, 55, 75, 90}; + case LONG_SWORD: return new int[]{ 6, 21, 36, 56, 76, 91}; + case TWO_HAND_SWORD: return new int[]{14, 29, 44, 64, 84, 99}; + case AXE: return new int[]{ 1, 16, 31, 51, 71, 86}; + case MACE: return new int[]{ 2, 17, 32, 52, 72, 87}; + case WARHAMMER: return new int[]{ 9, 24, 39, 59, 79, 94}; + case BATTLE_AXE: return new int[]{10, 25, 40, 60, 80, 95}; + case CLAWS: return new int[]{13, 28, 43, 63, 83, 98}; + case CHAIN_BODY: return new int[]{11, 26, 41, 61, 81, 96}; + case PLATE_LEGS: return new int[]{16, 31, 46, 66, 86, 99}; + case PLATE_SKIRT: return new int[]{16, 31, 46, 66, 86, 99}; + case PLATE_BODY: return new int[]{18, 33, 48, 68, 88, 99}; + case NAILS: return new int[]{ 4, 19, 34, 54, 74, 89}; + case MEDIUM_HELM: return new int[]{ 3, 18, 33, 53, 73, 88}; + case FULL_HELM: return new int[]{ 7, 22, 37, 57, 77, 92}; + case SQUARE_SHIELD: return new int[]{ 8, 23, 38, 58, 78, 93}; + case KITE_SHIELD: return new int[]{12, 27, 42, 62, 82, 97}; + case DART_TIPS: return new int[]{ 4, 19, 34, 54, 74, 89}; + case ARROWTIPS: return new int[]{ 5, 20, 35, 55, 75, 90}; + case KNIVES: return new int[]{ 7, 22, 37, 57, 77, 92}; + case BOLTS: return new int[]{ 3, 18, 33, 53, 73, 88}; + // Tier-locked / shared-slot members items (see isMembersOnly). The -1 tiers fall + // through the level gate (treated as "unknown", not a hard refuse) and rely on + // stall-detection if a wrong bar is picked. + case OIL_LAMP: return new int[]{-1, 26, -1, -1, -1, -1}; // oil lantern frame: iron, 26 + case BRONZE_WIRE: return new int[]{ 4, 17, 36, -1, -1, -1}; // wire(4)/iron spit(17)/steel studs(36) + case BULLSEYE_LAMP: return new int[]{-1, -1, 49, -1, -1, -1}; // bullseye frame: steel, 49 + default: return new int[]{}; + } + } + + /** + * Progressive-mode picker: the best item to smith at the given bar tier for a player of the + * given Smithing level. "Best" = most bars per craft (most XP per craft, fewest interface + * clicks), tie-broken by highest level requirement. Skips members-only items unless isMember. + * Returns null if nothing is makeable (e.g. a bar tier too high for any item at this level). + */ + public static AnvilItem bestForLevel(Bars bar, int smithingLevel, boolean isMember) { + AnvilItem best = null; + for (AnvilItem item : values()) { + if (!isMember && isMembersOnly(item)) continue; + int req = item.getRequiredLevel(bar); + if (req < 0 || req > smithingLevel) continue; + if (best == null + || item.getRequiredBars() > best.getRequiredBars() + || (item.getRequiredBars() == best.getRequiredBars() + && req > best.getRequiredLevel(bar))) { + best = item; + } + } + return best; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/AnvilLocationOption.java b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/AnvilLocationOption.java new file mode 100644 index 0000000000..36737f4cbf --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/AnvilLocationOption.java @@ -0,0 +1,66 @@ +package net.runelite.client.plugins.microbot.smithingplus.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +/** + * User-facing dropdown of named anvils. Mirrors {@code FurnaceLocationOption} from smeltingplus. + * Each constant carries the display name and the canonical walk-to WorldPoint; the script + * resolves the actual anvil tile at runtime via a name-based object query. + * + *
{@link #AUTO_NEAREST} falls back to "find an anvil within 20 tiles of initialPlayerLocation" + * behavior (requires the user to start near an anvil, mirroring upstream VarrockAnvil's + * stand-at-anvil-or-restart behavior). + * + *
Anvil dataset is backed by {@link AnvilLocations}. + * + *
Important: variant anvils (e.g. Lumbridge's Rusted Anvil, object ID 39620) restrict the + * smithing widget to bronze items only. Picking a non-bronze bar at such an anvil will leave + * the bot stalled with the widget showing greyed slots. The {@link #isBronzeOnly()} flag exposes + * this. + */ +@Getter +@RequiredArgsConstructor +public enum AnvilLocationOption { + AUTO_NEAREST("Auto / nearest", null, null, false, false), + + // F2P + LUMBRIDGE_RUSTED("Lumbridge (bronze)", + "Lumbridge Rusted Anvil", new WorldPoint(3227, 3258, 0), false, true), + VARROCK_WEST("Varrock West", + "Varrock West Anvil", new WorldPoint(3188, 3426, 0), false, false), + + // Varrock Central: 2 anvils in Horvik's armour shop, F2P, all-bar, 31 squares from W bank. + VARROCK_CENTRAL("Varrock Central", + "Varrock Central Anvil", new WorldPoint(3225, 3424, 0), false, false), + + // Varrock East: 2 anvils just south of the east bank on the west side, F2P, all-bar, + // 35 squares from E bank. + VARROCK_EAST("Varrock East", + "Varrock East Anvil", new WorldPoint(3247, 3410, 0), false, false), + + // P2P anvils. The anchor is the town area near the anvil; the runtime 20-tile finder resolves + // the exact anvil tile. + YANILLE("Yanille (P2P)", + "Yanille Anvil", new WorldPoint(2614, 3084, 0), true, false), + SEERS_VILLAGE("Seers' Village (P2P)", + "Seers' Village Anvil", new WorldPoint(2701, 3482, 0), true, false), + BURTHORPE("Burthorpe (P2P)", + "Burthorpe Anvil", new WorldPoint(2899, 3542, 0), true, false); + + private final String displayName; + private final String locationName; + private final WorldPoint worldPoint; + private final boolean membersOnly; + private final boolean bronzeOnly; + + @Override + public String toString() { + return displayName; + } + + public boolean hostsTarget(Object target) { + return this != AUTO_NEAREST; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/AnvilLocations.java b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/AnvilLocations.java new file mode 100644 index 0000000000..15afd69365 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/AnvilLocations.java @@ -0,0 +1,89 @@ +package net.runelite.client.plugins.microbot.smithingplus.data; + +import net.runelite.api.coords.WorldPoint; + +import java.util.Arrays; +import java.util.List; + +/** + * Anvil location dataset. Mirrors {@code FurnaceLocations} from smeltingplus but for anvils. + * + *
{@link AnvilLocationOption} is the user-facing dropdown facade; this class is internal data + * backing it. + * + *
Pick a specific bank to override. The script walks to {@link #getWorldPoint()} + * and opens the bank there. + * + *
Copied from the {@code miningplus} / {@code smeltingplus} skill-agnostic dataset. + * Coordinates from OSRS Wiki bank-booth tiles. + */ +@Getter +@RequiredArgsConstructor +public enum BankLocationOption { + AUTO_NEAREST("Auto / nearest", null, false), + + LUMBRIDGE_CASTLE("Lumbridge Castle (2F)", new WorldPoint(3208, 3220, 2), false), + DRAYNOR_VILLAGE("Draynor Village", new WorldPoint(3092, 3243, 0), false), + AL_KHARID("Al Kharid (10gp toll)", new WorldPoint(3270, 3168, 0), false), + // Fadli's bank at Emir's Arena: F2P, much closer to Al Kharid Mine. Three banking entities + // cluster here: + // - Bank chest #1 at (3382, 3270, 0): full bank (deposit + withdraw) + // - Bank chest #2 at (3381, 3269, 0): full bank (deposit + withdraw) + // - Deposit Box at (3385, 3272, 0): deposit-only + // This coord sits 1-2 tiles east of both chests; Rs2Bank.openBank() nearest-entity search + // biases toward a bank chest, so withdraw-required cycles (smelting, smithing) work here too, + // not just mining. + AL_KHARID_ARENA("Al Kharid Arena", new WorldPoint(3383, 3269, 0), false), + VARROCK_WEST("Varrock West", new WorldPoint(3185, 3437, 0), false), + VARROCK_EAST("Varrock East", new WorldPoint(3253, 3420, 0), false), + GRAND_EXCHANGE("Grand Exchange", new WorldPoint(3164, 3489, 0), false), + EDGEVILLE("Edgeville", new WorldPoint(3094, 3492, 0), false), + FALADOR_WEST("Falador West", new WorldPoint(2945, 3370, 0), false), + FALADOR_EAST("Falador East", new WorldPoint(3013, 3355, 0), false), + PORT_SARIM("Port Sarim (depot)", new WorldPoint(3045, 3236, 0), false), + // Ferox Enclave bank (F2P safe zone at the Wilderness gateway, lvl 0-16). A banker and bank + // chest sit on the western end of the enclave. + // WARNING: Rs2Walker may path through lvl 1-2 Wilderness on approach. Safe inside enclave. + FEROX_ENCLAVE("Ferox Enclave", new WorldPoint(3128, 3637, 0), false), + + // Members banks + SEERS_VILLAGE("Seers' Village (P2P)", new WorldPoint(2722, 3493, 0), true), + CATHERBY("Catherby (P2P)", new WorldPoint(2810, 3441, 0), true), + ARDOUGNE_NORTH("N. Ardougne (P2P)", new WorldPoint(2616, 3332, 0), true), + ARDOUGNE_SOUTH("S. Ardougne (P2P)", new WorldPoint(2655, 3283, 0), true), + BURGH_DE_ROTT("Burgh de Rott (P2P)", new WorldPoint(3494, 3211, 0), true), + SHILO_VILLAGE("Shilo Village (P2P)", new WorldPoint(2851, 2954, 0), true), + // The Mining Guild bank chest is underground in the members-only section (the F2P side has + // only shops + rocks). This coord targets the underground chest vicinity near the mining floor + // ((3046, 9756, 0) in MineLocationOption.MINING_GUILD), nudged +4 Y toward the chest. + MINING_GUILD_FALADOR("Mining Guild (P2P)", new WorldPoint(3046, 9760, 0), true); + + private final String displayName; + private final WorldPoint worldPoint; + private final boolean membersOnly; + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/Bars.java b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/Bars.java new file mode 100644 index 0000000000..70118451f1 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/Bars.java @@ -0,0 +1,52 @@ +package net.runelite.client.plugins.microbot.smithingplus.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.gameval.ItemID; + +/** + * Smithing bar definitions. AutoSmithingPlus uses this enum as the user-facing bar selector + * (Bronze / Iron / Steel / Mithril / Adamantite / Runite). + * + *
Bar names and smithing level requirements are from the + * OSRS Wiki Smelting bars + * page; item IDs are RuneLite client constants ({@code net.runelite.api.gameval.ItemID}). + */ +@Getter +@RequiredArgsConstructor +public enum Bars { + BRONZE("Bronze bar", ItemID.BRONZE_BAR, 1), + IRON("Iron bar", ItemID.IRON_BAR, 15), + STEEL("Steel bar", ItemID.STEEL_BAR, 30), + MITHRIL("Mithril bar", ItemID.MITHRIL_BAR, 50), + ADAMANTITE("Adamantite bar", ItemID.ADAMANTITE_BAR, 70), + RUNITE("Runite bar", ItemID.RUNITE_BAR, 85); + + private final String name; + private final int id; + private final int requiredSmithingLevel; + + @Override + public String toString() { + return name; + } + public int getId() { return id; } + + /** + * In-game name prefix for smithed products at this tier (e.g. "Bronze dagger", "Rune + * platebody"). Note products use "Adamant" and "Rune", not the bar's "Adamantite"/"Runite". + * Combined with {@link AnvilItem#getProductBaseName()} to look up the product's GE price for + * the overlay GP/hr line. + */ + public String getProductPrefix() { + switch (this) { + case BRONZE: return "Bronze"; + case IRON: return "Iron"; + case STEEL: return "Steel"; + case MITHRIL: return "Mithril"; + case ADAMANTITE: return "Adamant"; + case RUNITE: return "Rune"; + default: return null; + } + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/Ores.java b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/Ores.java new file mode 100644 index 0000000000..0d49eaa48c --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/smithingplus/data/Ores.java @@ -0,0 +1,33 @@ +package net.runelite.client.plugins.microbot.smithingplus.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Ore enum retained from the smelting fork as keys in {@link Bars}'s recipe map. Smithing does + * not consume ores (anvil work uses bars + hammer); dropping this enum would force divergence + * from the smeltingplus data layer. + * + *