This commit is contained in:
2025-10-12 22:41:10 +03:00
parent ce480a1185
commit 128173339e
89 changed files with 12846 additions and 72 deletions

View File

@@ -0,0 +1,19 @@
package ee.futur.easygiantsfoundry;
import net.runelite.api.Client;
import net.runelite.api.widgets.Widget;
public class BonusWidget
{
private static final int BONUS_WIDGET = 49414148;
private static final int BONUS_COLOR = 0xfcd703;
static boolean isActive(Client client)
{
Widget bonusWidget = client.getWidget(BONUS_WIDGET);
return bonusWidget != null
&& bonusWidget.getChildren() != null
&& bonusWidget.getChildren().length != 0
&& bonusWidget.getChild(0).getTextColor() == BONUS_COLOR;
}
}

View File

@@ -0,0 +1,43 @@
package ee.futur.easygiantsfoundry;
public class EasyGiantsFoundryClientIDs
{
// heat and progress are from 0-1000
protected static final int VARBIT_HEAT = 13948;
protected static final int VARBIT_PROGRESS = 13949;
protected static final int VARBIT_BRONZE_COUNT = 13931;
protected static final int VARBIT_IRON_COUNT = 13932;
protected static final int VARBIT_STEEL_COUNT = 13933;
protected static final int VARBIT_MITHRIL_COUNT = 13934;
protected static final int VARBIT_ADAMANT_COUNT = 13935;
protected static final int VARBIT_RUNE_COUNT = 13936;
protected static final int VARBIT_FORTE_SELECTED = 13910;
protected static final int VARBIT_BLADE_SELECTED = 13911;
protected static final int VARBIT_TIP_SELECTED = 13912;
// 0 - load bars
// 1 - set mould
// 2 - collect preform
// 3 -
protected static final int VARBIT_GAME_STAGE = 13914;
protected static final int VARBIT_PREFORM_STORED = 13947;
protected static final int WIDGET_HEAT_PARENT = 49414153;
protected static final int WIDGET_LOW_HEAT_PARENT = 49414163;
protected static final int WIDGET_MED_HEAT_PARENT = 49414164;
protected static final int WIDGET_HIGH_HEAT_PARENT = 49414165;
protected static final int WIDGET_PROGRESS_PARENT = 49414219;
// children with type 3 are stage boxes
// every 11th child is a sprite
protected static final int SPRITE_ID_TRIP_HAMMER = 4442;
protected static final int SPRITE_ID_GRINDSTONE = 4443;
protected static final int SPRITE_ID_POLISHING_WHEEL = 4444;
protected static final int ANIMATION_HEATING = 827;
protected static final int ANIMATION_COOLING = 832;
}

View File

@@ -0,0 +1,712 @@
package ee.futur.easygiantsfoundry;
import java.awt.Color;
import ee.futur.easygiantsfoundry.enums.FontType;
import net.runelite.client.config.Alpha;
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.Range;
import net.runelite.client.config.Notification;
import net.runelite.client.ui.ColorScheme;
@ConfigGroup(EasyGiantsFoundryConfig.GROUP)
public interface EasyGiantsFoundryConfig extends Config
{
String GROUP = "easygiantsfoundry";
String SOUND_ID = "soundID";
String POINTS_KEY = "easygiantsfoundrypoints";
@ConfigSection(
name = "Notifications",
description = "Notifications",
position = 0
)
String notificationList = "notificationList";
@ConfigItem(
keyName = "giantsFoundryStageNotification",
name = "Notify stage changes",
description = "Notifies just before completing a stage",
position = 0,
section = notificationList
)
default Notification stageNotification()
{
return Notification.ON;
}
@ConfigItem(
keyName = "giantsFoundryHeatNotification",
name = "Notify heat changes",
description = "Notifies just before overheating/cooling when using tools",
position = 1,
section = notificationList
)
default Notification heatNotification()
{
return Notification.ON;
}
@ConfigItem(
keyName = "giantsFoundryStageThreshold",
name = "Stage threshold notification",
description = "The number of actions left required for the notification.",
position = 2,
section = notificationList
)
default int StageNotificationsThreshold()
{
return 1;
}
@ConfigItem(
keyName = "giantsFoundryHeatThreshold",
name = "Heat threshold notification",
description = "The heat level left required for the notification.",
position = 3,
section = notificationList
)
default int HeatNotificationsThreshold()
{
return 1;
}
@ConfigItem(
keyName = "bonusNotification",
name = "Notify bonus",
description = "Notifies when bonus appears",
position = 4,
section = notificationList
)
default Notification bonusNotification()
{
return Notification.OFF;
}
@ConfigItem(
keyName = "bonusSound",
name = "Bonus sound",
description = "Plays a sound when bonus appears",
position = 5,
section = notificationList
)
default boolean bonusSoundNotify()
{
return true;
}
@ConfigItem(
keyName = SOUND_ID,
name = "Bonus sound ID",
description = "Sound Effect ID to play when bonus appears",
position = 6,
section = notificationList
)
default int soundId()
{
return 4212;
}
@ConfigSection(
name = "Highlights",
description = "3D npc/object highlights",
position = 1
)
String highlightList = "highlightList";
@ConfigItem(
name = "Highlight Style",
description = "The style of the highlight",
position = 0,
section = highlightList,
keyName = "overlayOption")
default HighlightStyle highlightStyle()
{
return HighlightStyle.HIGHLIGHT_CLICKBOX;
}
@Range(
min = 1,
max = 4
)
@ConfigItem(
keyName = "borderThickness",
name = "Border Thickness",
description = "The thickness of the border",
position = 1,
section = highlightList
)
default int borderThickness()
{
return 1;
}
@Range(
min = 0,
max = 4
)
@ConfigItem(
keyName = "borderFeather",
name = "Border Feather",
description = "The feather of the border",
position = 2,
section = highlightList
)
default int borderFeather()
{
return 0;
}
// alpha
@Range(
min = 0,
max = 255
)
@ConfigItem(
keyName = "borderAlpha",
name = "Border Alpha",
description = "The alpha of the border highlight",
position = 3,
section = highlightList
)
default int borderAlpha()
{
return 255;
}
@ConfigItem(
keyName = "toolsHighlight",
name = "Highlight Tools",
description = "Highlights current tool with symbolic colors",
position = 4,
section = highlightList
)
default boolean highlightTools()
{
return true;
}
@Range(
min = 100,
max = 200
)
@ConfigItem(
keyName = "clickboxScale",
name = "Clickbox scale %",
description = "Scale clickbox highlights to make targets easier to click.",
position = 4,
section = highlightList
)
default int clickboxScale()
{
return 150;
}
@ConfigItem(
keyName = "waterLavaHighlight",
name = "Highlight Waterfall/Lava Pool",
description = "Highlight Lava Pool / Waterfall when heat change required",
position = 5,
section = highlightList
)
default boolean highlightWaterAndLava()
{
return true;
}
@ConfigItem(
keyName = "mouldHighlight",
name = "Highlight Mould",
description = "Highlight Mould when it should be clicked",
position = 6,
section = highlightList
)
default boolean highlightMould()
{
return true;
}
@ConfigItem(
keyName = "crucibleHighlight",
name = "Highlight Crucible",
description = "Highlight Crucible when it should be filled/poured",
position = 7,
section = highlightList
)
default boolean highlightCrucible()
{
return true;
}
@ConfigItem(
keyName = "kovacHighlight",
name = "Highlight Kovac for hand in",
description = "Highlight Kovac when sword can be handed in",
position = 8,
section = highlightList
)
default boolean highlightKovac()
{
return true;
}
@ConfigItem(
keyName = "crucibleContent",
name = "Show Crucible content and quality",
description = "Show the content and quality of the crucible",
position = 9,
section = highlightList
)
default boolean showCrucibleContent()
{
return true;
}
@ConfigItem(
keyName = "storageHighlight",
name = "Highlight Preform Storage",
description = "Highlight Storage when it contains a preform.",
position = 10,
section = highlightList
)
default boolean highlightStorage()
{
return true;
}
@ConfigSection(
name = "Info Panel",
description = "Settings for the Info Panel overlay",
position = 2
)
String infoPanelList = "infoPanelList";
@ConfigItem(
keyName = "infoTitle",
name = "Title",
description = "Toggle for \"Easy Giants' Foundry\" text",
position = 0,
section = infoPanelList
)
default boolean drawTitle()
{
return true;
}
@ConfigItem(
keyName = "heatInfo",
name = "Heat",
description = "Toggle for Heat text",
position = 1,
section = infoPanelList
)
default boolean drawHeatInfo()
{
return true;
}
@ConfigItem(
keyName = "stageInfo",
name = "Stage",
description = "Toggle for Stage text",
position = 2,
section = infoPanelList
)
default boolean drawStageInfo()
{
return true;
}
@ConfigItem(
keyName = "actionLeft",
name = "Actions Left",
description = "Toggle for actions left text",
position = 3,
section = infoPanelList
)
default boolean drawActionsLeft()
{
return true;
}
@ConfigItem(
keyName = "heatLeft",
name = "Heat Left",
description = "Toggle for heat left text",
position = 4,
section = infoPanelList
)
default boolean drawHeatLeft()
{
return true;
}
@ConfigItem(
keyName = "bonusActions",
name = "Bonus Actions",
description = "Toggle for Bonus actions text",
position = 5,
section = infoPanelList
)
default boolean drawBonusActions()
{
return true;
}
@ConfigItem(
keyName = "shopPoints",
name = "Reputation",
description = "Toggle for reputation text",
position = 6,
section = infoPanelList
)
default boolean drawShopPoints()
{
return false;
}
@ConfigItem(
keyName = "heatDelta",
name = "Heat change",
description = "Show the heat delta and recommended location for the next adjust.",
position = 7,
section = infoPanelList
)
default boolean drawHeatDelta()
{
return true;
}
@ConfigItem(
keyName = "heatPlan",
name = "Heat plan",
description = "Show fast/slow heat action plans with predicted results.",
position = 8,
section = infoPanelList
)
default boolean drawHeatPlan()
{
return true;
}
@ConfigItem(
keyName = "heatActionTimer",
name = "Heat action timer",
description = "Show the active heat action countdown in the info panel.",
position = 9,
section = infoPanelList
)
default boolean drawHeatActionTimer()
{
return true;
}
@ConfigSection(
name = "Info Overlay",
description = "Overlay Text Info On Objects",
position = 3
)
String infoOverlay = "infoOverlay";
@ConfigItem(
keyName = "actionLeftOverlay",
name = "Actions Left Overlay",
description = "Toggle for actions left overlay",
position = 0,
section = infoOverlay
)
default boolean drawActionLeftOverlay()
{
return true;
}
@ConfigItem(
keyName = "heatLeftOverlay",
name = "Heat Left Overlay",
description = "Toggle for heat left overlay",
position = 1,
section = infoOverlay
)
default boolean drawHeatLeftOverlay()
{
return true;
}
@ConfigItem(
keyName = "crucibleInfoOverlay",
name = "Crucible Info Overlay",
description = "Toggle for crucible info overlay",
position = 2,
section = infoOverlay
)
default boolean drawCrucibleInfoOverlay()
{
return true;
}
@ConfigItem(
keyName = "mouldInfoOverlay",
name = "Mould Info Overlay",
description = "Toggle for mould info overlay",
position = 3,
section = infoOverlay
)
default boolean drawMouldInfoOverlay()
{
return true;
}
@ConfigItem(
keyName = "LavaWaterInfoOverlay",
name = "Lava/Waterfall Info Overlay",
description = "Toggle for lava/waterfall info overlay",
position = 4,
section = infoOverlay
)
default boolean drawLavaWaterInfoOverlay()
{
return true;
}
@ConfigSection(
name = "Colour",
description = "Colours",
position = 4
)
String colourList = "colourList";
@ConfigItem(
keyName = "mouldText",
name = "Mould Text",
description = "Colour for optimal mould text",
position = 0,
section = colourList
)
default Color mouldTextColour()
{
return new Color(0xdc10d);
}
@ConfigItem(
keyName = "generalColour",
name = "General",
description = "Colour for highlighting objects/npcs in general",
position = 1,
section = colourList
)
default Color generalHighlight()
{
return Color.CYAN;
}
@ConfigItem(
keyName = "lavaWaterColour",
name = "Lava/Waterfall",
description = "Colour for highlighting lava/waterfall",
position = 2,
section = colourList
)
default Color lavaWaterfallColour()
{
return ColorScheme.PROGRESS_COMPLETE_COLOR;
}
@ConfigItem(
keyName = "toolGood",
name = "Tool Good",
description = "Colour for highlighting current tool when they are usable",
position = 3,
section = colourList
)
default Color toolGood()
{
return ColorScheme.PROGRESS_COMPLETE_COLOR;
}
@ConfigItem(
keyName = "toolBad",
name = "Tool Bad",
description = "Colour for highlighting current tool when they are not usable",
position = 4,
section = colourList
)
default Color toolBad()
{
return ColorScheme.PROGRESS_ERROR_COLOR;
}
@ConfigItem(
keyName = "toolCaution",
name = "Tool Caution",
description = "Colour for highlighting current tool when they are about to be not usable",
position = 5,
section = colourList
)
default Color toolCaution()
{
return ColorScheme.PROGRESS_INPROGRESS_COLOR;
}
@ConfigItem(
keyName = "toolBonus",
name = "Tool Bonus",
description = "Colour for highlighting current tool when they have a bonus to click on",
position = 6,
section = colourList
)
default Color toolBonus()
{
return Color.CYAN;
}
@ConfigItem(
keyName = "textBackground",
name = "Text Background",
description = "Set a color to draw a box behind text.",
position = 7,
section = colourList
)
@Alpha
default Color textBackground()
{
return null;
}
@ConfigItem(
keyName = "dynamicOverlayFont",
name = "Dynamic Overlay Font",
description = "Choose the font type for the info overlay.<br/>" +
"Defaults to your setting from RuneLite -> Overlay settings -> Dynamic overlay font.",
position = 10,
section = colourList
)
default FontType dynamicOverlayFont()
{
return FontType.DEFAULT;
}
@ConfigItem(
keyName = "textOutline",
name = "Text Outline",
description = "Use an outline around text instead of a shadow.",
position = 11,
section = colourList
)
default boolean textOutline()
{
return false;
}
@ConfigItem(
position = -100,
keyName = "alwaysShowInfoPanel",
name = "Always show",
description = "Always show the info panel, even outside of Giants' Foundry.",
section = infoPanelList
)
default boolean alwaysDrawInfoPanel()
{
return false;
}
@ConfigItem(
position = 100,
keyName = "drawMetals",
name = "Metals",
description = "Show total metals count in the info panel.",
section = infoPanelList
)
default boolean drawMetals()
{
return false;
}
@ConfigItem(
position = 101,
keyName = "drawAllMetals",
name = "Metals: show all",
description = "Show rows for metals even if you don't have any of that type.",
section = infoPanelList
)
default boolean drawAllMetals()
{
return false;
}
@ConfigItem(
position = 110,
keyName = "countOre",
name = "Metals: count ore",
description = "Include raw ores in the metals count.",
section = infoPanelList
)
default boolean countOre()
{
return true;
}
@ConfigItem(
position = 111,
keyName = "countBars",
name = "Metals: count bars",
description = "Include smelted bars in the metals count.",
section = infoPanelList
)
default boolean countBars()
{
return true;
}
@ConfigItem(
position = 112,
keyName = "countEquipment",
name = "Metals: count equipment",
description = "Include equipment in the metals count.",
section = infoPanelList
)
default boolean countEquipment()
{
return true;
}
@ConfigSection(
name = "Advanced",
description = "Advanced Settings",
position = 5
)
String advancedSettings = "generalSettings";
@Range(
max = 50
)
@ConfigItem(
keyName = "heatActionBuffer", // renamed to reset player's settings for previous bugged implementation
name = "Padding Ticks",
description = "Number of inefficient idle ticks between actions; calculations will pad more than optimal heat compensate for heat decay during idle/afk.",
position = 0,
section = advancedSettings
)
default int heatActionPadTicks()
{
return 3;
}
@ConfigItem(
keyName = "debugging",
name = "Show Debugging",
description = "Shows debugging visuals used for development",
position = 0,
section = advancedSettings,
warning = "Only used for development."
)
default boolean debugging()
{
return false;
}
}

