Files
k4rli-plugins/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay3D.java
riktenx 5cb7de46c6 text visibility enhancements
* add config for text outline, draw text over gameobject highlight
* add configurable dynamic overlay font
* space heat/action text according to font height
* draw text manually
* add configurable text background
2025-08-29 11:59:15 -04:00

589 lines
16 KiB
Java

package com.toofifty.easygiantsfoundry;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.*;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryHelper.getHeatColor;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryHelper.renderTextLocation;
import static com.toofifty.easygiantsfoundry.MouldHelper.SWORD_TYPE_1_VARBIT;
import static com.toofifty.easygiantsfoundry.MouldHelper.SWORD_TYPE_2_VARBIT;
import com.toofifty.easygiantsfoundry.enums.CommissionType;
import com.toofifty.easygiantsfoundry.enums.FontType;
import com.toofifty.easygiantsfoundry.enums.Heat;
import com.toofifty.easygiantsfoundry.enums.Stage;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
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())
{
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)
{
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)
{
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)
{
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 void drawStorage(Graphics2D graphics)
{
Shape shape = storage.getConvexHull();
if (shape != null)
{
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)
{
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());
}
}
}