diff options
| author | Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> | 2023-12-21 14:53:52 +0800 |
|---|---|---|
| committer | Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> | 2023-12-21 14:53:52 +0800 |
| commit | 003834e36b145791dd603858c924926be70e1281 (patch) | |
| tree | f8fff7b26d3d3880259498c2f3ab18738d20184e /src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle | |
| parent | 66b6be50ed9480d2d6e442c21ad16ed4bd48b2d6 (diff) | |
| download | Skyblocker-003834e36b145791dd603858c924926be70e1281.tar.gz Skyblocker-003834e36b145791dd603858c924926be70e1281.tar.bz2 Skyblocker-003834e36b145791dd603858c924926be70e1281.zip | |
Refactor puzzle solvers
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle')
6 files changed, 759 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java new file mode 100644 index 00000000..8de1e3fe --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java @@ -0,0 +1,250 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.joml.Intersectiond; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class CreeperBeams extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName()); + + private static final float[][] COLORS = { + DyeColor.LIGHT_BLUE.getColorComponents(), + DyeColor.LIME.getColorComponents(), + DyeColor.YELLOW.getColorComponents(), + DyeColor.MAGENTA.getColorComponents(), + }; + private static final float[] GREEN_COLOR_COMPONENTS = DyeColor.GREEN.getColorComponents(); + + private static final int FLOOR_Y = 68; + private static final int BASE_Y = 74; + private static final CreeperBeams INSTANCE = new CreeperBeams("creeper", "creeper-room"); + + private static ArrayList<Beam> beams = new ArrayList<>(); + private static BlockPos base = null; + + private CreeperBeams(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + @Override + public void reset() { + super.reset(); + beams.clear(); + base = null; + } + + @Override + public void tick() { + + // don't do anything if the room is solved + if (!shouldSolve()) { + return; + } + + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + + // clear state if not in dungeon + if (world == null || player == null || !Utils.isInDungeons()) { + return; + } + + // try to find base if not found and solve + if (base == null) { + base = findCreeperBase(player, world); + if (base == null) { + return; + } + Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); + ArrayList<BlockPos> targets = findTargets(world, base); + beams = findLines(creeperPos, targets); + } + + // update the beam states + beams.forEach(b -> b.updateState(world)); + + // check if the room is solved + if (!isTarget(world, base)) { + reset(); + } + } + + // find the sea lantern block beneath the creeper + private static BlockPos findCreeperBase(ClientPlayerEntity player, ClientWorld world) { + + // find all creepers + List<CreeperEntity> creepers = world.getEntitiesByClass( + CreeperEntity.class, + player.getBoundingBox().expand(50D), + EntityPredicates.VALID_ENTITY); + + if (creepers.isEmpty()) { + return null; + } + + // (sanity) check: + // if the creeper isn't above a sea lantern, it's not the target. + for (CreeperEntity ce : creepers) { + Vec3d creeperPos = ce.getPos(); + BlockPos potentialBase = BlockPos.ofFloored(creeperPos.x, BASE_Y, creeperPos.z); + if (isTarget(world, potentialBase)) { + return potentialBase; + } + } + + return null; + + } + + // find the sea lanterns (and the ONE prismarine ty hypixel) in the room + private static ArrayList<BlockPos> findTargets(ClientWorld world, BlockPos basePos) { + ArrayList<BlockPos> targets = new ArrayList<>(); + + BlockPos start = new BlockPos(basePos.getX() - 15, BASE_Y + 12, basePos.getZ() - 15); + BlockPos end = new BlockPos(basePos.getX() + 16, FLOOR_Y, basePos.getZ() + 16); + + for (BlockPos pos : BlockPos.iterate(start, end)) { + if (isTarget(world, pos)) { + targets.add(new BlockPos(pos)); + } + } + return targets; + } + + // generate lines between targets and finally find the solution + private static ArrayList<Beam> findLines(Vec3d creeperPos, ArrayList<BlockPos> targets) { + + ArrayList<ObjectDoublePair<Beam>> allLines = new ArrayList<>(); + + // optimize this a little bit by + // only generating lines "one way", i.e. 1 -> 2 but not 2 -> 1 + for (int i = 0; i < targets.size(); i++) { + for (int j = i + 1; j < targets.size(); j++) { + Beam beam = new Beam(targets.get(i), targets.get(j)); + double dist = Intersectiond.distancePointLine( + creeperPos.x, creeperPos.y, creeperPos.z, + beam.line[0].x, beam.line[0].y, beam.line[0].z, + beam.line[1].x, beam.line[1].y, beam.line[1].z); + allLines.add(ObjectDoublePair.of(beam, dist)); + } + } + + // this feels a bit heavy-handed, but it works for now. + + ArrayList<Beam> result = new ArrayList<>(); + allLines.sort(Comparator.comparingDouble(ObjectDoublePair::rightDouble)); + + while (result.size() < 4 && !allLines.isEmpty()) { + Beam solution = allLines.get(0).left(); + result.add(solution); + + // remove the line we just added and other lines that use blocks we're using for + // that line + allLines.remove(0); + allLines.removeIf(beam -> solution.containsComponentOf(beam.left())); + } + + if (result.size() != 4) { + LOGGER.error("Not enough solutions found. This is bad..."); + } + + return result; + } + + @Override + public void render(WorldRenderContext wrc) { + + // don't render if solved or disabled + if (!shouldSolve() || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { + return; + } + + // lines.size() is always <= 4 so no issues OOB issues with the colors here. + for (int i = 0; i < beams.size(); i++) { + beams.get(i).render(wrc, COLORS[i]); + } + } + + private static boolean isTarget(ClientWorld world, BlockPos pos) { + Block block = world.getBlockState(pos).getBlock(); + return block == Blocks.SEA_LANTERN || block == Blocks.PRISMARINE; + } + + // helper class to hold all the things needed to render a beam + private static class Beam { + + // raw block pos of target + public BlockPos blockOne; + public BlockPos blockTwo; + + // middle of targets used for rendering the line + public Vec3d[] line = new Vec3d[2]; + + // boxes used for rendering the block outline + public Box outlineOne; + public Box outlineTwo; + + // state: is this beam created/inputted or not? + private boolean toDo = true; + + public Beam(BlockPos a, BlockPos b) { + blockOne = a; + blockTwo = b; + line[0] = new Vec3d(a.getX() + 0.5, a.getY() + 0.5, a.getZ() + 0.5); + line[1] = new Vec3d(b.getX() + 0.5, b.getY() + 0.5, b.getZ() + 0.5); + outlineOne = new Box(a); + outlineTwo = new Box(b); + } + + // used to filter the list of all beams so that no two beams share a target + public boolean containsComponentOf(Beam other) { + return this.blockOne.equals(other.blockOne) + || this.blockOne.equals(other.blockTwo) + || this.blockTwo.equals(other.blockOne) + || this.blockTwo.equals(other.blockTwo); + } + + // update the state: is the beam created or not? + public void updateState(ClientWorld world) { + toDo = !(world.getBlockState(blockOne).getBlock() == Blocks.PRISMARINE + && world.getBlockState(blockTwo).getBlock() == Blocks.PRISMARINE); + } + + // render either in a color if not created or faintly green if created + public void render(WorldRenderContext wrc, float[] color) { + if (toDo) { + RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); + RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); + RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); + } else { + RenderHelper.renderOutline(wrc, outlineOne, GREEN_COLOR_COMPONENTS, 1, false); + RenderHelper.renderOutline(wrc, outlineTwo, GREEN_COLOR_COMPONENTS, 1, false); + RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java new file mode 100644 index 00000000..5774eaef --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java @@ -0,0 +1,158 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * This class provides functionality to render outlines around Blaze entities + */ +public class DungeonBlaze extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName()); + private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; + private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; + private static final DungeonBlaze INSTANCE = new DungeonBlaze("blaze", "blaze-room-1-high", "blaze-room-1-low"); + + private static ArmorStandEntity highestBlaze = null; + private static ArmorStandEntity lowestBlaze = null; + private static ArmorStandEntity nextHighestBlaze = null; + private static ArmorStandEntity nextLowestBlaze = null; + + private DungeonBlaze(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + /** + * Updates the state of Blaze entities and triggers the rendering process if necessary. + */ + @Override + public void tick() { + if (!shouldSolve()) { + return; + } + ClientWorld world = MinecraftClient.getInstance().world; + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (world == null || player == null || !Utils.isInDungeons()) return; + List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(world, player); + sortBlazes(blazes); + updateBlazeEntities(blazes); + } + + /** + * Retrieves Blaze entities in the world and parses their health information. + * + * @param world The client world to search for Blaze entities. + * @return A list of Blaze entities and their associated health. + */ + private static List<ObjectIntPair<ArmorStandEntity>> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) { + List<ObjectIntPair<ArmorStandEntity>> blazes = new ArrayList<>(); + for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) { + String blazeName = blaze.getName().getString(); + if (blazeName.contains("Blaze") && blazeName.contains("/")) { + try { + int health = Integer.parseInt((blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1)).replaceAll(",", "")); + blazes.add(ObjectIntPair.of(blaze, health)); + } catch (NumberFormatException e) { + handleException(e); + } + } + } + return blazes; + } + + /** + * Sorts the Blaze entities based on their health values. + * + * @param blazes The list of Blaze entities to be sorted. + */ + private static void sortBlazes(List<ObjectIntPair<ArmorStandEntity>> blazes) { + blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt)); + } + + /** + * Updates information about Blaze entities based on sorted list. + * + * @param blazes The sorted list of Blaze entities with associated health values. + */ + private static void updateBlazeEntities(List<ObjectIntPair<ArmorStandEntity>> blazes) { + if (!blazes.isEmpty()) { + lowestBlaze = blazes.get(0).left(); + int highestIndex = blazes.size() - 1; + highestBlaze = blazes.get(highestIndex).left(); + if (blazes.size() > 1) { + nextLowestBlaze = blazes.get(1).left(); + nextHighestBlaze = blazes.get(highestIndex - 1).left(); + } + } + } + + /** + * Renders outlines for Blaze entities based on health and position. + * + * @param wrc The WorldRenderContext used for rendering. + */ + @Override + public void render(WorldRenderContext wrc) { + try { + if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazeSolver) { + if (highestBlaze.getY() < 69) { + renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc); + } + if (lowestBlaze.getY() > 69) { + renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc); + } + } + } catch (Exception e) { + handleException(e); + } + } + + /** + * Renders outlines for Blaze entities and connections between them. + * + * @param blaze The Blaze entity for which to render an outline. + * @param nextBlaze The next Blaze entity for connection rendering. + * @param wrc The WorldRenderContext used for rendering. + */ + private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) { + Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); + RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f, false); + + if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) { + Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); + RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f, false); + + Vec3d blazeCenter = blazeBox.getCenter(); + Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); + + RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); + } + } + + /** + * Handles exceptions by logging and printing stack traces. + * + * @param e The exception to handle. + */ + private static void handleException(Exception e) { + LOGGER.error("[Skyblocker BlazeRenderer] Encountered an unknown exception", e); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java new file mode 100644 index 00000000..04446e60 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java @@ -0,0 +1,58 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import com.mojang.brigadier.Command; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.events.DungeonEvents; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; +import de.hysky.skyblocker.utils.render.Renderable; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public abstract class DungeonPuzzle implements Tickable, Renderable { + private final String puzzleName; + @NotNull + private final Set<String> roomNames; + private boolean shouldSolve; + + public DungeonPuzzle(String puzzleName, String... roomName) { + this(puzzleName, Set.of(roomName)); + } + + public DungeonPuzzle(String puzzleName, @NotNull Set<String> roomNames) { + this.puzzleName = puzzleName; + this.roomNames = roomNames; + DungeonEvents.PUZZLE_MATCHED.register(room -> { + if (roomNames.contains(room.getName())) { + room.addSubProcess(this); + shouldSolve = true; + } + }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("solvePuzzle").then(literal(puzzleName).executes(context -> { + Room currentRoom = DungeonManager.getCurrentRoom(); + if (currentRoom != null) { + currentRoom.addSubProcess(this); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§aSolving " + puzzleName + " puzzle in the current room.")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); + } + return Command.SINGLE_SUCCESS; + })))))); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + public boolean shouldSolve() { + return shouldSolve; + } + + public void reset() { + shouldSolve = false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java new file mode 100644 index 00000000..c5e55f93 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.regex.Matcher; + +public class ThreeWeirdos extends ChatPatternListener { + public ThreeWeirdos() { + super("^§e\\[NPC] §c([A-Z][a-z]+)§f: (?:The reward is(?: not in my chest!|n't in any of our chests\\.)|My chest (?:doesn't have the reward\\. We are all telling the truth\\.|has the reward and I'm telling the truth!)|At least one of them is lying, and the reward is not in §c§c[A-Z][a-z]+'s §rchest\\!|Both of them are telling the truth\\. Also, §c§c[A-Z][a-z]+ §rhas the reward in their chest\\!)$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().locations.dungeons.solveThreeWeirdos ? null : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) return false; + client.world.getEntitiesByClass( + ArmorStandEntity.class, + client.player.getBoundingBox().expand(3), + entity -> { + Text customName = entity.getCustomName(); + return customName != null && customName.getString().equals(matcher.group(1)); + } + ).forEach( + entity -> entity.setCustomName(Text.of(Formatting.GREEN + matcher.group(1))) + ); + return false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java new file mode 100644 index 00000000..90028a4f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java @@ -0,0 +1,145 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.decoration.ItemFrameEntity; +import net.minecraft.item.FilledMapItem; +import net.minecraft.item.map.MapState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Thanks to Danker for a reference implementation! + */ +public class TicTacToe extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); + private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; + private static final TicTacToe INSTANCE = new TicTacToe("tic-tac-toe", "tic-tac-toe-1"); + private static Box nextBestMoveToMake = null; + + private TicTacToe(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + @Override + public void tick() { + if (!shouldSolve()) { + return; + } + + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + + nextBestMoveToMake = null; + + if (world == null || player == null || !Utils.isInDungeons()) return; + + //Search within 21 blocks for item frames that contain maps + Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21); + List<ItemFrameEntity> itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); + + try { + //Only attempt to solve if its the player's turn + if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) { + char[][] board = new char[3][3]; + BlockPos leftmostRow = null; + int sign = 1; + char facing = 'X'; + + for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { + MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); + + if (mapState == null) continue; + + int column = 0, row; + sign = 1; + + //Find position of the item frame relative to where it is on the tic tac toe board + if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1; + BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ()); + + for (int i = 2; i >= 0; i--) { + int realI = i * sign; + BlockPos blockPos = itemFramePos; + + if (itemFrame.getX() % 0.5 == 0) { + blockPos = itemFramePos.add(realI, 0, 0); + } else if (itemFrame.getZ() % 0.5 == 0) { + blockPos = itemFramePos.add(0, 0, realI); + facing = 'Z'; + } + + Block block = world.getBlockState(blockPos).getBlock(); + if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { + leftmostRow = blockPos; + column = i; + + break; + } + } + + //Determine the row of the item frame + if (itemFrame.getY() == 72.5) { + row = 0; + } else if (itemFrame.getY() == 71.5) { + row = 1; + } else if (itemFrame.getY() == 70.5) { + row = 2; + } else { + continue; + } + + + //Get the color of the middle pixel of the map which determines whether its X or O + int middleColor = mapState.colors[8256] & 255; + + if (middleColor == 114) { + board[row][column] = 'X'; + } else if (middleColor == 33) { + board[row][column] = 'O'; + } + + int bestMove = TicTacToeUtils.getBestMove(board) - 1; + + if (leftmostRow != null) { + double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX(); + double drawY = 72 - (double) (bestMove / 3); + double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ(); + + nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1); + } + } + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e); + } + } + + @Override + public void render(WorldRenderContext context) { + try { + if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) { + RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5, false); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java new file mode 100644 index 00000000..0f73457c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java @@ -0,0 +1,109 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.waypoint.FairySouls; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import java.util.*; +import java.util.regex.Matcher; + +public class Trivia extends ChatPatternListener { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map<String, String[]> answers; + private List<String> solutions = Collections.emptyList(); + + public Trivia() { + super("^ +(?:([A-Za-z,' ]*\\?)|§6 ([ⓐⓑⓒ]) §a([a-zA-Z0-9 ]+))$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().locations.dungeons.solveTrivia ? ChatFilterResult.FILTER : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + String riddle = matcher.group(3); + if (riddle != null) { + if (!solutions.contains(riddle)) { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null) + MinecraftClient.getInstance().player.sendMessage(Text.of(" " + Formatting.GOLD + matcher.group(2) + Formatting.RED + " " + riddle), false); + return player != null; + } + } else updateSolutions(matcher.group(0)); + return false; + } + + private void updateSolutions(String question) { + try { + String trimmedQuestion = question.trim(); + if (trimmedQuestion.equals("What SkyBlock year is it?")) { + long currentTime = System.currentTimeMillis() / 1000L; + long diff = currentTime - 1560276000; + int year = (int) (diff / 446400 + 1); + solutions = Collections.singletonList("Year " + year); + } else { + String[] questionAnswers = answers.get(trimmedQuestion); + if (questionAnswers != null) solutions = Arrays.asList(questionAnswers); + } + } catch (Exception e) { //Hopefully the solver doesn't go south + LOGGER.error("[Skyblocker] Failed to update the Trivia puzzle answers!", e); + } + } + + static { + answers = Collections.synchronizedMap(new HashMap<>()); + answers.put("What is the status of The Watcher?", new String[]{"Stalker"}); + answers.put("What is the status of Bonzo?", new String[]{"New Necromancer"}); + answers.put("What is the status of Scarf?", new String[]{"Apprentice Necromancer"}); + answers.put("What is the status of The Professor?", new String[]{"Professor"}); + answers.put("What is the status of Thorn?", new String[]{"Shaman Necromancer"}); + answers.put("What is the status of Livid?", new String[]{"Master Necromancer"}); + answers.put("What is the status of Sadan?", new String[]{"Necromancer Lord"}); + answers.put("What is the status of Maxor?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Storm?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Necron?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"}); + answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"}); + answers.put("What is the name of Rick's brother?", new String[]{"Pat"}); + answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"}); + answers.put("What is the name of the person that upgrades pets?", new String[]{"Kat"}); + answers.put("What is the name of the lady of the Nether?", new String[]{"Elle"}); + answers.put("Which villager in the Village gives you a Rogue Sword?", new String[]{"Jamie"}); + answers.put("How many unique minions are there?", new String[]{"59 Minions"}); + answers.put("Which of these enemies does not spawn in the Spider's Den?", new String[]{"Zombie Spider", "Cave Spider", "Wither Skeleton", "Dashing Spooder", "Broodfather", "Night Spider"}); + answers.put("Which of these monsters only spawns at night?", new String[]{"Zombie Villager", "Ghast"}); + answers.put("Which of these is not a dragon in The End?", new String[]{"Zoomer Dragon", "Weak Dragon", "Stonk Dragon", "Holy Dragon", "Boomer Dragon", "Booger Dragon", "Older Dragon", "Elder Dragon", "Stable Dragon", "Professor Dragon"}); + FairySouls.runAsyncAfterFairySoulsLoad(() -> { + answers.put("How many total Fairy Souls are there?", getFairySoulsSizeString(null)); + answers.put("How many Fairy Souls are there in Spider's Den?", getFairySoulsSizeString("combat_1")); + answers.put("How many Fairy Souls are there in The End?", getFairySoulsSizeString("combat_3")); + answers.put("How many Fairy Souls are there in The Farming Islands?", getFairySoulsSizeString("farming_1")); + answers.put("How many Fairy Souls are there in Crimson Isle?", getFairySoulsSizeString("crimson_isle")); + answers.put("How many Fairy Souls are there in The Park?", getFairySoulsSizeString("foraging_1")); + answers.put("How many Fairy Souls are there in Jerry's Workshop?", getFairySoulsSizeString("winter")); + answers.put("How many Fairy Souls are there in Hub?", getFairySoulsSizeString("hub")); + answers.put("How many Fairy Souls are there in The Hub?", getFairySoulsSizeString("hub")); + answers.put("How many Fairy Souls are there in Deep Caverns?", getFairySoulsSizeString("mining_2")); + answers.put("How many Fairy Souls are there in Gold Mine?", getFairySoulsSizeString("mining_1")); + answers.put("How many Fairy Souls are there in Dungeon Hub?", getFairySoulsSizeString("dungeon_hub")); + }); + } + + @NotNull + private static String[] getFairySoulsSizeString(@Nullable String location) { + return new String[]{"%d Fairy Souls".formatted(FairySouls.getFairySoulsSize(location))}; + } +} |