View File

@@ -0,0 +1,56 @@
package ee.futur.easygiantsfoundry;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Point;
import net.runelite.client.ui.ColorScheme;
import javax.inject.Singleton;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
@Slf4j
@Singleton
public final class EasyGiantsFoundryHelper
{
public static Color getHeatColor(int actions, int heat)
{
if (heat >= actions)
{
return ColorScheme.PROGRESS_COMPLETE_COLOR;
}
if (heat > 0)
{
return ColorScheme.PROGRESS_INPROGRESS_COLOR;
}
return ColorScheme.PROGRESS_ERROR_COLOR;
}
public static void renderTextLocation(Graphics2D graphics, Point point, String text, Color fg, Color bg, boolean outline)
{
if (bg != null)
{
FontMetrics fm = graphics.getFontMetrics();
graphics.setColor(bg);
graphics.fillRect(point.getX(), point.getY() - fm.getHeight(), fm.stringWidth(text), fm.getHeight());
}
graphics.setColor(Color.BLACK);
if (outline)
{
graphics.drawString(text, point.getX(), point.getY() + 1);
graphics.drawString(text, point.getX(), point.getY() - 1);
graphics.drawString(text, point.getX() + 1, point.getY());
graphics.drawString(text, point.getX() - 1, point.getY());
}
else
{
graphics.drawString(text, point.getX() + 1, point.getY() + 1);
}
graphics.setColor(fg);
graphics.drawString(text, point.getX(), point.getY());
}
}

View File

@@ -0,0 +1,509 @@
package ee.futur.easygiantsfoundry;
import com.google.inject.Provides;
import static ee.futur.easygiantsfoundry.EasyGiantsFoundryClientIDs.VARBIT_GAME_STAGE;
import static ee.futur.easygiantsfoundry.EasyGiantsFoundryClientIDs.VARBIT_HEAT;
import ee.futur.easygiantsfoundry.enums.Stage;
import javax.inject.Inject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.GameState;
import net.runelite.api.gameval.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.MenuAction;
import net.runelite.api.Skill;
import net.runelite.api.events.*;
import net.runelite.api.widgets.Widget;
import net.runelite.client.Notifier;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.ui.overlay.OverlayManager;
import java.util.Objects;
import java.util.Set;
@Slf4j
@PluginDescriptor(
name = "Easy Giants' Foundry",
description = "Helpful overlays for the Giants' Foundry minigame"
)
public class EasyGiantsFoundryPlugin extends Plugin
{
private static final int TRIP_HAMMER = 44619;
private static final int GRINDSTONE = 44620;
private static final int POLISHING_WHEEL = 44621;
private static final int LAVA_POOL = 44631;
private static final int WATERFALL = 44632;
private static final int CRUCIBLE = 44776;
private static final int MOULD_JIG = 44777;
private static final int STORAGE = 44778;
private static final int KOVAC_NPC = 11472;
private static final int PREFORM = 27010;
private static final int REPUTATION_VARBIT = 3436;
// 5 total items, includes Smiths gloves (i);
private static final Set<Integer> SMITHS_OUTFIT_IDS = Set.of(27023, 27025, 27027, 27029, 27031);
private Stage oldStage;
private int lastBoost;
private boolean bonusNotified = false;
@Getter
private int reputation;
@Inject
private EasyGiantsFoundryState state;
@Inject
private EasyGiantsFoundryHelper helper;
@Inject
private MetalBarCounter metalBarCounter;
@Inject
private OverlayManager overlayManager;
@Inject
private FoundryOverlay2D overlay2d;
@Inject
private FoundryOverlay3D overlay3d;
@Inject
private MouldHelper mouldHelper;
@Inject
private EasyGiantsFoundryConfig config;
@Inject
private Notifier notifier;
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private ConfigManager configManager;
@Override
protected void startUp()
{
overlayManager.add(overlay2d);
overlayManager.add(overlay3d);
if (client.getGameState() == GameState.LOGGED_IN)
{
reputation = client.getVarpValue(REPUTATION_VARBIT);
}
}
@Override
protected void shutDown()
{
overlayManager.remove(overlay2d);
overlayManager.remove(overlay3d);
metalBarCounter.clear();
}
@Subscribe
public void onGameObjectSpawned(GameObjectSpawned event)
{
GameObject gameObject = event.getGameObject();
switch (gameObject.getId())
{
case POLISHING_WHEEL:
state.setEnabled(true);
overlay3d.polishingWheel = gameObject;
break;
case GRINDSTONE:
overlay3d.grindstone = gameObject;
break;
case LAVA_POOL:
overlay3d.lavaPool = gameObject;
break;
case WATERFALL:
overlay3d.waterfall = gameObject;
break;
case TRIP_HAMMER:
overlay3d.tripHammer = gameObject;
break;
case MOULD_JIG:
overlay3d.mouldJig = gameObject;
break;
case CRUCIBLE:
overlay3d.crucible = gameObject;
break;
case STORAGE:
overlay3d.storage = gameObject;
break;
}
}
@Subscribe
public void onGameObjectDespawned(GameObjectDespawned event)
{
GameObject gameObject = event.getGameObject();
switch (gameObject.getId())
{
case POLISHING_WHEEL:
state.setEnabled(false);
overlay3d.polishingWheel = null;
break;
case GRINDSTONE:
overlay3d.grindstone = null;
break;
case LAVA_POOL:
overlay3d.lavaPool = null;
break;
case WATERFALL:
overlay3d.waterfall = null;
break;
case TRIP_HAMMER:
overlay3d.tripHammer = null;
break;
case MOULD_JIG:
overlay3d.mouldJig = null;
break;
case CRUCIBLE:
overlay3d.crucible = null;
break;
case STORAGE:
overlay3d.storage = null;
break;
}
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
if (event.getGameState().equals(GameState.LOADING))
{
state.setEnabled(false);
}
if (event.getGameState().equals(GameState.LOGGED_IN))
{
reputation = client.getVarpValue(REPUTATION_VARBIT);
}
}
@Subscribe
public void onStatChanged(StatChanged statChanged)
{
final int curBoost = statChanged.getBoostedLevel();
// if the difference between current and last boost is != 0 then a stat boost (or drop) change occurred
if (!statChanged.getSkill().equals(Skill.SMITHING) ||
curBoost != lastBoost ||
!state.isEnabled() ||
state.getCurrentStage() == null)
{
lastBoost = curBoost;
return;
}
if (config.stageNotification().isEnabled() &&
state.getActionsLeftInStage() == config.StageNotificationsThreshold() &&
(oldStage == null || oldStage != state.getCurrentStage()))
{
notifier.notify(config.stageNotification(), "About to finish the current stage!");
oldStage = state.getCurrentStage();
}
else if (config.heatNotification().isEnabled() &&
state.getActionsForHeatLevel() == config.HeatNotificationsThreshold())
{
notifier.notify(config.heatNotification(), "About to run out of heat!");
}
}
@Subscribe
public void onNpcSpawned(NpcSpawned event)
{
if (event.getNpc().getId() == KOVAC_NPC)
{
overlay3d.kovac = event.getNpc();
}
}
@Subscribe
public void onNpcDespawned(NpcDespawned event)
{
if (event.getNpc().getId() == KOVAC_NPC)
{
overlay3d.kovac = null;
}
}
@Subscribe
public void onItemContainerChanged(ItemContainerChanged event)
{
if (event.getContainerId() == InventoryID.WORN)
{
if (event.getItemContainer().count(PREFORM) == 0)
{
state.reset();
oldStage = null;
}
else
{
updateSmithsOutfitPieces();
}
}
else if (event.getContainerId() == InventoryID.INV || event.getContainerId() == InventoryID.BANK)
{
metalBarCounter.put(event.getItemContainer());
}
}
public void onMenuEntryAdded(MenuEntryAdded event)
{
if (event.getOption().startsWith("Heat-preform") || event.getOption().startsWith("Dunk-preform"))
{
}
else if (event.getOption().startsWith("Cool-preform") || event.getOption().startsWith("Quench-preform")) {
}
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked event)
{
clientThread.invokeAtTickEnd(() ->
{
if (!(event.getMenuAction() == MenuAction.GAME_OBJECT_FIRST_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_SECOND_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_THIRD_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_FOURTH_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_FIFTH_OPTION
|| event.getMenuAction() == MenuAction.WIDGET_TARGET_ON_GAME_OBJECT
|| event.getMenuAction() == MenuAction.WALK))
{
return;
}
if (!state.isEnabled()) return;
if (event.getMenuTarget().contains("Crucible "))
{
if (event.getMenuOption().equals("Pour"))
{
if (client.getVarbitValue(VARBIT_GAME_STAGE) == 1)
{
state.setLastKnownCrucibleScore((int) state.getCrucibleScore());
}
// add persistent game message of the alloy value so user can reference later.
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "The score of the preform is <col=00FFFF>" + ((int) state.getCrucibleScore() + state.getMouldScore()), null);
}
}
// Could not find a varbit to capture, so capture the menu-option directly.
// start the HeatActionStateMachine when varbit begins to update in onVarbitChanged()
if (event.getMenuOption().startsWith("Heat-preform"))
{
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(false, true, "heats");
}
else if (event.getMenuOption().startsWith("Dunk-preform"))
{
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(true, true, "dunks");
}
else if (event.getMenuOption().startsWith("Cool-preform"))
{
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(false, false, "cools");
}
else if (event.getMenuOption().startsWith("Quench-preform"))
{
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(true, false, "quenches");
}
else if (!state.heatActionStateMachine.isIdle()) // canceled heating/cooling, stop the heating state-machine
{
state.heatActionStateMachine.stop();
}
});
}
@Subscribe
public void onScriptPostFired(ScriptPostFired event)
{
if (event.getScriptId() == MouldHelper.DRAW_MOULD_LIST_SCRIPT
|| event.getScriptId() == MouldHelper.REDRAW_MOULD_LIST_SCRIPT
|| event.getScriptId() == MouldHelper.SELECT_MOULD_SCRIPT
|| event.getScriptId() == MouldHelper.RESET_MOULD_SCRIPT)
{
mouldHelper.selectBest(event.getScriptId());
updateMouldScore();
}
}
private void updateMouldScore() {
state.setMouldScore(mouldHelper.getTotalScore());
// show mould score on Mould UI Title
Widget mouldParent = client.getWidget(47054850);
int mouldScore = state.getMouldScore();
if (mouldParent != null && mouldScore >= 0)
{
Widget title = Objects.requireNonNull(mouldParent.getChild(1));
// not sure why, the ":" character turns into ": ," when rendered; omitting it.
title.setText("Giants' Foundry Mould Setup <col=FFFFFF>(Score " + mouldScore + ")");
}
}
// previous heat varbit value, used to filter out passive heat decay.
private int previousHeat = 0;
@Subscribe
public void onVarbitChanged(VarbitChanged event)
{
if (event.getVarpId() == REPUTATION_VARBIT)
{
reputation = client.getVarpValue(REPUTATION_VARBIT);
}
// STAGE becomes 0 again after player picks up the preform
if (event.getVarbitId() == VARBIT_GAME_STAGE && event.getValue() == 0)
{
// clear out the current and soon to be previous scores.
state.setLastKnownCrucibleScore(-1);
state.setMouldScore(-1);
}
// start the heating state-machine when the varbit updates
// if heat varbit updated and the user clicked, start the state-machine
if (event.getVarbitId() == VARBIT_HEAT)
{
// ignore passive heat decay, one heat per two ticks
int delta = event.getValue() - previousHeat;
// sign check: num * num > 0 == same sign
if (delta != -1)
{
if (state.heatActionStateMachine.getActionname() != null)
{
// if the state-machine is idle, start it
if (state.heatActionStateMachine.isIdle())
{
state.heatActionStateMachine.start(state, config, previousHeat);
}
state.heatActionStateMachine.onTick();
}
if (config.debugging())
{
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "",
"Heat: <col=FF0000>" + event.getValue() + "</col>" +
"Delta: <col=00FFFF>" + delta + "</col> " +
"Heating Ticks: <col=00FFFF>" + state.heatActionStateMachine.heatingTicks + "</col>" +
" Cooling Ticks: <col=00FFFF>" + state.heatActionStateMachine.coolingTicks + "</col>" +
" Remaining Ticks: <col=00FFFF>" + state.heatActionStateMachine.getRemainingDuration(), "");
}
}
// client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "Delta: <col=00FFFF>" + delta + "</col> ", "");
previousHeat = event.getValue();
}
}
@Subscribe
protected void onConfigChanged(ConfigChanged configChanged)
{
if (!EasyGiantsFoundryConfig.GROUP.equals(configChanged.getGroup()))
{
return;
}
if (EasyGiantsFoundryConfig.SOUND_ID.equals(configChanged.getKey()))
{
clientThread.invoke(() -> client.playSoundEffect(config.soundId()));
}
}
@Subscribe
public void onGameTick(GameTick event)
{
checkBonus();
}
private void checkBonus()
{
if (!state.isEnabled() || state.getCurrentStage() == null
|| state.getCurrentStage().getHeat() != state.getCurrentHeat()
|| !BonusWidget.isActive(client))
{
bonusNotified = false;
return;
}
if (bonusNotified)
{
return;
}
state.addBonusActionReceived();
if (config.bonusNotification().isEnabled())
{
notifier.notify(config.bonusNotification(), "Bonus - Click tool");
}
if (config.bonusSoundNotify())
{
client.playSoundEffect(config.soundId());
}
bonusNotified = true;
}
private void updateSmithsOutfitPieces()
{
int pieces = 0;
ItemContainer equipment = client.getItemContainer(InventoryID.WORN);
if (equipment != null)
{
for (Item item : equipment.getItems())
{
if (item != null && isSmithsOutfitPiece(item.getId()))
{
pieces++;
}
}
}
state.setSmithsOutfitPieces(pieces);
}
private boolean isSmithsOutfitPiece(int itemId)
{
return SMITHS_OUTFIT_IDS.contains(itemId);
}
@Provides
EasyGiantsFoundryConfig provideConfig(ConfigManager configManager)
{
return configManager.getConfig(EasyGiantsFoundryConfig.class);
}
}

