From fa9e6b7663c6f81e08e6e3cc1cf25907522ae82a Mon Sep 17 00:00:00 2001 From: olim88 Date: Mon, 3 Mar 2025 04:10:02 +0000 Subject: Fossil Solver (#1156) * nearly working nearly working state however lots of the code will need refactoring once i understand what it needs to do * fix broken code still needs refactor but works now * code should work now needs a refactor next and a few small feature * add usefull infomation to tooltips * big refactor * add config option * fix bugs * improve test and tweak fossil types * add more tooltip infomation * add translations for tooltips * fix rebase * clean up code hopefully good code now. ready to finally make a pr * Fix config location * Fix static usage * Clean up names --------- Co-authored-by: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> --- .../config/categories/MiningCategory.java | 8 + .../skyblocker/config/configs/MiningConfig.java | 3 + .../dwarven/fossil/FossilCalculations.java | 242 +++++++++++++++++++++ .../skyblock/dwarven/fossil/FossilSolver.java | 171 +++++++++++++++ .../skyblock/dwarven/fossil/FossilTypes.java | 74 +++++++ .../skyblock/dwarven/fossil/Structures.java | 145 ++++++++++++ .../skyblock/item/tooltip/TooltipManager.java | 2 + .../utils/container/ContainerSolverManager.java | 4 +- 8 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilCalculations.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilSolver.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilTypes.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/Structures.java (limited to 'src/main/java') diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java index 4b7d198d..fa3294c6 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java @@ -274,6 +274,14 @@ public class MiningCategory { newValue -> config.mining.glacite.coldOverlay = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.mining.glacite.fossilSolver")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.glacite.fossilSolver.@Tooltip"))) + .binding(defaults.mining.glacite.fossilSolver, + () -> config.mining.glacite.fossilSolver, + newValue -> config.mining.glacite.fossilSolver = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .option(Option.createBuilder() .name(Text.translatable("skyblocker.config.mining.glacite.enableCorpseFinder")) .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.glacite.enableCorpseFinder.@Tooltip"))) diff --git a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java index 33ed1b05..671b777a 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java @@ -165,6 +165,9 @@ public class MiningConfig { @SerialEntry public boolean coldOverlay = true; + @SerialEntry + public boolean fossilSolver = true; + @SerialEntry public boolean enableCorpseFinder = true; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilCalculations.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilCalculations.java new file mode 100644 index 00000000..14ca38c3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilCalculations.java @@ -0,0 +1,242 @@ +package de.hysky.skyblocker.skyblock.dwarven.fossil; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class FossilCalculations { + private static final List POSSIBLE_STATES = getAllPossibleStates(); + private static final int EXCAVATOR_WIDTH = 9; + private static final int EXCAVATOR_HEIGHT = 6; + + + /** + * The number of still possible fossil permutations from last time {@link FossilCalculations#getFossilChance} was invoked + */ + protected static int permutations = -1; + /** + * The least amount of clicks needed to uncover a fossil from last time {@link FossilCalculations#getFossilChance} was invoked + */ + protected static int minimumTiles; + + protected static String fossilName; + + /** + * Returns an array of how likely a slot is to contain a fossil assuming one has to exist + * + * @param tiles the state of the excavator window + * @return the probability of a fossil being in a tile + */ + + protected static double[] getFossilChance(Structures.TileGrid tiles, String percentage) { + int[] total = new int[EXCAVATOR_WIDTH * EXCAVATOR_HEIGHT]; + minimumTiles = EXCAVATOR_WIDTH * EXCAVATOR_HEIGHT; + AtomicInteger fossilCount = new AtomicInteger(); + Arrays.stream(tiles.state()).forEach(row -> Arrays.stream(row).forEach(tile -> {if (tile.equals(Structures.TileState.FOSSIL)) fossilCount.getAndIncrement();})); + + //loop though tile options and if they are valid + List validStates = new ArrayList<>(); + for (Structures.permutation state : POSSIBLE_STATES) { + if (state.isValid(tiles, percentage)) { + validStates.add(state); + //update minimum left if it's smaller than current value + int min = state.type().tileCount - fossilCount.get(); + if (min < minimumTiles) { + minimumTiles = min; + } + } + } + //update permutations value + permutations = validStates.size(); + + //update fossil name value + if (validStates.isEmpty()) { + fossilName = null; + } else { + //assume there is only one type of fossil and set name to value of first fossil + fossilName = validStates.getFirst().type().name; + for (Structures.permutation fossil : validStates) { + //if there is more than one type of fossil reset name to null + if (!fossil.type().name.equals(fossilName)) { + fossilName = null; + break; + } + } + } + + //from all the valid states work out the chance of each tile being a fossil + int index = 0; + for (int y = 0; y < EXCAVATOR_HEIGHT; y++) { + for (int x = 0; x < EXCAVATOR_WIDTH; x++) { + if (tiles.getSlot(x, y) == Structures.TileState.UNKNOWN) { + for (Structures.permutation state : validStates) { + if (state.isFossilCollision(x, y)) { + total[index] += 1; + } + } + } + index++; + } + } + + + return Arrays.stream(total).mapToDouble(x -> (double) x / validStates.size()).toArray(); + } + + /** + * Converts a dictionary of item stacks to{@link Structures.TileGrid}. assuming each row will be 9 tiles and there will be 6 rows + * + * @param currentState dictionary of item in container + * @return input container converted into 2d {@link Structures.TileState} array + */ + protected static Structures.TileGrid convertItemsToTiles(Int2ObjectMap currentState) { + Structures.TileGrid output = new Structures.TileGrid(new Structures.TileState[EXCAVATOR_HEIGHT][EXCAVATOR_WIDTH]); + //go through each slot and work out its state + int index = 0; + for (int y = 0; y < EXCAVATOR_HEIGHT; y++) { + for (int x = 0; x < EXCAVATOR_WIDTH; x++) { + Item item = currentState.get(index).getItem(); + if (item == Items.WHITE_STAINED_GLASS_PANE) { + output.updateSlot(x, y, Structures.TileState.FOSSIL); + } else if (item == Items.BROWN_STAINED_GLASS_PANE) { + output.updateSlot(x, y, Structures.TileState.UNKNOWN); + } else { + output.updateSlot(x, y, Structures.TileState.EMPTY); + } + index++; + } + } + return output; + } + + /** + * Finds all possible fossil combinations and creates a list to return + * + * @return list of all possible fossil arrangements + */ + protected static List getAllPossibleStates() { + List output = new ArrayList<>(); + //loop though each fossil type and for each possible rotation add valid offset of add to output list + + //loop through fossils + for (FossilTypes fossil : FossilTypes.values()) { + //loop though rotations + for (Structures.TransformationOptions rotation : fossil.rotations) { + //get the rotated grid of the fossil + Structures.TileGrid grid = transformGrid(new Structures.TileGrid(fossil.grid), rotation); + //get possible offsets for the grid based on width and height + int maxXOffset = EXCAVATOR_WIDTH - grid.width(); + int maxYOffset = EXCAVATOR_HEIGHT - grid.height(); + //loop though possible offsets and for each of them create a screen state and return the value + for (int x = 0; x <= maxXOffset; x++) { + for (int y = 0; y <= maxYOffset; y++) { + output.add(new Structures.permutation(fossil, grid, x, y)); + } + } + } + } + return output; + } + + /** + * Transforms a grid for each of the options in {@link Structures.TransformationOptions} + * + * @param grid input grid + * @param transformation transformation to perform on gird + * @return transformed grid + */ + private static Structures.TileGrid transformGrid(Structures.TileGrid grid, Structures.TransformationOptions transformation) { + switch (transformation) { + case ROTATED_90 -> { + return rotateGrid(grid, 90); + } + case ROTATED_180 -> { + return rotateGrid(grid, 180); + } + case ROTATED_270 -> { + return rotateGrid(grid, 270); + } + case FLIP_ROTATED_0 -> { + return flipGrid(grid); + } + case FLIP_ROTATED_90 -> { + return rotateGrid(flipGrid(grid), 90); + } + case FLIP_ROTATED_180 -> { + return rotateGrid(flipGrid(grid), 180); + } + case FLIP_ROTATED_270 -> { + return rotateGrid(flipGrid(grid), 270); + } + default -> { + return grid; + } + } + } + + /** + * Flips the grid along the vertical axis + * + * @param grid input grid + * @return flipped grid + */ + private static Structures.TileGrid flipGrid(Structures.TileGrid grid) { + Structures.TileGrid output = new Structures.TileGrid(new Structures.TileState[grid.height()][grid.width()]); + for (int x = 0; x < grid.width(); x++) { + for (int y = 0; y < grid.height(); y++) { + output.updateSlot(x, y, grid.getSlot(x, grid.height() - 1 - y)); + } + } + return output; + } + + /** + * Applies a rotation to a grid + * + * @param grid input grid + * @param rotation rotation amount in degrees + * @return rotated grid + */ + private static Structures.TileGrid rotateGrid(Structures.TileGrid grid, int rotation) { + int startingWidth = grid.width() - 1; + int startingHeight = grid.height() - 1; + switch (rotation) { + case 90 -> { + Structures.TileGrid output = new Structures.TileGrid(new Structures.TileState[grid.height()][grid.width()]); + for (int x = 0; x < grid.width(); x++) { + for (int y = 0; y < grid.height(); y++) { + output.updateSlot(startingWidth - x, y, grid.getSlot(x, y)); + } + } + return output; + } + case 180 -> { + Structures.TileGrid output = new Structures.TileGrid(new Structures.TileState[grid.height()][grid.width()]); + for (int x = 0; x < grid.width(); x++) { + for (int y = 0; y < grid.height(); y++) { + output.updateSlot(startingWidth - x, startingHeight - y, grid.getSlot(x, y)); + } + } + return output; + } + case 270 -> { + Structures.TileGrid output = new Structures.TileGrid(new Structures.TileState[grid.height()][grid.width()]); + for (int x = 0; x < grid.width(); x++) { + for (int y = 0; y < grid.height(); y++) { + output.updateSlot(x, startingHeight - y, grid.getSlot(x, y)); + } + } + return output; + } + default -> { + return grid; + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilSolver.java new file mode 100644 index 00000000..6951bf0f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilSolver.java @@ -0,0 +1,171 @@ +package de.hysky.skyblocker.skyblock.dwarven.fossil; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dwarven.fossil.Structures.TileGrid; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.LineSmoothener; +import de.hysky.skyblocker.utils.container.SimpleContainerSolver; +import de.hysky.skyblocker.utils.container.TooltipAdder; +import de.hysky.skyblocker.utils.render.gui.ColorHighlight; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalDouble; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static de.hysky.skyblocker.skyblock.dwarven.fossil.FossilCalculations.*; + +public class FossilSolver extends SimpleContainerSolver implements TooltipAdder { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final Pattern PERCENTAGE_PATTERN = Pattern.compile("Fossil Excavation Progress: (\\d{1,2}.\\d)%"); + private static final Pattern CHARGES_PATTERN = Pattern.compile("Chisel Charges Remaining: (\\d{1,2})"); + + private static String percentage; + private static double[] probability; + private static int chiselLeft = -1; + + public FossilSolver() { + super("Fossil Excavator"); + } + + @Override + public List getColors(Int2ObjectMap slots) { + //convert to container + TileGrid mainTileGrid = convertItemsToTiles(slots); + //get how many chisels the player has left + if (!slots.isEmpty()) { + chiselLeft = getChiselLeft(slots.values().stream().findAny().get()); + } else { + chiselLeft = -1; + } + //get the fossil chance percentage + if (percentage == null) { + percentage = getFossilPercentage(slots); + } + //get chance for each + probability = getFossilChance(mainTileGrid, percentage); + //get the highlight amount and return + return convertChanceToColor(probability); + } + + /** + * Checks the tooltip of an item to see how many chisel uses a player has left + * + * @param itemStack item to use to check the tooltip + * @return how many chisels are left and "-1" if not found + */ + private int getChiselLeft(ItemStack itemStack) { + for (Text line : itemStack.getTooltip(Item.TooltipContext.DEFAULT, CLIENT.player, TooltipType.BASIC)) { + Matcher matcher = CHARGES_PATTERN.matcher(line.getString()); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + } + return -1; + } + + /** + * See if there is any found fossils then see if there is a fossil chance percentage in the tooltip + * + * @param slots items to check tooltip of + * @return null if there is none or the value of the percentage + */ + private String getFossilPercentage(Int2ObjectMap slots) { + for (ItemStack item : slots.values()) { + for (Text line : item.getTooltip(Item.TooltipContext.DEFAULT, CLIENT.player, TooltipType.BASIC)) { + Matcher matcher = PERCENTAGE_PATTERN.matcher(line.getString()); + if (matcher.matches()) { + return matcher.group(1); + } + } + } + return null; + } + + @Override + public boolean isEnabled() { + return SkyblockerConfigManager.get().mining.glacite.fossilSolver; + } + + /** + * Converts tile probability into a color for each of the remaining tiles + * @param chances the probability in order from 0-1 + * @return the colors formated for the {@link SimpleContainerSolver#getColors(Int2ObjectMap)} + */ + private static List convertChanceToColor(double[] chances) { + List outputColors = new ArrayList<>(); + Color gradientColor = Color.BLUE; + //loop though all the chance values and set the color to match probability. full color means that its 100% + OptionalDouble highProbability = Arrays.stream(chances).max(); + for (int i = 0; i < chances.length; i++) { + double chance = chances[i]; + if (Double.isNaN(chances[i]) || chances[i] == 0) { + continue; + } + if (chances[i] == highProbability.getAsDouble()) { + outputColors.add(ColorHighlight.green(i)); + continue; + } + outputColors.add(new ColorHighlight(i, (int) (chance * 255) << 24 | gradientColor.getRed() << 16 | gradientColor.getGreen() << 8 | gradientColor.getBlue())); + } + return outputColors; + } + + /** + * Add solver info to tooltips + * + * @param focusedSlot the slot focused by the player + * @param stack unused + * @param lines the lines for the tooltip + */ + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List lines) { //todo translatable + //only add if fossil or dirt + if (stack.getItem() != Items.GRAY_STAINED_GLASS_PANE && stack.getItem() != Items.BROWN_STAINED_GLASS_PANE) { + return; + } + //add spacer + lines.add(LineSmoothener.createSmoothLine()); + + //if no permutation say this instead of other stats + if (permutations == 0) { + lines.add(Text.translatable("skyblocker.config.mining.glacite.fossilSolver.toolTip.noFossilFound").formatted(Formatting.GOLD)); + return; + } + + //add permutation count + lines.add(Text.translatable("skyblocker.config.mining.glacite.fossilSolver.toolTip.possiblePatterns").append(Text.literal(String.valueOf(permutations)).formatted(Formatting.YELLOW))); + //add minimum tiles left count + lines.add(Text.translatable("skyblocker.config.mining.glacite.fossilSolver.toolTip.minimumTilesLeft").append(Text.literal(String.valueOf(minimumTiles)).formatted(chiselLeft >= minimumTiles ? Formatting.YELLOW : Formatting.RED))); + //add probability if available and not uncovered + if (focusedSlot != null && probability != null && probability.length > focusedSlot.getIndex() && stack.getItem() == Items.BROWN_STAINED_GLASS_PANE) { + lines.add(Text.translatable("skyblocker.config.mining.glacite.fossilSolver.toolTip.probability").append(Text.literal(Math.round(probability[focusedSlot.getIndex()] * 100) + "%").formatted(Formatting.YELLOW))); + } + //if only 1 type of fossil left and a fossil is partially uncovered add the fossil name + if (fossilName != null && percentage != null) { + lines.add(Text.translatable("skyblocker.config.mining.glacite.fossilSolver.toolTip.foundFossil").append(Text.literal(fossilName).formatted(Formatting.YELLOW))); + } + } + + @Override + public int getPriority() { + return 0; + } + + +} + + + diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilTypes.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilTypes.java new file mode 100644 index 00000000..4e3d1b1d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/FossilTypes.java @@ -0,0 +1,74 @@ +package de.hysky.skyblocker.skyblock.dwarven.fossil; + +import java.util.List; + +/** + * All the possible fossils and there transformations + */ +public enum FossilTypes { + CLAW(new Structures.TileState[][]{ + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY}, + {Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.FLIP_ROTATED_90, Structures.TransformationOptions.FLIP_ROTATED_180), "7.7", 13, "Claw"), + TUSK(new Structures.TileState[][]{ + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_90, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.FLIP_ROTATED_270), "12.5", 8, "Tusk"), + UGLY(new Structures.TileState[][]{ + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_90, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.ROTATED_270), "6.2", 16, "Ugly"), + HELIX(new Structures.TileState[][]{ + {Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_90, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.ROTATED_270), "7.1", 14, "Helix"), + WEBBED(new Structures.TileState[][]{ + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.FLIP_ROTATED_0), "10", 10, "Webbed"), + FOOTPRINT(new Structures.TileState[][]{ + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_90, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.ROTATED_270), "7.7", 13, "Footprint"), + CLUBBED(new Structures.TileState[][]{ + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL}, + {Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.EMPTY} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.FLIP_ROTATED_0, Structures.TransformationOptions.FLIP_ROTATED_180), "9.1", 11, "Clubbed"), + SPINE(new Structures.TileState[][]{ + {Structures.TileState.EMPTY, Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY, Structures.TileState.EMPTY}, + {Structures.TileState.EMPTY, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.EMPTY}, + {Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL, Structures.TileState.FOSSIL} + }, List.of(Structures.TransformationOptions.ROTATED_0, Structures.TransformationOptions.ROTATED_90, Structures.TransformationOptions.ROTATED_180, Structures.TransformationOptions.ROTATED_270), "8.3", 12, "Spine"); + + final List rotations; + final Structures.TileState[][] grid; + final String percentage; + final int tileCount; + final String name; + + + FossilTypes(Structures.TileState[][] grid, List rotations, String percentage, int tileCount, String name) { + this.grid = grid; + this.rotations = rotations; + this.percentage = percentage; + this.tileCount = tileCount; + this.name = name; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/Structures.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/Structures.java new file mode 100644 index 00000000..3e7af14f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/fossil/Structures.java @@ -0,0 +1,145 @@ +package de.hysky.skyblocker.skyblock.dwarven.fossil; + +public class Structures { + /** + * The three possible states a tile could be in + */ + protected enum TileState { + UNKNOWN, + EMPTY, + FOSSIL + } + + protected enum TransformationOptions { + ROTATED_0, + ROTATED_90, + ROTATED_180, + ROTATED_270, + FLIP_ROTATED_0, + FLIP_ROTATED_90, + FLIP_ROTATED_180, + FLIP_ROTATED_270; + } + + /** + * Stores a grid of {@link TileState} as a 2d array referencable with an x and y + * + * @param state the starting state of the grid + */ + protected record TileGrid(TileState[][] state) { + void updateSlot(int x, int y, TileState newState) { + state[y][x] = newState; + } + + TileState getSlot(int x, int y) { + return state[y][x]; + } + + int width() { + return state[0].length; + } + + int height() { + return state.length; + } + } + + /** + * Stores all the different value a permutation of a fossil can have + * + * @param type what the type of fossil it is + * @param grid what the grid will look like for the fossil + * @param xOffset where it's positioned in the excavator window in the x direction + * @param yOffset where it's positioned in the excavator window in the y direction + */ + protected record permutation(FossilTypes type, TileGrid grid, int xOffset, int yOffset) { + /** + * Works out if this is a valid state based on the current state of the excavator window + * + * @param currentState the state of the excavator window + * @return if this screen state can exist depending on found tiles + */ + boolean isValid(TileGrid currentState, String percentage) { + //check the percentage + if (percentage != null && !percentage.equals(type.percentage)) { + return false; + } + //check conflicting tiles + for (int x = 0; x < currentState.width(); x++) { + for (int y = 0; y < currentState.height(); y++) { + TileState knownState = currentState.getSlot(x, y); + //if there is a miss match return false + switch (knownState) { + case UNKNOWN -> { + //still do not know if the tiles will match or not so carry on + continue; + } + case FOSSIL -> { + if (!isFossilCollision(x, y)) { + return false; + } + } + case EMPTY -> { + if (!isEmptyCollision(x, y)) { + return false; + } + } + } + } + } + //if no conflicts return ture + return true; + } + + /** + * If an empty tile is able to exist in this position in the excavator window and the permutation to still be valid + * (will still be valid if out of bound of the permutation) + * + * @param positionX pos x + * @param positionY pos y + * @return if the fossil is valid + */ + private boolean isEmptyCollision(int positionX, int positionY) { + try { + return isState(positionX, positionY, TileState.EMPTY); + } catch (IndexOutOfBoundsException f) { + return true; + } + } + + /** + * If a fossil is able to exist in this position in the excavator window and the permutation to still be valid + * (will not be valid of out of bounds of the permutation) + * + * @param positionX pos x + * @param positionY pos y + * @return if the fossil is valid + */ + boolean isFossilCollision(int positionX, int positionY) { + try { + return isState(positionX, positionY, TileState.FOSSIL); + } catch (IndexOutOfBoundsException f) { + return false; + } + } + + /** + * Returns true if the given state and this permutation line up at a given location + * + * @param positionX position in the excavator window x + * @param positionY position in the excavator window y + * @param state the state the excavator window is at this location + * @return if states match + */ + private boolean isState(int positionX, int positionY, TileState state) { + int x = positionX - xOffset; + int y = positionY - yOffset; + //if they are not in range of the grid they are not a fossil + if (x < 0 || x >= grid.width() || y < 0 || y >= grid.height()) { + throw new IndexOutOfBoundsException("not in grid"); + } + //return if position in grid is fossil + return grid.getSlot(x, y) == state; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java index b015e755..32ccf8e3 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java @@ -4,6 +4,7 @@ import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; import de.hysky.skyblocker.skyblock.bazaar.ReorderHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver; +import de.hysky.skyblocker.skyblock.dwarven.fossil.FossilSolver; import de.hysky.skyblocker.skyblock.item.tooltip.adders.*; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.container.TooltipAdder; @@ -29,6 +30,7 @@ public class TooltipManager { new SupercraftReminder(), ChocolateFactorySolver.INSTANCE, BitsHelper.INSTANCE, + new FossilSolver(), new ReorderHelper(), new StackingEnchantProgressTooltip(0), //Would be best to have after the lore but the tech doesn't exist for that new NpcPriceTooltip(1), diff --git a/src/main/java/de/hysky/skyblocker/utils/container/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/container/ContainerSolverManager.java index 2f4e9bc0..5b7f999b 100644 --- a/src/main/java/de/hysky/skyblocker/utils/container/ContainerSolverManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/container/ContainerSolverManager.java @@ -15,6 +15,7 @@ import de.hysky.skyblocker.skyblock.dungeon.terminal.LightsOnTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal; import de.hysky.skyblocker.skyblock.dwarven.CommissionHighlight; +import de.hysky.skyblocker.skyblock.dwarven.fossil.FossilSolver; import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver; import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver; import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; @@ -54,7 +55,8 @@ public class ContainerSolverManager { ChocolateFactorySolver.INSTANCE, new ReorderHelper(), BitsHelper.INSTANCE, - new RaffleTaskHighlight() + new RaffleTaskHighlight(), + new FossilSolver() }; private static ContainerSolver currentSolver = null; private static List highlights; -- cgit