386 lines
7.8 KiB
Java
386 lines
7.8 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|
|
|