View File

@@ -0,0 +1,345 @@
package ee.futur.easygiantsfoundry;
import static ee.futur.easygiantsfoundry.MathUtil.max1;
import static ee.futur.easygiantsfoundry.EasyGiantsFoundryClientIDs.*;
import ee.futur.easygiantsfoundry.enums.Heat;
import ee.futur.easygiantsfoundry.enums.Stage;
import static ee.futur.easygiantsfoundry.enums.Stage.GRINDSTONE;
import static ee.futur.easygiantsfoundry.enums.Stage.POLISHING_WHEEL;
import static ee.futur.easygiantsfoundry.enums.Stage.TRIP_HAMMER;
import lombok.Getter;
import lombok.Setter;
import net.runelite.api.Client;
import net.runelite.api.widgets.Widget;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Singleton
public class EasyGiantsFoundryState
{
@Inject
private Client client;
@Setter
@Getter
private boolean enabled;
@Getter
private int bonusActionsReceived = 0;
@Setter
private int smithsOutfitPieces;
@Setter
@Getter
private int mouldScore = -1; // starts -1 because mould score is unknown
@Setter
@Getter
private int lastKnownCrucibleScore = -1; // will be set when "Pour"ed (because the crucible will be empty then)
private final List<Stage> stages = new ArrayList<>();
private double heatRangeRatio = 0;
public final HeatActionStateMachine heatActionStateMachine = new HeatActionStateMachine();
public void reset()
{
stages.clear();
heatRangeRatio = 0;
bonusActionsReceived = 0;
}
public int getHeatAmount()
{
return client.getVarbitValue(VARBIT_HEAT);
}
public int getProgressAmount()
{
return client.getVarbitValue(VARBIT_PROGRESS);
}
public double getHeatRangeRatio()
{
if (heatRangeRatio == 0)
{
Widget heatWidget = client.getWidget(WIDGET_HEAT_PARENT);
Widget medHeat = client.getWidget(WIDGET_MED_HEAT_PARENT);
if (medHeat == null || heatWidget == null)
{
return 0;
}
heatRangeRatio = medHeat.getWidth() / (double) heatWidget.getWidth();
}
return heatRangeRatio;
}
public int[] getLowHeatRange()
{
return new int[]{
(int) ((1 / 6d - getHeatRangeRatio() / 2) * 1000),
(int) ((1 / 6d + getHeatRangeRatio() / 2) * 1000),
};
}
public int[] getMedHeatRange()
{
return new int[]{
(int) ((3 / 6d - getHeatRangeRatio() / 2) * 1000),
(int) ((3 / 6d + getHeatRangeRatio() / 2) * 1000),
};
}
public int[] getHighHeatRange()
{
return new int[]{
(int) ((5 / 6d - getHeatRangeRatio() / 2) * 1000),
(int) ((5 / 6d + getHeatRangeRatio() / 2) * 1000),
};
}
public List<Stage> getStages()
{
if (stages.isEmpty())
{
Widget progressParent = client.getWidget(WIDGET_PROGRESS_PARENT);
if (progressParent == null || progressParent.getChildren() == null)
{
return new ArrayList<>();
}
for (Widget child : progressParent.getChildren())
{
switch (child.getSpriteId())
{
case SPRITE_ID_TRIP_HAMMER:
stages.add(TRIP_HAMMER);
break;
case SPRITE_ID_GRINDSTONE:
stages.add(GRINDSTONE);
break;
case SPRITE_ID_POLISHING_WHEEL:
stages.add(POLISHING_WHEEL);
break;
}
}
}
return stages;
}
public Stage getCurrentStage()
{
int index = (int) (getProgressAmount() / 1000d * getStages().size());
if (index < 0 || index > getStages().size() - 1)
{
return null;
}
return getStages().get(index);
}
public Heat getCurrentHeat()
{
int heat = getHeatAmount();
int[] low = getLowHeatRange();
if (heat >= low[0] && heat <= low[1])
{
return Heat.LOW;
}
int[] med = getMedHeatRange();
if (heat >= med[0] && heat <= med[1])
{
return Heat.MED;
}
int[] high = getHighHeatRange();
if (heat >= high[0] && heat <= high[1])
{
return Heat.HIGH;
}
return Heat.NONE;
}
public int getHeatChangeNeeded()
{
Heat requiredHeat = getCurrentStage().getHeat();
int heat = getHeatAmount();
int[] range;
switch (requiredHeat)
{
case LOW:
range = getLowHeatRange();
break;
case MED:
range = getMedHeatRange();
break;
case HIGH:
range = getHighHeatRange();
break;
default:
return 0;
}
if (heat < range[0])
return range[0] - heat;
else if (heat > range[1])
return range[1] - heat;
else
return 0;
}
public int getBonusActionsExpected()
{
if (getStages().size() >= 6)
{
return 3;
}
return 2;
}
public void addBonusActionReceived()
{
++bonusActionsReceived;
}
public int getCrucibleCount()
{
int bronze = client.getVarbitValue(VARBIT_BRONZE_COUNT);
int iron = client.getVarbitValue(VARBIT_IRON_COUNT);
int steel = client.getVarbitValue(VARBIT_STEEL_COUNT);
int mithril = client.getVarbitValue(VARBIT_MITHRIL_COUNT);
int adamant = client.getVarbitValue(VARBIT_ADAMANT_COUNT);
int rune = client.getVarbitValue(VARBIT_RUNE_COUNT);
return bronze + iron + steel + mithril + adamant + rune;
}
public double getCrucibleScore()
{
// https://oldschool.runescape.wiki/w/Giants%27_Foundry#Metal_score
if (getCrucibleCount() == 0) return 0;
final int BRONZE_VALUE = 1;
final int IRON_VALUE = 2;
final int STEEL_VALUE = 3;
final int MITHRIL_VALUE = 4;
final int ADAMANT_VALUE = 5;
final int RUNE_VALUE = 6;
final int bronzeNum = client.getVarbitValue(VARBIT_BRONZE_COUNT);
final int ironNum = client.getVarbitValue(VARBIT_IRON_COUNT);
final int steelNum = client.getVarbitValue(VARBIT_STEEL_COUNT);
final int mithrilNum = client.getVarbitValue(VARBIT_MITHRIL_COUNT);
final int adamantNum = client.getVarbitValue(VARBIT_ADAMANT_COUNT);
final int runeNum = client.getVarbitValue(VARBIT_RUNE_COUNT);
final double bronzeVal = (10 * BRONZE_VALUE * bronzeNum) / 28.0;
final double ironVal = (10 * IRON_VALUE * ironNum) / 28.0;
final double steelVal = (10 * STEEL_VALUE * steelNum) / 28.0;
final double mithrilVal = (10 * MITHRIL_VALUE * mithrilNum) / 28.0;
final double adamantVal = (10 * ADAMANT_VALUE * adamantNum) / 28.0;
final double runeVal = (10 * RUNE_VALUE * runeNum) / 28.0;
Double[] metals = new Double[] {
bronzeVal,
ironVal,
steelVal,
mithrilVal,
adamantVal,
runeVal
};
// Descending order
Arrays.sort(metals, Collections.reverseOrder());
return
((10 * metals[0] + 10 * metals[1]) + max1(metals[0]) * max1(metals[1])) / 10.0;
}
/**
* Get the amount of progress each stage needs
*/
public double getProgressPerStage()
{
return 1000d / getStages().size();
}
public int getActionsLeftInStage()
{
int progress = getProgressAmount();
double progressPerStage = getProgressPerStage();
double progressTillNext = progressPerStage - progress % progressPerStage;
Stage current = getCurrentStage();
// Each Smith's Outfit piece adds 20% chance to increase action progress by 1, or 100% for all 4 pieces.
double smithsOutfitBonus = smithsOutfitPieces == 4 ? 1 : 0.2 * smithsOutfitPieces;
return (int) Math.ceil(progressTillNext / (current.getProgressPerAction() + smithsOutfitBonus));
}
public int[] getCurrentHeatRange()
{
switch (getCurrentStage())
{
case POLISHING_WHEEL:
return getLowHeatRange();
case GRINDSTONE:
return getMedHeatRange();
case TRIP_HAMMER:
return getHighHeatRange();
default:
return new int[]{0, 0};
}
}
/**
* Get the amount of current stage actions that can be
* performed before the heat drops too high or too low to
* continue
*/
public int getActionsForHeatLevel()
{
Heat heatStage = getCurrentHeat();
Stage stage = getCurrentStage();
if (heatStage != stage.getHeat())
{
// not the right heat to start with
return 0;
}
int[] range = getCurrentHeatRange();
int actions = 0;
int heat = getHeatAmount();
while (heat >= range[0] && heat <= range[1])
{
actions++;
heat += stage.getHeatChange();
}
return actions;
}
public boolean isPlayerRunning()
{
return client.getVarpValue(173) == 1;
}
}

View File

