From 8e70ec86a97dc644880cc2add2d03b3478e814f8 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 10 Jun 2026 20:08:17 -0600 Subject: [PATCH] Add bank-aware foundry planner --- README.md | 3 + .../EasyGiantsFoundryConfig.java | 12 ++ .../easygiantsfoundry/FoundryOverlay2D.java | 57 ++++++ .../FoundryRecommendation.java | 51 ++++++ .../FoundryRecommendationPlanner.java | 162 ++++++++++++++++++ .../easygiantsfoundry/MetalBarCounter.java | 18 ++ .../FoundryRecommendationPlannerTest.java | 24 +++ 7 files changed, 327 insertions(+) create mode 100644 src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendation.java create mode 100644 src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlanner.java create mode 100644 src/test/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlannerTest.java diff --git a/README.md b/README.md index 32bd900..cc2d737 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ The "Easy Giants' Foundry" plugin is designed to optimize your performance in th * **Cyan:** Click tool again for bonus progress - **Alloy Quality and Crucible Content** - Overlays **current crucible contents** and the **quality of the alloy** being forged. +- **Bank-Aware Foundry Planner** + - Suggests the best Giants' Foundry alloy from your banked bars and supported metal equipment. + - Shows banked sword count and metal score from the Info Panel. - **Best Mould Guidance** - Highlights the **best moulds to use** for your current task, guiding your selection process. - **Progress and Actions Tracking** diff --git a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java index 5c1b682..61e5636 100644 --- a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java +++ b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java @@ -586,6 +586,18 @@ default boolean drawAllMetals() return false; } + @ConfigItem( + position = 102, + keyName = "drawPlanner", + name = "Foundry planner", + description = "Show the best Giants' Foundry combo from your banked bars and equipment.", + section = infoPanelList + ) + default boolean drawPlanner() + { + return false; + } + @ConfigItem( position = 110, keyName = "countOre", diff --git a/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay2D.java b/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay2D.java index 1954b3a..c30e66a 100644 --- a/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay2D.java +++ b/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay2D.java @@ -11,12 +11,15 @@ import javax.inject.Singleton; import net.runelite.api.Client; +import net.runelite.api.Skill; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.overlay.OverlayPanel; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.components.LineComponent; import net.runelite.client.ui.overlay.components.TitleComponent; +import java.util.Optional; + @Singleton public class FoundryOverlay2D extends OverlayPanel { @@ -25,6 +28,7 @@ public class FoundryOverlay2D extends OverlayPanel private final EasyGiantsFoundryPlugin plugin; private final EasyGiantsFoundryState state; private final MetalBarCounter metalBarCounter; + private final FoundryRecommendationPlanner recommendationPlanner; private final EasyGiantsFoundryConfig config; @Inject @@ -33,12 +37,14 @@ private FoundryOverlay2D( EasyGiantsFoundryPlugin plugin, EasyGiantsFoundryState state, MetalBarCounter metalBarCounter, + FoundryRecommendationPlanner recommendationPlanner, EasyGiantsFoundryConfig config) { this.client = client; this.plugin = plugin; this.state = state; this.metalBarCounter = metalBarCounter; + this.recommendationPlanner = recommendationPlanner; this.config = config; this.setPosition(OverlayPosition.BOTTOM_LEFT); } @@ -87,6 +93,11 @@ public Dimension render(Graphics2D graphics) drawMetals(graphics); } + if (config.drawPlanner()) + { + drawPlanner(); + } + if (swordPickedUp) { Heat heat = state.getCurrentHeat(); @@ -171,4 +182,50 @@ private void drawMetalCount(Graphics2D graphics2D, String displayName, int count ); } } + + private void drawPlanner() + { + if (!metalBarCounter.isSeenBank()) + { + panelComponent.getChildren().add( + LineComponent.builder() + .left("Planner: open bank") + .leftColor(Color.RED) + .build() + ); + return; + } + + Optional recommendation = recommendationPlanner.recommend(client.getRealSkillLevel(Skill.SMITHING)); + if (recommendation.isEmpty()) + { + panelComponent.getChildren().add( + LineComponent.builder() + .left("Planner") + .right("no 28-metal combo") + .build() + ); + return; + } + + FoundryRecommendation best = recommendation.get(); + panelComponent.getChildren().add( + LineComponent.builder() + .left("Best foundry") + .right(best.getRecipeText()) + .build() + ); + panelComponent.getChildren().add( + LineComponent.builder() + .left("Banked") + .right(best.getSwordsText()) + .build() + ); + panelComponent.getChildren().add( + LineComponent.builder() + .left("Score") + .right(Integer.toString(best.getMetalScore())) + .build() + ); + } } diff --git a/src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendation.java b/src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendation.java new file mode 100644 index 0000000..9f118aa --- /dev/null +++ b/src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendation.java @@ -0,0 +1,51 @@ +package com.toofifty.easygiantsfoundry; + +import com.toofifty.easygiantsfoundry.enums.MetalBarType; +import lombok.Value; + +@Value +public class FoundryRecommendation +{ + MetalBarType primary; + int primaryCount; + MetalBarType secondary; + int secondaryCount; + int swords; + int metalScore; + + public String getRecipeText() + { + if (secondary == null) + { + return primaryCount + " " + displayName(primary); + } + + return primaryCount + " " + displayName(primary) + " + " + secondaryCount + " " + displayName(secondary); + } + + public String getSwordsText() + { + return swords + " sword" + (swords == 1 ? "" : "s"); + } + + private static String displayName(MetalBarType type) + { + switch (type) + { + case BRONZE: + return "Bronze"; + case IRON: + return "Iron"; + case STEEL: + return "Steel"; + case MITHRIL: + return "Mithril"; + case ADAMANT: + return "Adamant"; + case RUNITE: + return "Rune"; + default: + throw new IllegalArgumentException("Unknown metal type: " + type); + } + } +} diff --git a/src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlanner.java b/src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlanner.java new file mode 100644 index 0000000..098de47 --- /dev/null +++ b/src/main/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlanner.java @@ -0,0 +1,162 @@ +package com.toofifty.easygiantsfoundry; + +import com.toofifty.easygiantsfoundry.enums.MetalBarType; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Optional; + +import static com.toofifty.easygiantsfoundry.MathUtil.max1; + +@Singleton +public class FoundryRecommendationPlanner +{ + private static final int SWORD_METAL_COUNT = 28; + + @Inject + private MetalBarCounter metalBarCounter; + + public Optional recommend(int smithingLevel) + { + Optional best = Optional.empty(); + + for (MetalBarType type : MetalBarType.values()) + { + best = chooseBest(best, evaluate(smithingLevel, type, SWORD_METAL_COUNT, null, 0)); + } + + MetalBarType[] types = MetalBarType.values(); + for (int i = 0; i < types.length - 1; i++) + { + MetalBarType lower = types[i]; + MetalBarType higher = types[i + 1]; + for (int lowerCount = 1; lowerCount < SWORD_METAL_COUNT; lowerCount++) + { + best = chooseBest(best, evaluate(smithingLevel, lower, lowerCount, higher, SWORD_METAL_COUNT - lowerCount)); + } + } + + return best; + } + + private Optional chooseBest( + Optional current, + Optional candidate) + { + if (candidate.isEmpty()) + { + return current; + } + + if (current.isEmpty()) + { + return candidate; + } + + return compare(candidate.get(), current.get()) > 0 ? candidate : current; + } + + private int compare(FoundryRecommendation left, FoundryRecommendation right) + { + return Comparator + .comparingInt((FoundryRecommendation recommendation) -> recommendation.getMetalScore() * recommendation.getSwords()) + .thenComparingInt(FoundryRecommendation::getMetalScore) + .thenComparingInt(FoundryRecommendation::getSwords) + .compare(left, right); + } + + private Optional evaluate( + int smithingLevel, + MetalBarType primary, + int primaryCount, + MetalBarType secondary, + int secondaryCount) + { + if (smithingLevel < levelRequirement(primary) || (secondary != null && smithingLevel < levelRequirement(secondary))) + { + return Optional.empty(); + } + + int swords = metalBarCounter.getFoundryInput(primary) / primaryCount; + if (secondary != null) + { + swords = Math.min(swords, metalBarCounter.getFoundryInput(secondary) / secondaryCount); + } + + if (swords == 0) + { + return Optional.empty(); + } + + int metalScore = getMetalScore(primary, primaryCount, secondary, secondaryCount); + + return Optional.of(new FoundryRecommendation( + primary, + primaryCount, + secondary, + secondaryCount, + swords, + metalScore)); + } + + static int getMetalScore(MetalBarType primary, int primaryCount, MetalBarType secondary, int secondaryCount) + { + Double[] metals = new Double[] { + getWeightedValue(primary, primaryCount), + secondary == null ? 0d : getWeightedValue(secondary, secondaryCount) + }; + + Arrays.sort(metals, Collections.reverseOrder()); + + return (int) ((10 * metals[0] + 10 * metals[1] + max1(metals[0]) * max1(metals[1])) / 10.0); + } + + private static double getWeightedValue(MetalBarType type, int count) + { + return (10 * metalTier(type) * count) / (double) SWORD_METAL_COUNT; + } + + private static int metalTier(MetalBarType type) + { + switch (type) + { + case BRONZE: + return 1; + case IRON: + return 2; + case STEEL: + return 3; + case MITHRIL: + return 4; + case ADAMANT: + return 5; + case RUNITE: + return 6; + default: + throw new IllegalArgumentException("Unknown metal type: " + type); + } + } + + private static int levelRequirement(MetalBarType type) + { + switch (type) + { + case BRONZE: + case IRON: + return 15; + case STEEL: + return 30; + case MITHRIL: + return 50; + case ADAMANT: + return 70; + case RUNITE: + return 85; + default: + throw new IllegalArgumentException("Unknown metal type: " + type); + } + } +} diff --git a/src/main/java/com/toofifty/easygiantsfoundry/MetalBarCounter.java b/src/main/java/com/toofifty/easygiantsfoundry/MetalBarCounter.java index 5b65ade..d8d9f07 100644 --- a/src/main/java/com/toofifty/easygiantsfoundry/MetalBarCounter.java +++ b/src/main/java/com/toofifty/easygiantsfoundry/MetalBarCounter.java @@ -36,6 +36,17 @@ public int get(MetalBarType type) return count; } + public int getFoundryInput(MetalBarType type) + { + int count = 0; + for (Map counts : index.values()) + { + count += counts.get(type).sumFoundryInput(config); + } + + return count; + } + public void clear() { index.clear(); @@ -144,5 +155,12 @@ public int sum(EasyGiantsFoundryConfig config) sum += config.countEquipment() ? equipment : 0; return sum; } + + public int sumFoundryInput(EasyGiantsFoundryConfig config) + { + int sum = config.countBars() ? bars : 0; + sum += config.countEquipment() ? equipment : 0; + return sum; + } } } diff --git a/src/test/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlannerTest.java b/src/test/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlannerTest.java new file mode 100644 index 0000000..9b1e03d --- /dev/null +++ b/src/test/java/com/toofifty/easygiantsfoundry/FoundryRecommendationPlannerTest.java @@ -0,0 +1,24 @@ +package com.toofifty.easygiantsfoundry; + +import com.toofifty.easygiantsfoundry.enums.MetalBarType; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class FoundryRecommendationPlannerTest +{ + @Test + public void calculatesKnownAdjacentAlloyScores() + { + assertEquals(65, FoundryRecommendationPlanner.getMetalScore(MetalBarType.STEEL, 14, MetalBarType.MITHRIL, 14)); + assertEquals(95, FoundryRecommendationPlanner.getMetalScore(MetalBarType.MITHRIL, 14, MetalBarType.ADAMANT, 14)); + assertEquals(130, FoundryRecommendationPlanner.getMetalScore(MetalBarType.ADAMANT, 14, MetalBarType.RUNITE, 14)); + } + + @Test + public void calculatesEfficientSplitScores() + { + assertEquals(89, FoundryRecommendationPlanner.getMetalScore(MetalBarType.MITHRIL, 18, MetalBarType.ADAMANT, 10)); + assertEquals(118, FoundryRecommendationPlanner.getMetalScore(MetalBarType.ADAMANT, 19, MetalBarType.RUNITE, 9)); + } +}