diff --git a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryClientIDs.java b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryClientIDs.java
index 9420e33..537ae7e 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryClientIDs.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryClientIDs.java
@@ -23,6 +23,8 @@ public class EasyGiantsFoundryClientIDs
// 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;
diff --git a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java
index b5006eb..baf7ea2 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryConfig.java
@@ -1,7 +1,6 @@
package com.toofifty.easygiantsfoundry;
import java.awt.Color;
-
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@@ -247,6 +246,18 @@ public interface EasyGiantsFoundryConfig extends Config
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",
@@ -502,6 +513,34 @@ public interface EasyGiantsFoundryConfig extends Config
description = "Advanced Settings",
position = 5
)
- String generalSettings = "generalSettings";
+ String advancedSettings = "generalSettings";
+
+ @Range(
+ max = 50
+ )
+ @ConfigItem(
+ keyName = "heatActionBuffer", // renamed to reset player's settings for previous bugged implementation
+ name = "Lava/Waterfall Padding Ticks",
+ description = "Units in ticks; buffers more than optimal heat when in lava/waterfall calculations to compensate for heat decay when the player is afk or running/walking slower than optimal.",
+ position = 0,
+ section = advancedSettings
+ )
+ default int heatActionPadTicks()
+ {
+ return 4;
+ }
+
+ @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;
+ }
}
diff --git a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryPlugin.java b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryPlugin.java
index 2b7e404..3dcf978 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryPlugin.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/EasyGiantsFoundryPlugin.java
@@ -17,18 +17,9 @@ import net.runelite.api.GameState;
import net.runelite.api.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.GameObjectDespawned;
-import net.runelite.api.events.GameObjectSpawned;
-import net.runelite.api.events.GameStateChanged;
-import net.runelite.api.events.GameTick;
-import net.runelite.api.events.ItemContainerChanged;
-import net.runelite.api.events.MenuOptionClicked;
-import net.runelite.api.events.NpcDespawned;
-import net.runelite.api.events.NpcSpawned;
-import net.runelite.api.events.ScriptPostFired;
-import net.runelite.api.events.StatChanged;
-import net.runelite.api.events.VarbitChanged;
+import net.runelite.api.events.*;
import net.runelite.api.widgets.Widget;
import net.runelite.client.Notifier;
import net.runelite.client.callback.ClientThread;
@@ -58,6 +49,7 @@ public class EasyGiantsFoundryPlugin extends Plugin
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;
@@ -156,10 +148,47 @@ public class EasyGiantsFoundryPlugin extends Plugin
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)
{
@@ -202,36 +231,6 @@ public class EasyGiantsFoundryPlugin extends Plugin
}
}
- @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;
- }
- }
@Subscribe
public void onNpcSpawned(NpcSpawned event)
@@ -268,50 +267,74 @@ public class EasyGiantsFoundryPlugin extends Plugin
}
}
+ 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)
{
- if (!state.isEnabled()) return;
-
- if (event.getMenuTarget().contains("Crucible "))
+ clientThread.invokeAtTickEnd(() ->
{
- if (event.getMenuOption().equals("Pour"))
+ 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))
{
- 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
stages = new ArrayList<>();
private double heatRangeRatio = 0;
@@ -153,19 +153,19 @@ public class EasyGiantsFoundryState
int heat = getHeatAmount();
int[] low = getLowHeatRange();
- if (heat > low[0] && heat < low[1])
+ if (heat >= low[0] && heat <= low[1])
{
return Heat.LOW;
}
int[] med = getMedHeatRange();
- if (heat > med[0] && heat < med[1])
+ if (heat >= med[0] && heat <= med[1])
{
return Heat.MED;
}
int[] high = getHighHeatRange();
- if (heat > high[0] && heat < high[1])
+ if (heat >= high[0] && heat <= high[1])
{
return Heat.HIGH;
}
@@ -326,7 +326,7 @@ public class EasyGiantsFoundryState
int[] range = getCurrentHeatRange();
int actions = 0;
int heat = getHeatAmount();
- while (heat > range[0] && heat < range[1])
+ while (heat >= range[0] && heat <= range[1])
{
actions++;
heat += stage.getHeatChange();
@@ -335,5 +335,5 @@ public class EasyGiantsFoundryState
return actions;
}
- public HeatActionStateMachine heatingCoolingState = new HeatActionStateMachine();
+ public HeatActionStateMachine heatActionStateMachine = new HeatActionStateMachine();
}
diff --git a/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay3D.java b/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay3D.java
index d93101d..3f60076 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay3D.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/FoundryOverlay3D.java
@@ -1,7 +1,6 @@
package com.toofifty.easygiantsfoundry;
-import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.VARBIT_GAME_STAGE;
-import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.WIDGET_PROGRESS_PARENT;
+import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.*;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryHelper.getHeatColor;
import static com.toofifty.easygiantsfoundry.MouldHelper.SWORD_TYPE_1_VARBIT;
import static com.toofifty.easygiantsfoundry.MouldHelper.SWORD_TYPE_2_VARBIT;
@@ -17,6 +16,7 @@ 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;
@@ -32,6 +32,7 @@ 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;
@@ -41,6 +42,7 @@ public class FoundryOverlay3D extends Overlay
GameObject waterfall;
GameObject mouldJig;
GameObject crucible;
+ GameObject storage;
NPC kovac;
private final Client client;
@@ -110,6 +112,15 @@ public class FoundryOverlay3D extends Overlay
drawKovacIfHandIn(graphics);
}
+ if (client.getVarbitValue(VARBIT_PREFORM_STORED) == 1)
+ {
+ if (config.highlightStorage())
+ {
+ drawStorage(graphics);
+ }
+ return null;
+ }
+
if (state.getCurrentStage() == null)
{
if (config.highlightMould())
@@ -126,13 +137,12 @@ public class FoundryOverlay3D extends Overlay
drawPreformScoreIfPoured(graphics);
}
-
return null;
}
Stage stage = state.getCurrentStage();
GameObject stageObject = getStageObject(stage);
- if (stageObject == null)
+ if (stageObject == null || graphics == null)
{
return null;
}
@@ -151,20 +161,30 @@ public class FoundryOverlay3D extends Overlay
drawObjectOutline(graphics, stageObject, color);
}
- if ((stage.getHeat() != heat || !state.heatingCoolingState.isIdle()) && config.highlightWaterAndLava())
+ // !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);
}
-
- if (state.heatingCoolingState.isCooling())
+ // mouse hover over preview
+ else if (config.drawLavaWaterInfoOverlay())
{
- drawHeatingCoolingOverlay(graphics, waterfall);
- }
- if (state.heatingCoolingState.isHeating())
- {
- drawHeatingCoolingOverlay(graphics, lavaPool);
- }
+ 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);
+ }
+ }
+ }
return null;
}
@@ -195,26 +215,87 @@ public class FoundryOverlay3D extends Overlay
modelOutlineRenderer.drawOutline(stageObject, config.borderThickness(), _color, config.borderFeather());
}
- private void drawHeatingCoolingOverlay(
+ private void drawHeatChangerPreviewOverlay(
Graphics2D graphics,
- GameObject stageObject
+ GameObject stageObject,
+ boolean isLava
)
{
- if (!config.drawLavaWaterInfoOverlay())
- {
- return;
- }
- if (state.heatingCoolingState.isIdle())
- {
- return;
- }
+ int sign = isLava ? 1 : -1;
+ int fastVelocity = 27 * sign;
+ int slowVelocity = 7 * sign;
+ int fastAccelBonus = 2 * sign;
+ int slowAccelBonus = 0;
+
+ HeatActionSolver.DurationResult fastResult =
+ HeatActionSolver.solve(
+ state.getCurrentStage(),
+ state.getCurrentHeatRange(),
+ state.getActionsLeftInStage(),
+ state.getHeatAmount(),
+ true,
+ isLava,
+ config.heatActionPadTicks() * 2
+ );
+
+ final int fastDuration = fastResult.getDuration();
+ HeatActionSolver.DurationResult slowResult =
+ HeatActionSolver.solve(
+ state.getCurrentStage(),
+ state.getCurrentHeatRange(),
+ state.getActionsLeftInStage(),
+ state.getHeatAmount(),
+ false,
+ isLava,
+ config.heatActionPadTicks() * 2
+ );
+ final int slowDuration = slowResult.getDuration();
+
+ final String fastName = isLava ? "dunks" : "quenches";
+ final String slowName = isLava ? "heats" : "cools";
String text;
- text = String.format("%d %s",
- state.heatingCoolingState.getRemainingDuration(),
- state.heatingCoolingState.getActionName()
- );
+ 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);
+ Color color = config.lavaWaterfallColour();
+
+ OverlayUtil.renderTextLocation(graphics, pos, text, color);
+ }
+
+ 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());
@@ -230,11 +311,13 @@ public class FoundryOverlay3D extends Overlay
int change = state.getHeatChangeNeeded();
Shape shape = null;
- if (change < 0 || state.heatingCoolingState.isCooling())
+ boolean isLava = change > 0;
+ boolean isWaterfall = change < 0;
+ if (isWaterfall || state.heatActionStateMachine.isCooling())
{
shape = waterfall.getClickbox();
}
- else if (change > 0 || state.heatingCoolingState.isHeating())
+ else if (isLava || state.heatActionStateMachine.isHeating())
{
shape = lavaPool.getClickbox();
}
@@ -254,9 +337,28 @@ public class FoundryOverlay3D extends Overlay
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);
+ }
+ }
}
- static final int CRUCIBLE_CAPACITY = 28;
private void drawCrucibleContent(Graphics2D graphics)
{
@@ -411,6 +513,19 @@ public class FoundryOverlay3D extends Overlay
}
}
+ 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);
@@ -430,6 +545,8 @@ public class FoundryOverlay3D extends Overlay
private void drawActionOverlay(Graphics2D graphics, GameObject gameObject)
{
+
+
int actionsLeft = state.getActionsLeftInStage();
int heatLeft = state.getActionsForHeatLevel();
@@ -440,6 +557,10 @@ public class FoundryOverlay3D extends Overlay
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
+ if (canvasLocation == null)
+ {
+ return;
+ }
OverlayUtil.renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft));
}
if (config.drawActionLeftOverlay())
@@ -449,6 +570,10 @@ public class FoundryOverlay3D extends Overlay
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() + 10);
OverlayUtil.renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft));
}
diff --git a/src/main/java/com/toofifty/easygiantsfoundry/HeatActionSolver.java b/src/main/java/com/toofifty/easygiantsfoundry/HeatActionSolver.java
index e500b9d..6516e15 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/HeatActionSolver.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/HeatActionSolver.java
@@ -3,13 +3,16 @@ package com.toofifty.easygiantsfoundry;
//import java.util.ArrayList;
//import java.util.List;
+import com.toofifty.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.
*
* the dx_n refers to successive derivatives of an ordinary-differential-equations
* https://en.wikipedia.org/wiki/Ordinary_differential_equation
- * also known as distance (dx0), speed (dx1), and acceleration (dx2).
+ * also known as position (dx0), speed (dx1), and acceleration (dx2).
*
* dx0 - players current heat at tick
* dx1 - dx0_current - dx0_last_tick, aka the first derivative
@@ -18,7 +21,17 @@ package com.toofifty.easygiantsfoundry;
* 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 = {
- * 27,
+ * 7,
+ * 8,
+ * 9,
+ * 11,
+ * 13,
+ * 15,
+ * 17,
+ * 19,
+ * 21,
+ * 24,
+ * 27, -- dunk/quench starts here
* 30,
* 33,
* 37,
@@ -77,148 +90,255 @@ subscribe(VarbitChanged.class, ev ->
public class HeatActionSolver
{
- /**
- * Warning: this method prefers overshooting goal. For example, if goal is 957,
- * it will return index that reaches >957.
- * This may be desirable if we're aiming to heat over range minimum,
- * but undesirable when cooling below range maximum; make sure to -1 the index if so.
- *
- *
- *
- * @param goal the desired heat destination
- * @param init_dx1 initial speed of heating/cooling. currently 7 for heat/cool, 27 for dunk/quench.
- * @param dx2_offset bonus acceleration. currently, 0 for heat/cool, 2 for dunk/quench.
- * @return Index here refers to tick. So an index of 10 means the goal can be reached in 10 ticks.
- */
- public static int findDx0Index(int goal, int init_dx1, int dx2_offset)
+ 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
+ };
+ 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;
- int dx1 = init_dx1;
- int count_index = 0;
- for (int dx2 = 1; dx0 <= goal; dx2++)
- { // Start from 1 up to the count inclusive
- int repetitions;
- if (dx2 == 1)
+
+ boolean decay = false;
+
+ while (true) {
+
+ if (index > MAX_INDEX)
{
- repetitions = 2; // The first number appears twice
+ break;
}
- else if (dx2 % 2 == 0)
+
+ if (!overshoot && dx0 + DX_1[index] > goal)
{
- repetitions = 6; // Even numbers appear six times
+ break;
}
- else
+ else if (overshoot && dx0 >= goal)
{
- repetitions = 4; // Odd numbers (after 1) appear four times
+ break;
}
- for (int j = 0; j < repetitions && dx0 <= goal; j++)
+
+ if (dx0 + DX_1[index] >= max)
{
- dx0 += dx1;
- dx1 += dx2 + dx2_offset; // Sum the current number 'repetitions' times
- count_index += 1;
+ break;
}
+
+ if (decay)
+ {
+ dx0 -= decayValue;
+ }
+
+
+ dx0 += DX_1[index];
+ ++index;
+ decay = !decay;
}
- return count_index;
+
+ if (isFast)
+ {
+ index -= FAST_INDEX;
+ }
+
+ return SolveResult.of(index, dx0, DX_1[index], -1);
}
- /**
- * We can use the pattern to get the dx2 at a specific index numerically
- *
- * @param index the index/tick we want to calculate dx2 at
- * @return the acceleration of heating/cooling at index/tick
- */
- public static int getDx2AtIndex(int index)
+ @Value(staticConstructor = "of")
+ public static class DurationResult
{
- if (index <= 1) return 1;
-
- index -= 2;
- // 0 1 2 3 4 5 6 7 8 9
- // e,e,e,e,e,e,o,o,o,o
-
- int block = index / 10;
- int block_idx = index % 10;
- int number = block * 2;
- if (block_idx <= 5)
- {
- return number + 2;
- }
- else
- {
- return number + 3;
- }
+ int duration;
+ boolean goalInRange;
+ boolean overshooting;
+ int predictedHeat;
}
-
- /**
- * We can use the pattern to get the dx1 at a specific index numerically
- *
- * @param index the index/tick we want to calculate the speed of heating/cooling
- * @param constant the initial speed of heating/cooling.
- * @return the speed of heating at index/tick
- */
- public static int getDx1AtIndex(int index, int constant)
+ public static DurationResult solve(
+ Stage stage,
+ int[] range,
+ int actionLeftInStage,
+ int start,
+ boolean isFast,
+ boolean isActionHeating,
+ int padding)
{
- int _dx1 = constant;
- for (int i = 0; i < index; ++i)
+
+ final boolean isStageHeating = stage.isHeating();
+
+ // 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] + padding));
+ final int max = Math.max(0, Math.min(1000, range[1] + padding));
+
+ 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)
{
- _dx1 += getDx2AtIndex(i);
+ 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;
+ }
+ }
}
- return _dx1;
+ int dx0 = result == null ? 0 : result.dx0;
+ if (!isActionHeating)
+ {
+ dx0 *= -1;
+ }
+
+
+ return DurationResult.of(estimatedDuration, goalInRange, overshoot, start + dx0);
}
-// Methods below are functional, but only used to for debugging & development
-
-// public static int getDx0AtIndex(int index, int constant)
-// {
-// int dx0 = 0;
-// int dx1 = getDx1AtIndex(0, constant);
-// for (int i = 0; i < index; i++)
-// { // Start from 1 up to the count inclusive
-// int dx2 = getDx2AtIndex(i);
-// dx1 += dx2; // Sum the current number 'repetitions' times
-// dx0 += dx1;
-// }
-// return dx0;
-// }
-
- // We iteratively generate dx2 into a list
-// public static List generateDx2List(int count)
-// {
-// List pattern = new ArrayList<>(); // This will hold our pattern
-// for (int n = 1, i = 0; i < count; n++)
-// { // Start from 1 up to the count inclusive
-// int repetitions;
-// if (n == 1)
-// {
-// repetitions = 2; // The first number appears twice
-// } else if (n % 2 == 0)
-// {
-// repetitions = 6; // Even numbers appear six times
-// } else
-// {
-// repetitions = 4; // Odd numbers (after 1) appear four times
-// }
-// for (int j = 0; j < repetitions && i < count; j++, i++)
-// {
-// pattern.add(n); // Append the current number 'repetitions' times
-// }
-// }
-// return pattern;
-// }
-
-// public static int findDx0IndexContinue(int goal, int constant, int init_index)
-// {
-// int dx0 = getDx0AtIndex(init_index, constant);
-// int dx1 = getDx1AtIndex(init_index, constant);
-// int count_index = init_index;
-// for (; dx0 <= goal; count_index++)
-// { // Start from 1 up to the count inclusive
-// int dx2 = getDx2AtIndex(count_index);
-// dx1 += dx2; // Sum the current number 'repetitions' times
-// dx0 += dx1;
-// }
-// return count_index - init_index;
-// }
}
diff --git a/src/main/java/com/toofifty/easygiantsfoundry/HeatActionStateMachine.java b/src/main/java/com/toofifty/easygiantsfoundry/HeatActionStateMachine.java
index 00e0da6..c0c3d19 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/HeatActionStateMachine.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/HeatActionStateMachine.java
@@ -1,6 +1,5 @@
package com.toofifty.easygiantsfoundry;
-import com.toofifty.easygiantsfoundry.enums.Stage;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -14,46 +13,45 @@ public class HeatActionStateMachine
/**
* Tick counter for heating, -1 means not currently heating.
*/
- int HeatingTicks = -1;
+ int heatingTicks = -1;
/**
* Tick counter for cooling, -1 means not currently cooling.
*/
- int CoolingTicks = -1;
+ int coolingTicks = -1;
- /**
- * The velocity of the heating/cooling action.
- */
- int Velocity;
+ boolean actionFast;
- /**
- * The acceleration bonus of the heating/cooling action.
- */
- int AccelerationBonus;
+ boolean actionHeating;
/**
* The starting heat amount of the heating/cooling action.
*/
- int StartingHeat;
+ int startingHeat;
/**
* The estimated tick duration of the heating/cooling action.
*/
- int EstimatedDuration;
+ int estimatedDuration;
/**
* The goal heat amount of the heating/cooling action.
*/
- int GoalHeat = 0;
+ 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;
+ String actionname = null;
- private EasyGiantsFoundryState State;
- private EasyGiantsFoundryConfig Config;
+ private EasyGiantsFoundryState state;
+ private EasyGiantsFoundryConfig config;
/**
* Start the state-machine with the given parameters.
@@ -64,26 +62,27 @@ public class HeatActionStateMachine
* @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(int, int, String)
+ * @see HeatActionStateMachine#setup(boolean, boolean, String)
*/
public void start(EasyGiantsFoundryState state, EasyGiantsFoundryConfig config, int startingHeat)
{
// use Velocity to determine if heating or cooling
- if (Velocity > 0)
+ if (actionHeating)
{
- HeatingTicks = 0;
- CoolingTicks = -1;
+ heatingTicks = 0;
+ coolingTicks = -1;
}
else
{
- CoolingTicks = 0;
- HeatingTicks = -1;
+ heatingTicks = -1;
+ coolingTicks = 0;
}
- StartingHeat = startingHeat - Velocity;
- State = state;
- Config = config;
- calculateEstimates();
+ this.startingHeat = startingHeat;
+ this.state = state;
+ this.config = config;
+
+ updateEstimates();
}
/**
@@ -95,11 +94,11 @@ public class HeatActionStateMachine
{
if (isHeating())
{
- return Math.max(0, EstimatedDuration - HeatingTicks);
+ return Math.max(0, (estimatedDuration - heatingTicks));
}
else if (isCooling())
{
- return Math.max(0, EstimatedDuration - CoolingTicks);
+ return Math.max(0, (estimatedDuration - coolingTicks));
}
else
{
@@ -111,125 +110,38 @@ public class HeatActionStateMachine
* 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 calculateEstimates()
+ public void updateEstimates()
{
- // 0: left/min 1: right/max
- int[] range = State.getCurrentHeatRange();
- int stageMin = range[0];
- int stageMax = range[1];
- Stage stage = State.getCurrentStage();
- int actionsLeft = State.getActionsLeftInStage();
- int actionsLeft_DeltaHeat = (actionsLeft+1) * stage.getHeatChange();
- if (isHeating())
- {
- if (stage.isHeating())
- {
- GoalHeat = Math.max(stageMin, stageMax - actionsLeft_DeltaHeat);
- if (StartingHeat < GoalHeat)
- {
- int duration = HeatActionSolver.findDx0Index(
- GoalHeat - StartingHeat,
- Velocity, AccelerationBonus
- );
+ HeatActionSolver.DurationResult result =
+ HeatActionSolver.solve(
+ getState().getCurrentStage(),
+ getState().getCurrentHeatRange(),
+ getState().getActionsLeftInStage(),
+ getStartingHeat(),
+ actionFast,
+ isHeating(),
+ config.heatActionPadTicks() * 2
+ );
- GoalHeat += duration / 2;
+ goalInRange = result.isGoalInRange();
+ isOverShooting = result.isOvershooting();
- EstimatedDuration = HeatActionSolver.findDx0Index(
- GoalHeat - StartingHeat,
- Velocity, AccelerationBonus
- );
- }
- else // overheating
- {
- EstimatedDuration = 0;
- }
- }
- else // is cooling
- {
- // actionsLeft_DeltaHeat is negative here
- GoalHeat = Math.min(stageMax, stageMin - actionsLeft_DeltaHeat);
- if (StartingHeat < GoalHeat)
- {
- int duration = HeatActionSolver.findDx0Index(
- GoalHeat - StartingHeat,
- Velocity, AccelerationBonus
- ) - 1;
+ predictedHeat = result.getPredictedHeat();
- GoalHeat -= duration / 2;
-
- EstimatedDuration = HeatActionSolver.findDx0Index(
- GoalHeat - StartingHeat,
- Velocity, AccelerationBonus
- ) - 1;
- }
- else // cold enough
- {
- EstimatedDuration = 0;
- }
- }
- }
- else if (isCooling())
- {
- if (stage.isHeating()) {
- GoalHeat = Math.max(stageMin, stageMax - actionsLeft_DeltaHeat);
- if (StartingHeat > GoalHeat)
- {
- int duration = HeatActionSolver.findDx0Index(
- StartingHeat - GoalHeat,
- Math.abs(Velocity), Math.abs(AccelerationBonus)
- ) - 1;
-
- GoalHeat += duration / 2;
-
- EstimatedDuration = HeatActionSolver.findDx0Index(
- (StartingHeat - GoalHeat),
- Math.abs(Velocity), Math.abs(AccelerationBonus)
- ) - 1;
- }
- else
- {
- EstimatedDuration = 0;
- }
- }
- // Heating Stage
- else {
- GoalHeat = Math.max(stageMax, stageMin + actionsLeft_DeltaHeat);
- if (StartingHeat > GoalHeat) // too hot
- {
- int duration = HeatActionSolver.findDx0Index(
- StartingHeat - GoalHeat,
- Math.abs(Velocity), Math.abs(AccelerationBonus)
- );
-
- GoalHeat -= duration / 2;
-
- EstimatedDuration = HeatActionSolver.findDx0Index(
- StartingHeat - GoalHeat,
- Math.abs(Velocity), Math.abs(AccelerationBonus)
- );
- }
- else // hot enough
- {
- EstimatedDuration = 0;
- }
- }
-
- }
+ estimatedDuration = result.getDuration();
}
/**
* Helper to remind the neccessary parameters to start the state-machine.
*
- * @param velocity the velocity of the heating/cooling action, 7 for slow, 27 for fast.
- * @param accelerationBonus the acceleration bonus of the heating/cooling action. Usually 0 for slow, 2 for fast.
* @param actionName the name of the action to display in the ui overlay
*/
- public void setup(int velocity, int accelerationBonus, String actionName)
+ public void setup(boolean isFast, boolean isHeating, String actionName)
{
- Velocity = velocity;
- AccelerationBonus = accelerationBonus;
- ActionName = actionName;
+ actionFast = isFast;
+ actionHeating = isHeating;
+ actionname = actionName;
}
/**
@@ -237,9 +149,9 @@ public class HeatActionStateMachine
*/
public void stop()
{
- HeatingTicks = -1;
- CoolingTicks = -1;
- ActionName = null;
+ heatingTicks = -1;
+ coolingTicks = -1;
+ actionname = null;
}
/**
@@ -249,7 +161,7 @@ public class HeatActionStateMachine
*/
public boolean isHeating()
{
- return HeatingTicks >= 0;
+ return heatingTicks >= 0;
}
/**
@@ -259,7 +171,7 @@ public class HeatActionStateMachine
*/
public boolean isCooling()
{
- return CoolingTicks >= 0;
+ return coolingTicks >= 0;
}
/**
@@ -277,25 +189,30 @@ public class HeatActionStateMachine
*/
public void onTick()
{
+ if (isIdle()) return;
+
if (isHeating())
{
- HeatingTicks++;
- if (HeatingTicks >= EstimatedDuration)
+ if (heatingTicks >= estimatedDuration)
{
stop();
}
+ else
+ {
+ heatingTicks++;
+ }
}
- if (isCooling())
+ else if (isCooling())
{
- CoolingTicks++;
- if (CoolingTicks >= EstimatedDuration)
+ if (coolingTicks >= estimatedDuration)
{
stop();
}
+ else
+ {
+ coolingTicks++;
+ }
}
-// log.info("\nReal Heat: " + State.getHeatAmount()
-// + "\nGoal Heat - StartingHeat: " + (GoalHeat - StartingHeat)
-// + "\nDuration: " + EstimatedDuration);
}
}
diff --git a/src/main/java/com/toofifty/easygiantsfoundry/MouldHelper.java b/src/main/java/com/toofifty/easygiantsfoundry/MouldHelper.java
index 5b14081..dad7e3e 100644
--- a/src/main/java/com/toofifty/easygiantsfoundry/MouldHelper.java
+++ b/src/main/java/com/toofifty/easygiantsfoundry/MouldHelper.java
@@ -114,7 +114,7 @@ public class MouldHelper
int height = scrollList.getHeight();
int scrollMax = scrollList.getScrollHeight();
Widget finalBestWidget = bestWidget;
- clientThread.invokeLater(() ->
+ clientThread.invokeAtTickEnd(() ->
{
if (finalBestWidget != null)
{
diff --git a/src/test/java/com/toofifty/easygiantsfoundry/HeatSolverTest.java b/src/test/java/com/toofifty/easygiantsfoundry/HeatSolverTest.java
deleted file mode 100644
index f5fb53b..0000000
--- a/src/test/java/com/toofifty/easygiantsfoundry/HeatSolverTest.java
+++ /dev/null
@@ -1,180 +0,0 @@
-package com.toofifty.easygiantsfoundry;
-
-import static org.junit.Assert.*;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-// playground to test HeatSolver
-public class HeatSolverTest
-{
-// @Test
-// public void TestHeatSolver_dx2_Iterative()
-// {
-// final int[] answer =
-// {1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6};
-// List produced = HeatActionSolver.generateDx2List(answer.length);
-//
-// System.err.println("Expected Length: " + answer.length + " Length: " + produced.size());
-// // print produced
-// for (Integer integer : produced)
-// {
-// System.err.print(integer + ",");
-// }
-// System.err.println();
-// // compare
-// for (int i = 0; i < answer.length; i++)
-// {
-// assertEquals("Asserting dx2 n=" + i, answer[i], produced.get(i).intValue());
-// }
-// }
-
- @Test
- public void TestHeatSolver_dx2_Numerical()
- {
- final int[] answer =
- {1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6};
- // test getDx2AtIndex
- for (int i = 0; i < answer.length; i++)
- {
- assertEquals("Asserting dx2 n=" + i, answer[i], HeatActionSolver.getDx2AtIndex(i));
- }
- }
-
- @Test
- public void TestHeatSolver_dx1()
- {
- final int c = 7;
- final int[] answer =
-// {1,1,2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6};
- {7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
- for (int i = 0; i < answer.length; i++)
- {
- assertEquals("Asserting dx1 n=" + i + " c=" + c + " answer=" + answer[i], answer[i], HeatActionSolver.getDx1AtIndex(i, c));
- }
- }
-
- @Test
- public void TestHeatSolver_dx2_from_groundtruth()
- {
- // runelite-shell script for retrieving heating/cooling delta.
- // copy-paste into developer-tools -> Shell
-
-
- // ground-truth answer from game
- final int[] answer =
- {7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
- for (int i = 0; i < answer.length - 1; i++)
- {
- System.err.print(answer[i + 1] - answer[i] + ",");
- }
- }
-
- @Test
- public void TestHeatSolver_Dx0()
- {
- final int[] answer_dx1 =
- {7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
- List answer_dx0 = new ArrayList<>();
-
-
- int sum = 0;
- for (int i = 0; i < answer_dx1.length; i++)
- {
- sum += answer_dx1[i];
- answer_dx0.add(sum);
- }
-
- System.err.println(answer_dx0);
-
- for (int i = 0; i < answer_dx1.length; i++)
- {
- TestHeatSolver_Dx0_Helper(answer_dx0.get(i), answer_dx0.get(0), i);
- }
- }
-
- @Test
- public void TestHeatSolver_Dx0_Manual()
- {
- for (int i = 0; i < 50; i++)
- {
- System.err.println("[" + (350 + i) + "]" + HeatActionSolver.findDx0Index(350 + i, 7, 0));
- }
- }
-
- @Test
- public void TestHeatSolver_Dx0_2()
- {
-// 7->1,15->2,24->3,35->4,48->5,63->6,80->7,99->8,120->9,144->10,171->11,201->12,234->13,271->14,312->15,357->16,406->17,459->18,516->19,578->20,645->21,717->22,794->23,877->24,966->25
- final int[] answer_dx1 =
- {7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
- List answer_dx0 = new ArrayList<>();
-
-
- int sum = 0;
- for (int i = 0; i < answer_dx1.length; i++)
- {
- sum += answer_dx1[i];
- answer_dx0.add(sum);
- }
-
- System.err.println(answer_dx0);
-
-// System.err.println(
-// HeatSolver.findDx0IndexContinue(406, 7, 0));
-// System.err.println(
-// HeatSolver.findDx0IndexContinue(406, 7, 10));
-// System.err.println(
-// HeatSolver.findDx0IndexContinue(406, 7, 17));
-//
-// System.err.println(
-// HeatSolver.findDx0IndexContinue(1000, 7, 0));
- System.err.println(
- HeatActionSolver.findDx0Index(957, 27, 2));
-// System.err.println(
-// HeatActionSolver.findDx0Index(1000, 7, 1));
- }
-
- public void TestHeatSolver_Dx0_Helper(int dx0, int constant, int answer_index)
- {
- System.err.print(dx0 + "->" + HeatActionSolver.findDx0Index(dx0, constant, 0) + ",");
-
- // test calcDx0Index
- assertEquals("Asserting dx0 for index " + answer_index,
- answer_index, HeatActionSolver.findDx0Index(dx0, constant, 0));
- }
-
- @Test
- public void Calc()
- {
- int[] dx1 = {
- 27,
- 30,
- 33,
- 37,
- 41,
- 45,
- 49,
- 53,
- 57,
- 62,
- 67,
- 72,
- 77,
- 83,
- 89,
- 95,
- 91,
- };
-
- List dx2 = new ArrayList<>();
- for (int i = 0; i < dx1.length - 1; i++)
- {
- dx2.add(dx1[i+1] - dx1[i]);
- }
-
- System.err.println(dx2);
- }
-
-}
\ No newline at end of file