@@ -0,0 +1,460 @@
package ee.futur.easygiantsfoundry;
import ee.futur.easygiantsfoundry.enums.Heat;
import ee.futur.easygiantsfoundry.enums.MetalBarType;
import ee.futur.easygiantsfoundry.enums.Stage;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.runelite.api.Client;
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;
@Singleton
public class FoundryOverlay2D extends OverlayPanel
{
private static final int REGION_ID = 13491;
private static final int PANEL_WIDTH = 240;
private final Client client;
private final EasyGiantsFoundryPlugin plugin;
private final EasyGiantsFoundryState state;
private final MetalBarCounter metalBarCounter;
private final EasyGiantsFoundryConfig config;
@Inject
private FoundryOverlay2D(
Client client,
EasyGiantsFoundryPlugin plugin,
EasyGiantsFoundryState state,
MetalBarCounter metalBarCounter,
EasyGiantsFoundryConfig config)
{
this.client = client;
this.plugin = plugin;
this.state = state;
this.metalBarCounter = metalBarCounter;
this.config = config;
setPosition(OverlayPosition.BOTTOM_LEFT);
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!config.alwaysDrawInfoPanel() && client.getLocalPlayer().getWorldLocation().getRegionID() != REGION_ID)
{
return null;
}
panelComponent.getChildren().clear();
panelComponent.setPreferredSize(new Dimension(PANEL_WIDTH, 0));
boolean hasContent = false;
if (config.drawTitle())
{
panelComponent.getChildren().add(TitleComponent.builder().text("Easy Giants' Foundry").build());
hasContent = true;
}
boolean swordPickedUp = state.isEnabled() && state.getCurrentStage() != null;
if (swordPickedUp)
{
hasContent = appendSection(buildStatusLines(), hasContent);
hasContent = appendSection(buildProgressLines(), hasContent);
hasContent = appendSection(buildHeatPlannerLines(), hasContent);
}
hasContent = appendSection(buildReputationLines(), hasContent);
hasContent = appendSection(buildMetalLines(), hasContent);
if (!hasContent)
{
return null;
}
return super.render(graphics);
}
private boolean appendSection(List<LineComponent> lines, boolean hasPreviousContent)
{
if (lines.isEmpty())
{
return hasPreviousContent;
}
if (hasPreviousContent)
{
panelComponent.getChildren().add(LineComponent.builder().left(" ").right("").build());
}
panelComponent.getChildren().addAll(lines);
return true;
}
private List<LineComponent> buildStatusLines()
{
List<LineComponent> lines = new ArrayList<>();
Heat heat = state.getCurrentHeat();
Stage stage = state.getCurrentStage();
if (config.drawHeatInfo())
{
lines.add(
LineComponent.builder()
.left("Heat")
.right(heat.getName() + " (" + state.getHeatAmount() / 10 + "%)")
.rightColor(heat.getColor())
.build()
);
}
if (config.drawStageInfo())
{
lines.add(
LineComponent.builder()
.left("Stage")
.right(stage.getName() + " (" + state.getProgressAmount() / 10 + "%)")
.rightColor(stage.getHeat().getColor())
.build()
);
}
return lines;
}
private List<LineComponent> buildProgressLines()
{
List<LineComponent> lines = new ArrayList<>();
int actionsLeft = state.getActionsLeftInStage();
int heatLeft = state.getActionsForHeatLevel();
if (config.drawActionsLeft())
{
lines.add(LineComponent.builder().left("Actions left").right(Integer.toString(actionsLeft)).build());
}
if (config.drawHeatLeft())
{
lines.add(
LineComponent.builder()
.left("Heat left")
.right(Integer.toString(heatLeft))
.rightColor(getHeatColor(actionsLeft, heatLeft))
.build()
);
}
if (config.drawBonusActions())
{
lines.add(
LineComponent.builder()
.left("Bonus actions")
.right(state.getBonusActionsReceived() + "/" + state.getBonusActionsExpected())
.rightColor(getBonusActionsColor(state.getBonusActionsReceived(), state.getBonusActionsExpected()))
.build()
);
}
return lines;
}
private List<LineComponent> buildHeatPlannerLines()
{
List<LineComponent> lines = new ArrayList<>();
if (!config.drawHeatDelta() && !config.drawHeatPlan() && !config.drawHeatActionTimer())
{
return lines;
}
if (state.getCurrentStage() == null)
{
return lines;
}
int heatChangeNeeded = state.getHeatChangeNeeded();
if (config.drawHeatDelta())
{
lines.add(buildHeatDeltaLine(heatChangeNeeded));
}
if (config.drawHeatPlan())
{
lines.addAll(buildHeatPlanLines(heatChangeNeeded));
}
if (config.drawHeatActionTimer())
{
LineComponent timerLine = buildHeatActionTimerLine();
if (timerLine != null)
{
lines.add(timerLine);
}
}
return lines;
}
private LineComponent buildHeatDeltaLine(int heatChangeNeeded)
{
String location;
if (heatChangeNeeded > 0)
{
location = "Lava";
}
else if (heatChangeNeeded < 0)
{
location = "Waterfall";
}
else
{
location = "In range";
}
String deltaText = heatChangeNeeded == 0
? "0 (in range)"
: String.format("%+d (%s)", heatChangeNeeded, location);
Color color = heatChangeNeeded == 0 ? ColorScheme.PROGRESS_COMPLETE_COLOR : config.lavaWaterfallColour();
return LineComponent.builder()
.left("Heat Δ")
.right(deltaText)
.rightColor(color)
.build();
}
private List<LineComponent> buildHeatPlanLines(int heatChangeNeeded)
{
List<LineComponent> lines = new ArrayList<>();
if (heatChangeNeeded == 0)
{
lines.add(
LineComponent.builder()
.left("Heat plan")
.right("Hold")
.rightColor(ColorScheme.PROGRESS_COMPLETE_COLOR)
.build()
);
return lines;
}
boolean isHeating = heatChangeNeeded > 0;
HeatActionSolver.DurationResult fast = solveHeatAction(true, isHeating);
HeatActionSolver.DurationResult slow = solveHeatAction(false, isHeating);
boolean added = false;
if (fast != null && fast.getDuration() > 0)
{
lines.add(buildHeatPlanLine("Fast", isHeating, true, fast));
added = true;
}
if (slow != null && slow.getDuration() > 0)
{
lines.add(buildHeatPlanLine("Slow", isHeating, false, slow));
added = true;
}
if (!added)
{
lines.add(
LineComponent.builder()
.left("Heat plan")
.right("Hold")
.rightColor(ColorScheme.PROGRESS_COMPLETE_COLOR)
.build()
);
}
return lines;
}
private LineComponent buildHeatPlanLine(String label, boolean isHeating, boolean fast, HeatActionSolver.DurationResult result)
{
String actionName = getActionName(isHeating, fast);
String value = String.format("%s %dt → %s%s",
actionName,
result.getDuration(),
formatPredictedHeat(result.getPredictedHeat()),
formatPlanStatus(result));
return LineComponent.builder()
.left(label)
.right(value)
.rightColor(config.lavaWaterfallColour())
.build();
}
private HeatActionSolver.DurationResult solveHeatAction(boolean fast, boolean isHeating)
{
Stage stage = state.getCurrentStage();
if (stage == null)
{
return null;
}
return HeatActionSolver.solve(
stage,
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
fast,
isHeating,
config.heatActionPadTicks(),
state.isPlayerRunning()
);
}
private LineComponent buildHeatActionTimerLine()
{
HeatActionStateMachine machine = state.heatActionStateMachine;
if (machine.isIdle() || machine.getActionname() == null)
{
return null;
}
return LineComponent.builder()
.left("Action")
.right(String.format(
"%s %dt → %s%s",
prettifyActionName(machine.getActionname()),
machine.getRemainingDuration(),
formatPredictedHeat(machine.getPredictedHeat()),
machine.isGoalInRange() ? "" : machine.isOverShooting() ? " (overshoot)" : " (limit)"))
.rightColor(config.lavaWaterfallColour())
.build();
}
private String formatPredictedHeat(int predictedHeat)
{
return heatPercent(predictedHeat) + "%";
}
private String formatPlanStatus(HeatActionSolver.DurationResult result)
{
if (result == null || result.isGoalInRange())
{
return "";
}
return result.isOvershooting() ? " (overshoot)" : " (limit)";
}
private String getActionName(boolean isHeating, boolean fast)
{
if (isHeating)
{
return fast ? "Dunk" : "Heat";
}
return fast ? "Quench" : "Cool";
}
private String prettifyActionName(String action)
{
if (action == null || action.isEmpty())
{
return "";
}
switch (action)
{
case "dunks":
return "Dunk";
case "heats":
return "Heat";
case "cools":
return "Cool";
case "quenches":
return "Quench";
default:
return Character.toUpperCase(action.charAt(0)) + action.substring(1);
}
}
private int heatPercent(int heat)
{
return Math.max(0, Math.min(1000, heat)) / 10;
}
private List<LineComponent> buildReputationLines()
{
List<LineComponent> lines = new ArrayList<>();
if (!config.drawShopPoints())
{
return lines;
}
lines.add(LineComponent.builder().left("Reputation").right(Integer.toString(plugin.getReputation())).build());
return lines;
}
private List<LineComponent> buildMetalLines()
{
List<LineComponent> lines = new ArrayList<>();
if (!config.drawMetals())
{
return lines;
}
if (!metalBarCounter.isSeenBank())
{
lines.add(
LineComponent.builder()
.left("Metals: open bank")
.leftColor(Color.RED)
.build()
);
}
addMetalCount(lines, "Bronze bars:", metalBarCounter.get(MetalBarType.BRONZE));
addMetalCount(lines, "Iron bars:", metalBarCounter.get(MetalBarType.IRON));
addMetalCount(lines, "Steel bars:", metalBarCounter.get(MetalBarType.STEEL));
addMetalCount(lines, "Mithril bars:", metalBarCounter.get(MetalBarType.MITHRIL));
addMetalCount(lines, "Adamant bars:", metalBarCounter.get(MetalBarType.ADAMANT));
addMetalCount(lines, "Runite bars:", metalBarCounter.get(MetalBarType.RUNITE));
return lines;
}
private void addMetalCount(List<LineComponent> lines, String displayName, int count)
{
if (count > 0 || config.drawAllMetals())
{
lines.add(LineComponent.builder().left(displayName).right(Integer.toString(count)).build());
}
}
private Color getHeatColor(int actions, int heat)
{
if (heat >= actions)
{
return ColorScheme.PROGRESS_COMPLETE_COLOR;
}
if (heat > 0)
{
return ColorScheme.PROGRESS_INPROGRESS_COLOR;
}
return ColorScheme.PROGRESS_ERROR_COLOR;
}
private Color getBonusActionsColor(int received, int expected)
{
if (received >= expected)
{
return ColorScheme.PROGRESS_COMPLETE_COLOR;
}
return ColorScheme.PROGRESS_INPROGRESS_COLOR;
}
}

View File

@@ -0,0 +1,617 @@
package ee.futur.easygiantsfoundry;
import static ee.futur.easygiantsfoundry.EasyGiantsFoundryClientIDs.*;
import static ee.futur.easygiantsfoundry.EasyGiantsFoundryHelper.getHeatColor;
import static ee.futur.easygiantsfoundry.EasyGiantsFoundryHelper.renderTextLocation;
import static ee.futur.easygiantsfoundry.MouldHelper.SWORD_TYPE_1_VARBIT;
import static ee.futur.easygiantsfoundry.MouldHelper.SWORD_TYPE_2_VARBIT;
import ee.futur.easygiantsfoundry.enums.CommissionType;
import ee.futur.easygiantsfoundry.enums.FontType;
import ee.futur.easygiantsfoundry.enums.Heat;
import ee.futur.easygiantsfoundry.enums.Stage;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.MenuEntry;
import net.runelite.api.NPC;
import net.runelite.api.Perspective;
import net.runelite.api.Point;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.widgets.Widget;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer;
import org.apache.commons.lang3.StringUtils;
public class FoundryOverlay3D extends Overlay
{
private static final int HAND_IN_WIDGET = 49414221;
private static final int CRUCIBLE_CAPACITY = 28;
private final ModelOutlineRenderer modelOutlineRenderer;
GameObject tripHammer;
GameObject grindstone;
GameObject polishingWheel;
GameObject lavaPool;
GameObject waterfall;
GameObject mouldJig;
GameObject crucible;
GameObject storage;
NPC kovac;
private final Client client;
private final EasyGiantsFoundryState state;
private final EasyGiantsFoundryConfig config;
@Inject
private FoundryOverlay3D(
Client client,
EasyGiantsFoundryState state,
EasyGiantsFoundryConfig config,
ModelOutlineRenderer modelOutlineRenderer)
{
setPosition(OverlayPosition.DYNAMIC);
this.client = client;
this.state = state;
this.config = config;
this.modelOutlineRenderer = modelOutlineRenderer;
}
private Color getObjectColor(Stage stage, Heat heat)
{
if (stage.getHeat() != heat)
{
return config.toolBad();
}
if (BonusWidget.isActive(client))
{
return config.toolBonus();
}
int actionsLeft = state.getActionsLeftInStage();
int heatLeft = state.getActionsForHeatLevel();
if (actionsLeft <= 1 || heatLeft <= 1)
{
return config.toolCaution();
}
return config.toolGood();
}
private GameObject getStageObject(Stage stage)
{
switch (stage)
{
case TRIP_HAMMER:
return tripHammer;
case GRINDSTONE:
return grindstone;
case POLISHING_WHEEL:
return polishingWheel;
}
return null;
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!state.isEnabled())
{
return null;
}
if (config.dynamicOverlayFont() != FontType.DEFAULT)
{
graphics.setFont(config.dynamicOverlayFont().getFont());
}
if (config.highlightKovac())
{
drawKovacIfHandIn(graphics);
}
if (client.getVarbitValue(VARBIT_PREFORM_STORED) == 1)
{
if (config.highlightStorage())
{
drawStorage(graphics);
}
return null;
}
if (state.getCurrentStage() == null)
{
if (config.highlightMould())
{
drawMouldIfNotSet(graphics);
}
if (config.highlightCrucible())
{
drawCrucibleIfMouldSet(graphics);
}
if (config.drawMouldInfoOverlay())
{
drawMouldScoreIfMouldSet(graphics);
drawPreformScoreIfPoured(graphics);
}
return null;
}
Stage stage = state.getCurrentStage();
GameObject stageObject = getStageObject(stage);
if (stageObject == null || graphics == null)
{
return null;
}
Heat heat = state.getCurrentHeat();
Color color = getObjectColor(stage, heat);
// TODO Config
if (config.highlightStyle() == HighlightStyle.HIGHLIGHT_CLICKBOX)
{
drawObjectClickbox(graphics, stageObject, color);
}
else
{
drawObjectOutline(graphics, stageObject, color);
}
// !state.heatingCoolingState.isIdle()
// if the stage heat is already in range, but player still wants to do heat changes
if ((stage.getHeat() != heat || !state.heatActionStateMachine.isIdle()) && config.highlightWaterAndLava())
{
drawHeatChangers(graphics);
}
// mouse hover over preview
else if (config.drawLavaWaterInfoOverlay())
{
MenuEntry[] menuEntries = client.getMenuEntries();
if (menuEntries.length != 0)
{
MenuEntry hoveredMenu = menuEntries[menuEntries.length - 1];
if (hoveredMenu.getIdentifier() == lavaPool.getId())
{
drawHeatChangerPreviewOverlay(graphics, lavaPool, true);
}
else if (hoveredMenu.getIdentifier() == waterfall.getId())
{
drawHeatChangerPreviewOverlay(graphics, waterfall, false);
}
}
}
drawActionOverlay(graphics, stageObject);
return null;
}
private void drawObjectClickbox(Graphics2D graphics, GameObject stageObject, Color color)
{
Shape objectClickbox = stageObject.getClickbox();
if (objectClickbox != null && config.highlightTools())
{
objectClickbox = scaleShape(objectClickbox);
Point mousePosition = client.getMouseCanvasPosition();
if (objectClickbox.contains(mousePosition.getX(), mousePosition.getY()))
{
graphics.setColor(color.darker());
}
else
{
graphics.setColor(color);
}
graphics.draw(objectClickbox);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(objectClickbox);
}
}
private void drawObjectOutline(Graphics2D graphics, GameObject stageObject, Color color)
{
Color _color = new Color(color.getRed(), color.getGreen(), color.getBlue(), config.borderAlpha());
modelOutlineRenderer.drawOutline(stageObject, config.borderThickness(), _color, config.borderFeather());
}
private void drawHeatChangerPreviewOverlay(
Graphics2D graphics,
GameObject stageObject,
boolean isLava
)
{
HeatActionSolver.DurationResult fastResult =
HeatActionSolver.solve(
state.getCurrentStage(),
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
true,
isLava,
config.heatActionPadTicks(),
state.isPlayerRunning()
);
final int fastDuration = fastResult.getDuration();
HeatActionSolver.DurationResult slowResult =
HeatActionSolver.solve(
state.getCurrentStage(),
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
false,
isLava,
config.heatActionPadTicks(),
state.isPlayerRunning()
);
final int slowDuration = slowResult.getDuration();
final String fastName = isLava ? "dunks" : "quenches";
final String slowName = isLava ? "heats" : "cools";
String text;
if (config.debugging())
{
text = String.format("%d %s (predicted: %d) or %d %s (predicted: %d) (overshoot: %s goal-in-range: %s)",
fastDuration, fastName, fastResult.getPredictedHeat(), slowDuration, slowName, slowResult.getPredictedHeat(), slowResult.isOvershooting(), fastResult.isGoalInRange());
}
else
{
text = String.format("%d %s or %d %s ",
fastDuration, fastName, slowDuration, slowName);
}
LocalPoint stageLoc = stageObject.getLocalLocation();
stageLoc = new LocalPoint(stageLoc.getX(), stageLoc.getY());
Point pos = Perspective.getCanvasTextLocation(client, graphics, stageLoc, text, 50);
if (pos == null)
{
return;
}
Color color = config.lavaWaterfallColour();
renderTextLocation(graphics, pos, text, color, config.textBackground(), config.textOutline());
}
private void drawHeatChangerOverlay(Graphics2D graphics, GameObject stageObject)
{
String text;
if (config.debugging())
{
text = String.format("%d %s (overshoot: %s) [goal-in-range: %s]",
state.heatActionStateMachine.getRemainingDuration(),
state.heatActionStateMachine.getActionname(),
state.heatActionStateMachine.isOverShooting(),
state.heatActionStateMachine.isGoalInRange()
);
}
else
{
text = String.format("%d %s",
state.heatActionStateMachine.getRemainingDuration(),
state.heatActionStateMachine.getActionname()
);
}
LocalPoint stageLoc = stageObject.getLocalLocation();
stageLoc = new LocalPoint(stageLoc.getX(), stageLoc.getY());
Point pos = Perspective.getCanvasTextLocation(client, graphics, stageLoc, text, 50);
Color color = config.lavaWaterfallColour();
renderTextLocation(graphics, pos, text, color, config.textBackground(), config.textOutline());
}
private void drawHeatChangers(Graphics2D graphics)
{
int change = state.getHeatChangeNeeded();
Shape shape = null;
boolean isLava = change > 0;
boolean isWaterfall = change < 0;
if (isWaterfall || state.heatActionStateMachine.isCooling())
{
shape = waterfall.getClickbox();
}
else if (isLava || state.heatActionStateMachine.isHeating())
{
shape = lavaPool.getClickbox();
}
if (shape != null)
{
shape = scaleShape(shape);
Point mousePosition = client.getMouseCanvasPosition();
Color color = config.lavaWaterfallColour();
if (shape.contains(mousePosition.getX(), mousePosition.getY()))
{
graphics.setColor(color.darker());
}
else
{
graphics.setColor(color);
}
graphics.draw(shape);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
if (config.drawLavaWaterInfoOverlay())
{
if (state.heatActionStateMachine.isCooling())
{
drawHeatChangerOverlay(graphics, waterfall);
}
else if (isWaterfall)
{
drawHeatChangerPreviewOverlay(graphics, waterfall, false);
}
if (state.heatActionStateMachine.isHeating())
{
drawHeatChangerOverlay(graphics, lavaPool);
}
else if (isLava)
{
drawHeatChangerPreviewOverlay(graphics, lavaPool, true);
}
}
}
private void drawCrucibleContent(Graphics2D graphics)
{
if (!config.drawCrucibleInfoOverlay())
{
return;
}
String text = String.format("%d/%d score: %d", state.getCrucibleCount(), CRUCIBLE_CAPACITY, (int)state.getCrucibleScore());
LocalPoint crucibleLoc = crucible.getLocalLocation();
crucibleLoc = new LocalPoint(crucibleLoc.getX() - 100, crucibleLoc.getY());
Point pos = Perspective.getCanvasTextLocation(client, graphics, crucibleLoc, text, 200);
Color color;
if (state.getCrucibleCount() == CRUCIBLE_CAPACITY)
{
color = config.toolGood();
}
else
{
color = config.generalHighlight();
}
renderTextLocation(graphics, pos, text, color, config.textBackground(), config.textOutline());
}
private void drawMouldScoreIfMouldSet(Graphics2D graphics) {
if (client.getVarbitValue(SWORD_TYPE_1_VARBIT) == 0)
{
return;
}
if (client.getVarbitValue(VARBIT_GAME_STAGE) != 1)
{
return;
}
if (state.getMouldScore() < 0)
{
return;
}
String text = String.format("score: %d", state.getMouldScore());
LocalPoint mouldLoc = mouldJig.getLocalLocation();
Point pos = Perspective.getCanvasTextLocation(client, graphics, mouldLoc, text, 115);
Color color = config.generalHighlight();
renderTextLocation(graphics, pos, text, color, config.textBackground(), config.textOutline());
}
private void drawPreformScoreIfPoured(Graphics2D graphics) {
if (client.getVarbitValue(VARBIT_GAME_STAGE) != 2)
{
return;
}
if (state.getMouldScore() < 0 || state.getLastKnownCrucibleScore() < 0)
{
return;
}
int preformScore = state.getLastKnownCrucibleScore() + state.getMouldScore();
String text = String.format("score: %d", preformScore);
LocalPoint mouldLoc = mouldJig.getLocalLocation();
Point pos = Perspective.getCanvasTextLocation(client, graphics, mouldLoc, text, 115);
Color color = config.generalHighlight();
renderTextLocation(graphics, pos, text, color, config.textBackground(), config.textOutline());
}
private void drawCrucibleIfMouldSet(Graphics2D graphics)
{
if (client.getVarbitValue(SWORD_TYPE_1_VARBIT) == 0)
{
return;
}
if (client.getVarbitValue(VARBIT_GAME_STAGE) != 1)
{
return;
}
if (config.highlightStyle() == HighlightStyle.HIGHLIGHT_CLICKBOX)
{
Shape shape = crucible.getConvexHull();
if (shape != null)
{
shape = scaleShape(shape);
Color color = config.generalHighlight();
if (state.getCrucibleCount() == CRUCIBLE_CAPACITY)
{
graphics.setColor(config.toolGood());
}
else
{
graphics.setColor(config.generalHighlight());
}
graphics.draw(shape);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
}
else if (config.highlightStyle() == HighlightStyle.HIGHLIGHT_BORDER)
{
Color color;
if (state.getCrucibleCount() == CRUCIBLE_CAPACITY)
{
color = config.toolGood();
}
else
{
color = config.generalHighlight();
}
drawObjectOutline(graphics, crucible, color);
}
drawCrucibleContent(graphics);
}
private void drawMouldIfNotSet(Graphics2D graphics)
{
if (client.getWidget(WIDGET_PROGRESS_PARENT) != null
|| client.getVarbitValue(SWORD_TYPE_1_VARBIT) == 0
|| (client.getVarbitValue(VARBIT_GAME_STAGE) != 0
&& client.getVarbitValue(VARBIT_GAME_STAGE) != 2))
{
return;
}
if (config.highlightStyle() == HighlightStyle.HIGHLIGHT_CLICKBOX)
{
Shape shape = mouldJig.getConvexHull();
if (shape != null)
{
shape = scaleShape(shape);
Color color = config.generalHighlight();
graphics.setColor(color);
graphics.draw(shape);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
}
else if (config.highlightStyle() == HighlightStyle.HIGHLIGHT_BORDER)
{
drawObjectOutline(graphics, mouldJig, config.generalHighlight());
}
if (config.drawMouldInfoOverlay())
{
CommissionType type1 = CommissionType.forVarbit(client.getVarbitValue(SWORD_TYPE_1_VARBIT));
CommissionType type2 = CommissionType.forVarbit(client.getVarbitValue(SWORD_TYPE_2_VARBIT));
String text = StringUtils.capitalize(type1.toString().toLowerCase()) + " " + StringUtils.capitalize(type2.toString().toLowerCase());
LocalPoint textLocation = mouldJig.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 100);
canvasLocation = new Point(canvasLocation.getX(), canvasLocation.getY() + 10);
renderTextLocation(graphics, canvasLocation, text, config.generalHighlight(), config.textBackground(), config.textOutline());
}
}
private Shape scaleShape(Shape shape)
{
if (shape == null)
{
return null;
}
double scale = config.clickboxScale() / 100.0;
if (Math.abs(scale - 1.0) < 0.01)
{
return shape;
}
Rectangle2D bounds = shape.getBounds2D();
AffineTransform transform = new AffineTransform();
transform.translate(bounds.getCenterX(), bounds.getCenterY());
transform.scale(scale, scale);
transform.translate(-bounds.getCenterX(), -bounds.getCenterY());
return transform.createTransformedShape(shape);
}
private void drawStorage(Graphics2D graphics)
{
Shape shape = storage.getConvexHull();
if (shape != null)
{
shape = scaleShape(shape);
Color color = config.generalHighlight();
graphics.setColor(color);
graphics.draw(shape);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
}
private void drawKovacIfHandIn(Graphics2D graphics)
{
Widget handInWidget = client.getWidget(HAND_IN_WIDGET);
if (handInWidget != null && !handInWidget.isHidden())
{
Shape shape = kovac.getConvexHull();
if (shape != null)
{
shape = scaleShape(shape);
Color color = config.generalHighlight();
graphics.setColor(color);
graphics.draw(shape);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
}
}
private void drawActionOverlay(Graphics2D graphics, GameObject gameObject)
{
int actionsLeft = state.getActionsLeftInStage();
int heatLeft = state.getActionsForHeatLevel();
// Draw heat left
if (config.drawHeatLeftOverlay())
{
String text = "Heat left: " + heatLeft;
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
if (canvasLocation == null)
{
return;
}
renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft), config.textBackground(), config.textOutline());
}
if (config.drawActionLeftOverlay())
// Draw actions left
{
String text = "Actions left: " + actionsLeft;
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
if (canvasLocation == null)
{
return;
}
canvasLocation = new Point(canvasLocation.getX(), canvasLocation.getY() + graphics.getFontMetrics().getHeight());
renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft), config.textBackground(), config.textOutline());
}
}
}

View File

@@ -0,0 +1,385 @@
package ee.futur.easygiantsfoundry;
//import java.util.ArrayList;
//import java.util.List;
import ee.futur.easygiantsfoundry.enums.Stage;
import lombok.Value;
/**
* Solves the heating/cooling action and predicts tick duration (index)
* the naming convention is focused on the algorithm rather than in-game terminology for the context.
* <p>
* the dx_n refers to successive derivatives of an ordinary-differential-equations
* https://en.wikipedia.org/wiki/Ordinary_differential_equation
* also known as position (dx0), speed (dx1), and acceleration (dx2).
* <p>
* dx0 - players current heat at tick
* dx1 - dx0_current - dx0_last_tick, aka the first derivative
* dx2 - dx1_current - dx1_last_tick, aka the second derivative
* <p>
* for context, here's what dx1 extracted directly from in-game dunking looks like.
* the purpose of the HeatActionSolver.java is to accurately model this data.
* int[] dx1 = {
* 7,
* 8,
* 9,
* 11,
* 13,
* 15,
* 17,
* 19,
* 21,
* 24,
* 27, -- dunk/quench starts here
* 30,
* 33,
* 37,
* 41,
* 45,
* 49,
* 53,
* 57,
* 62,
* 67,
* 72,
* 77,
* 83,
* 89,
* 95,
* 91,
* };
*/
/* The following code-snippet can be copy-pasted into runelite developer-tools "Shell" to extract dx1
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Clipboard;
import java.util.concurrent.atomic.AtomicInteger;
int HEAT_ID = 13948;
AtomicInteger tickCounter = new AtomicInteger(-1);
AtomicInteger prevHeat = new AtomicInteger(client.getVarbitValue(HEAT_ID));
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
String output = "";
subscribe(VarbitChanged.class, ev ->
{
if (ev.getVarbitId() == HEAT_ID)
{
int deltaHeat = ev.getValue() - prevHeat.getAndSet(ev.getValue());
if (deltaHeat == -1) return; // ignore passive drain
String str = "[" + tickCounter.incrementAndGet()
+ "] deltaHeat: " + deltaHeat;
log.info(str);
output = output + deltaHeat + "\n";
StringSelection selection = new StringSelection(output);
clipboard.setContents(selection, selection);
}
});
*/
public class HeatActionSolver
{
public static final int[] DX_1 = new int[]{
7,
8,
9,
11,
13,
15,
17,
19,
21,
24,
27, // -- dunk/quench starts here
30,
33,
37,
41,
45,
49,
53,
57,
62,
67,
72,
77,
83,
89,
95,
91, // last one will always overshoot 1000
};
// index is stage, ordinal order
public static final int[] TOOL_TICK_CYCLE = new int[] {
5,
2,
2
};
public static final int MAX_INDEX = DX_1.length;
public static final int FAST_INDEX = 10;
@Value(staticConstructor = "of")
public static class SolveResult
{
int index;
int dx0;
int dx1;
int dx2;
}
private static SolveResult heatingSolve(int start, int goal, boolean overshoot, int max, boolean isFast)
{
return relativeSolve(goal - start, overshoot, max - start, isFast, -1);
}
private static SolveResult coolingSolve(int start, int goal, boolean overshoot, int min, boolean isFast)
{
return relativeSolve(start - goal, overshoot, start - min, isFast, 1);
}
private static SolveResult relativeSolve(int goal, boolean overshoot, int max, boolean isFast, int decayValue)
{
int index = isFast ? FAST_INDEX : 0;
int dx0 = 0;
boolean decay = false;
while (true) {
if (index > MAX_INDEX)
{
break;
}
if (!overshoot && dx0 + DX_1[index] > goal)
{
break;
}
else if (overshoot && dx0 >= goal)
{
break;
}
if (dx0 + DX_1[index] >= max)
{
break;
}
if (decay)
{
dx0 -= decayValue;
}
dx0 += DX_1[index];
++index;
decay = !decay;
}
if (isFast)
{
index -= FAST_INDEX;
}
return SolveResult.of(index, dx0, DX_1[index], -1);
}
@Value(staticConstructor = "of")
public static class DurationResult
{
int duration;
boolean goalInRange;
boolean overshooting;
int predictedHeat;
}
public static DurationResult solve(
Stage stage,
int[] range,
int actionLeftInStage,
int start,
boolean isFast,
boolean isActionHeating,
int paddingTicks,
boolean isRunning)
{
final boolean isStageHeating = stage.isHeating();
// adding tool cycle ticks because the first cycle at a tool is almost always nulled
// (unless manually reaching the tile, then clicking the tool)
final int toolDelay = TOOL_TICK_CYCLE[stage.ordinal()];
final int travelTicks = solveTravelTicks(isRunning, stage, isActionHeating) + toolDelay;
final int travelDecay = (int) Math.ceil((double) travelTicks / 2);
final int paddingDecay = (int) Math.ceil((double) paddingTicks / 2);
// adding 2.4s/8ticks worth of padding so preform doesn't decay out of range
// average distance from lava+waterfall around 8 ticks
// preform decays 1 heat every 2 ticks
final int min = Math.max(0, Math.min(1000, range[0] + paddingDecay + travelDecay));
final int max = Math.max(0, Math.min(1000, range[1] + paddingDecay + travelDecay));
final int actionsLeft_DeltaHeat = actionLeftInStage * stage.getHeatChange();
int estimatedDuration = 0;
final boolean goalInRange;
boolean overshoot = false;
SolveResult result = null;
// case actions are all cooling, heating is mirrored version
// goal: in-range // stage: heating
// overshoot goal
// <----------|stop|<---------------- heat
// ------|min|----------goal-------|max|
// stage ---------------->
// goal: out-range // stage: heating
// undershoot min
// ...----------|stop|<--------------------- heat
// -goal---|min|---------------------|max|
// stage ----------------->
// goal: in-range // stage: cooling
// undershoot goal
// <-------------------------|stop|<--------------- heat
// ------|min|----------goal-------|max|
// <---------------- stage
// goal: out-range // stage: cooling
// overshoot max
// <--------------------|stop|<--------------- heat
// --------|min|---------------------|max|----goal
// <---------------- stage
if (isActionHeating)
{
int goal = min - actionsLeft_DeltaHeat;
goalInRange = goal >= min && goal <= max;
if (isStageHeating)
{
if (start <= max)
{
overshoot = !goalInRange;
if (!goalInRange)
{
goal = min;
}
result = heatingSolve(start, goal, overshoot, max, isFast);
estimatedDuration = result.index;
}
}
else // cooling stage
{
// actionsLeft_DeltaHeat is negative here
if (start <= max)
{
overshoot = goalInRange;
if (!goalInRange)
{
goal = max;
}
result = heatingSolve(start, goal, overshoot, max, isFast);
estimatedDuration = result.index;
}
}
}
else // cooling action
{
int goal = max - actionsLeft_DeltaHeat;
goalInRange = goal >= min && goal <= max;
if (isStageHeating)
{
if (start >= min)
{
overshoot = goalInRange;
if (!goalInRange)
{
goal = min;
}
result = coolingSolve(start, goal, overshoot, min, isFast);
estimatedDuration = result.index;
}
}
else // cooling stage cooling action
{
if (start >= min)
{
overshoot = !goalInRange;
if (!goalInRange)
{
goal = max;
}
result = coolingSolve(start, goal, overshoot, min, isFast);
estimatedDuration = result.index;
}
}
}
int dx0 = result == null ? 0 : result.dx0;
if (!isActionHeating)
{
dx0 *= -1;
}
return DurationResult.of(estimatedDuration, goalInRange, overshoot, start + dx0);
}
private static int solveTravelTicks(boolean isRunning, Stage stage, boolean isLava)
{
final int distance;
if (isLava)
{
distance = stage.getDistanceToLava();
}
else
{
distance = stage.getDistanceToWaterfall();
}
if (isRunning)
{
// for odd distances, like 7
// 7 / 2 = 3.5
// rounded to 4
return (int) Math.ceil((double) distance / 2);
}
else
{
return distance;
}
}
}

View File

@@ -0,0 +1,219 @@
package ee.futur.easygiantsfoundry;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* A state-machine that keeps track of heating/cooling actions.
*/
@Slf4j
@Data
public class HeatActionStateMachine
{
/**
* Tick counter for heating, -1 means not currently heating.
*/
int heatingTicks = -1;
/**
* Tick counter for cooling, -1 means not currently cooling.
*/
int coolingTicks = -1;
boolean actionFast;
boolean actionHeating;
/**
* The starting heat amount of the heating/cooling action.
*/
int startingHeat;
/**
* The estimated tick duration of the heating/cooling action.
*/
int estimatedDuration;
/**
* The goal heat amount of the heating/cooling action.
*/
int goalHeat = 0;
// debug
boolean goalInRange;
boolean isOverShooting;
int predictedHeat;
/**
* The last action the player clicked on. Used for ui overlay to display.
* When null, the state-machine will stop() and reset.
*/
String actionname = null;
private EasyGiantsFoundryState state;
private EasyGiantsFoundryConfig config;
/**
* Start the state-machine with the given parameters.
* <p>
* These parameters have to be set-up manually before start().
* Velocity, AccelerationBonus, ActionName
*
* @param state the current state of the foundry
* @param config the current configuration of the plugin
* @param startingHeat the starting heat amount
* @see HeatActionStateMachine#setup(boolean, boolean, String)
*/
public void start(EasyGiantsFoundryState state, EasyGiantsFoundryConfig config, int startingHeat)
{
// use Velocity to determine if heating or cooling
if (actionHeating)
{
heatingTicks = 0;
coolingTicks = -1;
}
else
{
heatingTicks = -1;
coolingTicks = 0;
}
this.startingHeat = startingHeat;
this.state = state;
this.config = config;
updateEstimates();
}
/**
* Get the estimated remaining duration of the heating/cooling action.
*
* @return the estimated remaining duration in ticks
*/
public int getRemainingDuration()
{
if (isHeating())
{
return Math.max(0, (estimatedDuration - heatingTicks));
}
else if (isCooling())
{
return Math.max(0, (estimatedDuration - coolingTicks));
}
else
{
return 0;
}
}
/**
* Core logic. Runs once on {@link HeatActionStateMachine#start} and assumes synchronization with the game.
* Calculate the estimated duration and goal heat amount of the heating/cooling action.
*/
public void updateEstimates()
{
HeatActionSolver.DurationResult result =
HeatActionSolver.solve(
getState().getCurrentStage(),
getState().getCurrentHeatRange(),
getState().getActionsLeftInStage(),
getStartingHeat(),
actionFast,
isHeating(),
config.heatActionPadTicks(),
state.isPlayerRunning()
);
goalInRange = result.isGoalInRange();
isOverShooting = result.isOvershooting();
predictedHeat = result.getPredictedHeat();
estimatedDuration = result.getDuration();
}
/**
* Helper to remind the neccessary parameters to start the state-machine.
*
* @param actionName the name of the action to display in the ui overlay
*/
public void setup(boolean isFast, boolean isHeating, String actionName)
{
actionFast = isFast;
actionHeating = isHeating;
actionname = actionName;
}
/**
* Stop the state-machine.
*/
public void stop()
{
heatingTicks = -1;
coolingTicks = -1;
actionname = null;
}
/**
* Check if the state is currently heating.
*
* @return true if heating, false otherwise
*/
public boolean isHeating()
{
return heatingTicks >= 0;
}
/**
* Check if the state is currently cooling.
*
* @return true if cooling, false otherwise
*/
public boolean isCooling()
{
return coolingTicks >= 0;
}
/**
* Check if the heating/cooling state is currently idle. Neither heating nor cooling.
*
* @return
*/
public boolean isIdle()
{
return !(isHeating() || isCooling());
}
/**
* Tick the state-machine. This has to be called onVarbitChanged in order to sync with the game.
*/
public void onTick()
{
if (isIdle()) return;
if (isHeating())
{
if (heatingTicks >= estimatedDuration)
{
stop();
}
else
{
heatingTicks++;
}
}
else if (isCooling())
{
if (coolingTicks >= estimatedDuration)
{
stop();
}
else
{
coolingTicks++;
}
}
}
}

View File

@@ -0,0 +1,7 @@
package ee.futur.easygiantsfoundry;
public enum HighlightStyle
{
HIGHLIGHT_BORDER,
HIGHLIGHT_CLICKBOX
}

View File

@@ -0,0 +1,9 @@
package ee.futur.easygiantsfoundry;
public class MathUtil
{
public static double max1(double a)
{
return a > 0 ? a : 1;
}
}

View File

@@ -0,0 +1,148 @@
package ee.futur.easygiantsfoundry;
import ee.futur.easygiantsfoundry.enums.MetalBarSource;
import ee.futur.easygiantsfoundry.enums.MetalBarType;
import lombok.Getter;
import lombok.Value;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.gameval.InventoryID;
import net.runelite.api.gameval.ItemID;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
@Singleton
public class MetalBarCounter
{
private final Map<Integer, Map<MetalBarType, CountsBySource>> index = new HashMap<>(); // container id -> bar counts
@Inject
private EasyGiantsFoundryConfig config;
@Getter
private boolean seenBank = false;
public int get(MetalBarType type)
{
int count = 0;
for (Map<MetalBarType, CountsBySource> counts : index.values())
{
count += counts.get(type).sum(config);
}
return count;
}
public void clear()
{
index.clear();
seenBank = false;
}
public void put(ItemContainer container)
{
int tinOre = 0;
int copperOre = 0;
int ironOre = 0;
if (container.getId() == InventoryID.BANK)
{
seenBank = true;
}
Map<MetalBarType, CountsBySource> counts = newCounts();
for (Item item : container.getItems())
{
MetalBarValues.Record record = MetalBarValues.get(item.getId());
if (record == null)
{
continue;
}
// ore special cases:
// * add bronze bars equal to min(tin, copper)
// * there is an edge case here where it won't sum quite right multiple item containers but I don't really
// care to code for that right now
// * iron ore counts for both iron and steel
switch (item.getId())
{
case ItemID.TIN_ORE:
case ItemID.Cert.TIN_ORE:
tinOre += item.getQuantity();
break;
case ItemID.COPPER_ORE:
case ItemID.Cert.COPPER_ORE:
copperOre += item.getQuantity();
break;
case ItemID.IRON_ORE:
case ItemID.Cert.IRON_ORE:
ironOre += item.getQuantity();
break;
default:
counts.compute(record.getType(), (k, v) ->
v.add(record.getSource(), record.getValue() * item.getQuantity()));
}
}
int finalTinOre = tinOre;
int finalCopperOre = copperOre;
int finalIronOre = ironOre;
counts.compute(MetalBarType.BRONZE, (k, v) ->
v.add(MetalBarSource.ORE, Math.min(finalTinOre, finalCopperOre)));
counts.compute(MetalBarType.IRON, (k, v) ->
v.add(MetalBarSource.ORE, finalIronOre));
counts.compute(MetalBarType.STEEL, (k, v) ->
v.add(MetalBarSource.ORE, finalIronOre));
index.put(container.getId(), counts);
}
private static Map<MetalBarType, CountsBySource> newCounts()
{
Map<MetalBarType, CountsBySource> counts = new HashMap<>();
counts.put(MetalBarType.BRONZE, CountsBySource.empty());
counts.put(MetalBarType.IRON, CountsBySource.empty());
counts.put(MetalBarType.STEEL, CountsBySource.empty());
counts.put(MetalBarType.MITHRIL, CountsBySource.empty());
counts.put(MetalBarType.ADAMANT, CountsBySource.empty());
counts.put(MetalBarType.RUNITE, CountsBySource.empty());
return counts;
}
@Value
private static class CountsBySource
{
int ores, bars, equipment;
public static CountsBySource empty()
{
return new CountsBySource(0, 0, 0);
}
public CountsBySource add(MetalBarSource source, int count)
{
switch (source)
{
case ORE:
return new CountsBySource(ores + count, bars, equipment);
case BAR:
return new CountsBySource(ores, bars + count, equipment);
case EQUIPMENT:
return new CountsBySource(ores, bars, equipment + count);
default:
return this;
}
}
public int sum(EasyGiantsFoundryConfig config)
{
int sum = config.countOre() ? ores : 0;
sum += config.countBars() ? bars : 0;
sum += config.countEquipment() ? equipment : 0;
return sum;
}
}
}

View File

@@ -0,0 +1,238 @@
package ee.futur.easygiantsfoundry;
import ee.futur.easygiantsfoundry.enums.MetalBarSource;
import ee.futur.easygiantsfoundry.enums.MetalBarType;
import lombok.Value;
import net.runelite.api.gameval.ItemID;
import java.util.HashMap;
public class MetalBarValues
{
private static final HashMap<Integer, Record> values = new HashMap<>();
static
{
// tin, copper, and iron are included here so the counter doesn't ignore them, but their actual counts are
// handled as special cases
putOre(ItemID.TIN_ORE, MetalBarType.BRONZE);
putOre(ItemID.COPPER_ORE, MetalBarType.BRONZE);
putOre(ItemID.IRON_BAR, MetalBarType.IRON);
putOre(ItemID.MITHRIL_ORE, MetalBarType.MITHRIL);
putOre(ItemID.ADAMANTITE_ORE, MetalBarType.ADAMANT);
putOre(ItemID.RUNITE_ORE, MetalBarType.RUNITE);
putOre(ItemID.Cert.TIN_ORE, MetalBarType.BRONZE);
putOre(ItemID.Cert.COPPER_ORE, MetalBarType.BRONZE);
putOre(ItemID.Cert.IRON_BAR, MetalBarType.IRON);
putOre(ItemID.Cert.MITHRIL_ORE, MetalBarType.MITHRIL);
putOre(ItemID.Cert.ADAMANTITE_ORE, MetalBarType.ADAMANT);
putOre(ItemID.Cert.RUNITE_ORE, MetalBarType.RUNITE);
putBar(ItemID.BRONZE_BAR, MetalBarType.BRONZE);
putBar(ItemID.IRON_BAR, MetalBarType.IRON);
putBar(ItemID.STEEL_BAR, MetalBarType.STEEL);
putBar(ItemID.MITHRIL_BAR, MetalBarType.MITHRIL);
putBar(ItemID.ADAMANTITE_BAR, MetalBarType.ADAMANT);
putBar(ItemID.RUNITE_BAR, MetalBarType.RUNITE);
putBar(ItemID.Cert.BRONZE_BAR, MetalBarType.BRONZE);
putBar(ItemID.Cert.IRON_BAR, MetalBarType.IRON);
putBar(ItemID.Cert.STEEL_BAR, MetalBarType.STEEL);
putBar(ItemID.Cert.MITHRIL_BAR, MetalBarType.MITHRIL);
putBar(ItemID.Cert.ADAMANTITE_BAR, MetalBarType.ADAMANT);
putBar(ItemID.Cert.RUNITE_BAR, MetalBarType.RUNITE);
putEquipment(ItemID.BRONZE_SCIMITAR, MetalBarType.BRONZE, 1);
putEquipment(ItemID.BRONZE_LONGSWORD, MetalBarType.BRONZE, 1);
putEquipment(ItemID.BRONZE_FULL_HELM, MetalBarType.BRONZE, 1);
putEquipment(ItemID.BRONZE_SQ_SHIELD, MetalBarType.BRONZE, 1);
putEquipment(ItemID.BRONZE_CLAWS, MetalBarType.BRONZE, 1);
putEquipment(ItemID.BRONZE_WARHAMMER, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_BATTLEAXE, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_CHAINBODY, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_KITESHIELD, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_2H_SWORD, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_PLATELEGS, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_PLATESKIRT, MetalBarType.BRONZE, 2);
putEquipment(ItemID.BRONZE_PLATEBODY, MetalBarType.BRONZE, 4);
putEquipment(ItemID.Cert.BRONZE_SCIMITAR, MetalBarType.BRONZE, 1);
putEquipment(ItemID.Cert.BRONZE_LONGSWORD, MetalBarType.BRONZE, 1);
putEquipment(ItemID.Cert.BRONZE_FULL_HELM, MetalBarType.BRONZE, 1);
putEquipment(ItemID.Cert.BRONZE_SQ_SHIELD, MetalBarType.BRONZE, 1);
putEquipment(ItemID.Cert.BRONZE_CLAWS, MetalBarType.BRONZE, 1);
putEquipment(ItemID.Cert.BRONZE_WARHAMMER, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_BATTLEAXE, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_CHAINBODY, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_KITESHIELD, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_2H_SWORD, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_PLATELEGS, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_PLATESKIRT, MetalBarType.BRONZE, 2);
putEquipment(ItemID.Cert.BRONZE_PLATEBODY, MetalBarType.BRONZE, 4);
putEquipment(ItemID.IRON_SCIMITAR, MetalBarType.IRON, 1);
putEquipment(ItemID.IRON_LONGSWORD, MetalBarType.IRON, 1);
putEquipment(ItemID.IRON_FULL_HELM, MetalBarType.IRON, 1);
putEquipment(ItemID.IRON_SQ_SHIELD, MetalBarType.IRON, 1);
putEquipment(ItemID.IRON_CLAWS, MetalBarType.IRON, 1);
putEquipment(ItemID.IRON_WARHAMMER, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_BATTLEAXE, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_CHAINBODY, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_KITESHIELD, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_2H_SWORD, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_PLATELEGS, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_PLATESKIRT, MetalBarType.IRON, 2);
putEquipment(ItemID.IRON_PLATEBODY, MetalBarType.IRON, 4);
putEquipment(ItemID.Cert.IRON_SCIMITAR, MetalBarType.IRON, 1);
putEquipment(ItemID.Cert.IRON_LONGSWORD, MetalBarType.IRON, 1);
putEquipment(ItemID.Cert.IRON_FULL_HELM, MetalBarType.IRON, 1);
putEquipment(ItemID.Cert.IRON_SQ_SHIELD, MetalBarType.IRON, 1);
putEquipment(ItemID.Cert.IRON_CLAWS, MetalBarType.IRON, 1);
putEquipment(ItemID.Cert.IRON_WARHAMMER, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_BATTLEAXE, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_CHAINBODY, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_KITESHIELD, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_2H_SWORD, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_PLATELEGS, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_PLATESKIRT, MetalBarType.IRON, 2);
putEquipment(ItemID.Cert.IRON_PLATEBODY, MetalBarType.IRON, 4);
putEquipment(ItemID.STEEL_SCIMITAR, MetalBarType.STEEL, 1);
putEquipment(ItemID.STEEL_LONGSWORD, MetalBarType.STEEL, 1);
putEquipment(ItemID.STEEL_FULL_HELM, MetalBarType.STEEL, 1);
putEquipment(ItemID.STEEL_SQ_SHIELD, MetalBarType.STEEL, 1);
putEquipment(ItemID.STEEL_CLAWS, MetalBarType.STEEL, 1);
putEquipment(ItemID.STEEL_WARHAMMER, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_BATTLEAXE, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_CHAINBODY, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_KITESHIELD, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_2H_SWORD, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_PLATELEGS, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_PLATESKIRT, MetalBarType.STEEL, 2);
putEquipment(ItemID.STEEL_PLATEBODY, MetalBarType.STEEL, 4);
putEquipment(ItemID.Cert.STEEL_SCIMITAR, MetalBarType.STEEL, 1);
putEquipment(ItemID.Cert.STEEL_LONGSWORD, MetalBarType.STEEL, 1);
putEquipment(ItemID.Cert.STEEL_FULL_HELM, MetalBarType.STEEL, 1);
putEquipment(ItemID.Cert.STEEL_SQ_SHIELD, MetalBarType.STEEL, 1);
putEquipment(ItemID.Cert.STEEL_CLAWS, MetalBarType.STEEL, 1);
putEquipment(ItemID.Cert.STEEL_WARHAMMER, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_BATTLEAXE, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_CHAINBODY, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_KITESHIELD, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_2H_SWORD, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_PLATELEGS, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_PLATESKIRT, MetalBarType.STEEL, 2);
putEquipment(ItemID.Cert.STEEL_PLATEBODY, MetalBarType.STEEL, 4);
putEquipment(ItemID.MITHRIL_SCIMITAR, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.MITHRIL_LONGSWORD, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.MITHRIL_FULL_HELM, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.MITHRIL_SQ_SHIELD, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.MITHRIL_CLAWS, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.MITHRIL_WARHAMMER, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_BATTLEAXE, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_CHAINBODY, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_KITESHIELD, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_2H_SWORD, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_PLATELEGS, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_PLATESKIRT, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.MITHRIL_PLATEBODY, MetalBarType.MITHRIL, 4);
putEquipment(ItemID.Cert.MITHRIL_SCIMITAR, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.Cert.MITHRIL_LONGSWORD, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.Cert.MITHRIL_FULL_HELM, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.Cert.MITHRIL_SQ_SHIELD, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.Cert.MITHRIL_CLAWS, MetalBarType.MITHRIL, 1);
putEquipment(ItemID.Cert.MITHRIL_WARHAMMER, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_BATTLEAXE, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_CHAINBODY, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_KITESHIELD, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_2H_SWORD, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_PLATELEGS, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_PLATESKIRT, MetalBarType.MITHRIL, 2);
putEquipment(ItemID.Cert.MITHRIL_PLATEBODY, MetalBarType.MITHRIL, 4);
putEquipment(ItemID.ADAMANT_SCIMITAR, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.ADAMANT_LONGSWORD, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.ADAMANT_FULL_HELM, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.ADAMANT_SQ_SHIELD, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.ADAMANT_CLAWS, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.ADAMNT_WARHAMMER, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_BATTLEAXE, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_CHAINBODY, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_KITESHIELD, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_2H_SWORD, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_PLATELEGS, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_PLATESKIRT, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.ADAMANT_PLATEBODY, MetalBarType.ADAMANT, 4);
putEquipment(ItemID.Cert.ADAMANT_SCIMITAR, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.Cert.ADAMANT_LONGSWORD, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.Cert.ADAMANT_FULL_HELM, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.Cert.ADAMANT_SQ_SHIELD, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.Cert.ADAMANT_CLAWS, MetalBarType.ADAMANT, 1);
putEquipment(ItemID.Cert.ADAMNT_WARHAMMER, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_BATTLEAXE, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_CHAINBODY, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_KITESHIELD, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_2H_SWORD, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_PLATELEGS, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_PLATESKIRT, MetalBarType.ADAMANT, 2);
putEquipment(ItemID.Cert.ADAMANT_PLATEBODY, MetalBarType.ADAMANT, 4);
putEquipment(ItemID.RUNE_SCIMITAR, MetalBarType.RUNITE, 1);
putEquipment(ItemID.RUNE_LONGSWORD, MetalBarType.RUNITE, 1);
putEquipment(ItemID.RUNE_FULL_HELM, MetalBarType.RUNITE, 1);
putEquipment(ItemID.RUNE_SQ_SHIELD, MetalBarType.RUNITE, 1);
putEquipment(ItemID.RUNE_CLAWS, MetalBarType.RUNITE, 1);
putEquipment(ItemID.RUNE_WARHAMMER, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_BATTLEAXE, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_CHAINBODY, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_KITESHIELD, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_2H_SWORD, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_PLATELEGS, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_PLATESKIRT, MetalBarType.RUNITE, 2);
putEquipment(ItemID.RUNE_PLATEBODY, MetalBarType.RUNITE, 4);
putEquipment(ItemID.Cert.RUNE_SCIMITAR, MetalBarType.RUNITE, 1);
putEquipment(ItemID.Cert.RUNE_LONGSWORD, MetalBarType.RUNITE, 1);
putEquipment(ItemID.Cert.RUNE_FULL_HELM, MetalBarType.RUNITE, 1);
putEquipment(ItemID.Cert.RUNE_SQ_SHIELD, MetalBarType.RUNITE, 1);
putEquipment(ItemID.Cert.RUNE_CLAWS, MetalBarType.RUNITE, 1);
putEquipment(ItemID.Cert.RUNE_WARHAMMER, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_BATTLEAXE, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_CHAINBODY, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_KITESHIELD, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_2H_SWORD, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_PLATELEGS, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_PLATESKIRT, MetalBarType.RUNITE, 2);
putEquipment(ItemID.Cert.RUNE_PLATEBODY, MetalBarType.RUNITE, 4);
}
@Value
public static class Record
{
MetalBarType type;
MetalBarSource source;
int value;
}
public static Record get(int id)
{
return values.get(id);
}
private static void putOre(int id, MetalBarType type)
{
values.put(id, new Record(type, MetalBarSource.ORE, 1));
}
private static void putBar(int id, MetalBarType type)
{
values.put(id, new Record(type, MetalBarSource.BAR, 1));
}
private static void putEquipment(int id, MetalBarType type, int value)
{
values.put(id, new Record(type, MetalBarSource.EQUIPMENT, value));
}
private MetalBarValues()
{
}
}

View File

@@ -0,0 +1,146 @@
package ee.futur.easygiantsfoundry;
import ee.futur.easygiantsfoundry.enums.CommissionType;
import ee.futur.easygiantsfoundry.enums.Mould;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.ScriptID;
import net.runelite.api.widgets.Widget;
import net.runelite.client.callback.ClientThread;
public class MouldHelper
{
static final int MOULD_LIST_PARENT = 47054857;
static final int DRAW_MOULD_LIST_SCRIPT = 6093;
static final int REDRAW_MOULD_LIST_SCRIPT = 6095;
static final int RESET_MOULD_SCRIPT = 6108;
public static final int SELECT_MOULD_SCRIPT = 6098;
static final int SWORD_TYPE_1_VARBIT = 13907; // 4=Broad
static final int SWORD_TYPE_2_VARBIT = 13908; // 3=Flat
private static final int DISABLED_TEXT_COLOR = 0x9f9f9f;
private static final int SCORE_TYPE1_SCORE_WIDGET = 47054876;
private static final int SCORE_TYPE2_SCORE_WIDGET = 47054878;
@Inject
private Client client;
@Inject
private ClientThread clientThread;
@Inject
private EasyGiantsFoundryConfig config;
public Integer getTotalScore()
{
Widget type1Widget = client.getWidget(SCORE_TYPE1_SCORE_WIDGET);
Widget type2Widget = client.getWidget(SCORE_TYPE2_SCORE_WIDGET);
if (type1Widget == null || type2Widget == null)
{
return null;
}
String type1Str = type1Widget.getText();
String type2Str = type2Widget.getText();
// (+6) 6
// ^ space seperated
// or
// 6
if (type1Str.contains(" "))
{
type1Str = type1Str.substring(type1Str.lastIndexOf(' ') + 1);
}
if (type2Str.contains(" "))
{
type2Str = type2Str.substring(type2Str.lastIndexOf(' ') + 1);
}
int type1Score;
int type2Score;
try
{
type1Score = Integer.parseInt(type1Str);
type2Score = Integer.parseInt(type2Str);
} catch (NumberFormatException e)
{
return null;
}
return type1Score + type2Score;
}
public void selectBest(int scriptId)
{
Widget parent = client.getWidget(MOULD_LIST_PARENT);
if (parent == null || parent.getChildren() == null)
{
return;
}
Map<Mould, Widget> mouldToChild = getOptions(parent.getChildren());
int bestScore = -1;
Widget bestWidget = null;
CommissionType type1 = CommissionType.forVarbit(client.getVarbitValue(SWORD_TYPE_1_VARBIT));
CommissionType type2 = CommissionType.forVarbit(client.getVarbitValue(SWORD_TYPE_2_VARBIT));
for (Map.Entry<Mould, Widget> entry : mouldToChild.entrySet())
{
Mould mould = entry.getKey();
int score = mould.getScore(type1, type2);
if (score > bestScore)
{
bestScore = score;
bestWidget = entry.getValue();
}
}
if (bestWidget != null)
{
bestWidget.setTextColor(config.mouldTextColour().getRGB());
}
if (scriptId == DRAW_MOULD_LIST_SCRIPT || scriptId == REDRAW_MOULD_LIST_SCRIPT)
{
Widget scrollBar = client.getWidget(718, 11);
Widget scrollList = client.getWidget(718, 9);
if (scrollBar != null && scrollList != null)
{
int height = scrollList.getHeight();
int scrollMax = scrollList.getScrollHeight();
Widget finalBestWidget = bestWidget;
clientThread.invokeAtTickEnd(() ->
{
if (finalBestWidget != null)
{
client.runScript(
ScriptID.UPDATE_SCROLLBAR,
scrollBar.getId(),
scrollList.getId(),
Math.min(finalBestWidget.getOriginalY() - 2, scrollMax - height));
}
});
}
}
}
private Map<Mould, Widget> getOptions(Widget[] children)
{
Map<Mould, Widget> mouldToChild = new LinkedHashMap<>();
for (int i = 2; i < children.length; i += 17)
{
Widget child = children[i];
Mould mould = Mould.forName(child.getText());
if (mould != null && child.getTextColor() != DISABLED_TEXT_COLOR)
{
mouldToChild.put(mould, child);
}
}
return mouldToChild;
}
}

View File

@@ -0,0 +1,24 @@
package ee.futur.easygiantsfoundry.enums;
public enum CommissionType
{
NONE,
NARROW, // 1
LIGHT, // 2
FLAT, // 3
BROAD, // 4
HEAVY, // 5
SPIKED, // 6
;
public static final CommissionType[] values = CommissionType.values();
public static CommissionType forVarbit(int varbitValue)
{
if (varbitValue < 0 || varbitValue >= values.length)
{
return NONE;
}
return CommissionType.values[varbitValue];
}
}

View File

@@ -0,0 +1,27 @@
package ee.futur.easygiantsfoundry.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.runelite.client.ui.FontManager;
import java.awt.Font;
@Getter
@AllArgsConstructor
public enum FontType
{
DEFAULT("Default", null),
REGULAR("Regular", FontManager.getRunescapeFont()),
BOLD("Bold", FontManager.getRunescapeBoldFont()),
SMALL("Small", FontManager.getRunescapeSmallFont()),
;
private final String name;
private final Font font;
@Override
public String toString()
{
return name;
}
}

View File

@@ -0,0 +1,20 @@
package ee.futur.easygiantsfoundry.enums;
import java.awt.Color;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.runelite.client.ui.ColorScheme;
@Getter
@AllArgsConstructor
public enum Heat
{
LOW("Low", ColorScheme.PROGRESS_COMPLETE_COLOR),
MED("Medium", ColorScheme.PROGRESS_INPROGRESS_COLOR),
HIGH("High", ColorScheme.PROGRESS_ERROR_COLOR),
NONE("Not in range", ColorScheme.LIGHT_GRAY_COLOR);
private final String name;
private final Color color;
}

View File

@@ -0,0 +1,9 @@
package ee.futur.easygiantsfoundry.enums;
public enum MetalBarSource
{
ORE,
BAR,
EQUIPMENT,
;
}

View File

@@ -0,0 +1,12 @@
package ee.futur.easygiantsfoundry.enums;
public enum MetalBarType
{
BRONZE,
IRON,
STEEL,
MITHRIL,
ADAMANT,
RUNITE,
;
}

View File

@@ -0,0 +1,74 @@
package ee.futur.easygiantsfoundry.enums;
import com.google.common.collect.ImmutableMap;
import lombok.AllArgsConstructor;
import java.util.Map;
import static ee.futur.easygiantsfoundry.enums.CommissionType.*;
import static ee.futur.easygiantsfoundry.enums.MouldType.*;
@AllArgsConstructor
public enum Mould
{
CHOPPER_FORTE("Chopper Forte", FORTE, ImmutableMap.of(BROAD, 4, LIGHT, 4, FLAT, 4)),
GALDIUS_RICASSO("Galdius Ricasso", FORTE, ImmutableMap.of(BROAD, 4, HEAVY, 4, FLAT, 4)),
DISARMING_FORTE("Disarming Forte", FORTE, ImmutableMap.of(NARROW, 4, LIGHT, 4, SPIKED, 4)),
MEDUSA_RICASSO("Medusa Ricasso", FORTE, ImmutableMap.of(BROAD, 8, HEAVY, 6, FLAT, 8)),
SERPENT_RICASSO("Serpent Ricasso", FORTE, ImmutableMap.of(NARROW, 6, LIGHT, 8, FLAT, 8)),
SERRATED_FORTE("Serrated Forte", FORTE, ImmutableMap.of(NARROW, 8, HEAVY, 8, SPIKED, 6)),
STILETTO_FORTE("Stiletto Forte", FORTE, ImmutableMap.of(NARROW, 8, LIGHT, 10, FLAT, 8)),
DEFENDER_BASE("Defender Base", FORTE, ImmutableMap.of(BROAD, 8, HEAVY, 10, FLAT, 8)),
JUGGERNAUT_FORTE("Juggernaut Forte", FORTE, ImmutableMap.of(BROAD, 4, HEAVY, 4, SPIKED, 16)),
CHOPPER_FORTE_1("Chopper Forte +1", FORTE, ImmutableMap.of(BROAD, 3, LIGHT, 4, FLAT, 18)),
SPIKER("Spiker!", FORTE, ImmutableMap.of(NARROW, 1, HEAVY, 2, SPIKED, 22)),
SAW_BLADE("Saw Blade", BLADE, ImmutableMap.of(BROAD, 4, LIGHT, 4, SPIKED, 4)),
DEFENDERS_EDGE("Defenders Edge", BLADE, ImmutableMap.of(BROAD, 4, HEAVY, 4, SPIKED, 4)),
FISH_BLADE("Fish Blade", BLADE, ImmutableMap.of(NARROW, 4, LIGHT, 4, FLAT, 4)),
MEDUSA_BLADE("Medusa Blade", BLADE, ImmutableMap.of(BROAD, 8, HEAVY, 8, FLAT, 6)),
STILETTO_BLADE("Stiletto Blade", BLADE, ImmutableMap.of(NARROW, 8, LIGHT, 6, FLAT, 8)),
GLADIUS_EDGE("Gladius Edge", BLADE, ImmutableMap.of(NARROW, 6, HEAVY, 8, FLAT, 8)),
FLAMBERGE_BLADE("Flamberge Blade", BLADE, ImmutableMap.of(NARROW, 8, LIGHT, 8, SPIKED, 10)),
SERPENT_BLADE("Serpent Blade", BLADE, ImmutableMap.of(NARROW, 10, LIGHT, 8, FLAT, 8)),
CLAYMORE_BLADE("Claymore Blade", BLADE, ImmutableMap.of(BROAD, 16, HEAVY, 4, FLAT, 4)),
FLEUR_DE_BLADE("Fleur de Blade", BLADE, ImmutableMap.of(BROAD, 4, HEAVY, 18, SPIKED, 1)),
CHOPPA("Choppa!", BLADE, ImmutableMap.of(BROAD, 1, LIGHT, 22, FLAT, 2)),
PEOPLE_POKER_POINT("People Poker Point", TIP, ImmutableMap.of(NARROW, 4, HEAVY, 4, FLAT, 4)),
CHOPPER_TIP("Chopper Tip", TIP, ImmutableMap.of(BROAD, 4, LIGHT, 4, SPIKED, 4)),
MEDUSAS_HEAD("Medusa's Head", TIP, ImmutableMap.of(BROAD, 4, HEAVY, 4, SPIKED, 4)),
SERPENTS_FANG("Serpent's Fang", TIP, ImmutableMap.of(NARROW, 8, LIGHT, 6, SPIKED, 8)),
GLADIUS_POINT("Gladius Point", TIP, ImmutableMap.of(NARROW, 8, HEAVY, 8, FLAT, 6)),
SAW_TIP("Saw Tip", TIP, ImmutableMap.of(BROAD, 6, HEAVY, 8, SPIKED, 8)),
CORRUPTED_POINT("Corrupted Point", TIP, ImmutableMap.of(NARROW, 8, LIGHT, 10, SPIKED, 8)),
DEFENDERS_TIP("Defenders Tip", TIP, ImmutableMap.of(BROAD, 10, HEAVY, 8, SPIKED, 8)),
SERRATED_TIP("Serrated Tip", TIP, ImmutableMap.of(NARROW, 4, LIGHT, 16, SPIKED, 4)),
NEEDLE_POINT("Needle Point", TIP, ImmutableMap.of(NARROW, 18, LIGHT, 3, FLAT, 4)),
THE_POINT("The Point!", TIP, ImmutableMap.of(BROAD, 2, LIGHT, 1, FLAT, 22)),
;
private final String name;
private final MouldType mouldType;
private final Map<CommissionType, Integer> typeToScore;
public static final Mould[] values = Mould.values();
public static Mould forName(String text)
{
for (Mould mould : values)
{
if (mould.name.equalsIgnoreCase(text))
{
return mould;
}
}
return null;
}
public int getScore(CommissionType type1, CommissionType type2)
{
int score = 0;
score += typeToScore.getOrDefault(type1, 0);
score += typeToScore.getOrDefault(type2, 0);
return score;
}
}

View File

@@ -0,0 +1,8 @@
package ee.futur.easygiantsfoundry.enums;
public enum MouldType
{
FORTE,
BLADE,
TIP,
}

View File

@@ -0,0 +1,32 @@
package ee.futur.easygiantsfoundry.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum Stage
{
TRIP_HAMMER("Hammer", Heat.HIGH, 20, -25, 4, 14),
GRINDSTONE("Grind", Heat.MED, 10, 15, 7, 19),
POLISHING_WHEEL("Polish", Heat.LOW, 10, -17, 12, 10);
private final String name;
private final Heat heat;
private final int progressPerAction;
private final int heatChange;
private final int distanceToLava;
private final int distanceToWaterfall;
public boolean isHeating()
{
return heatChange > 0;
}
public boolean isCooling()
{
return heatChange < 0;
}
}