aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle
diff options
context:
space:
mode:
authorKevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>2023-12-21 14:53:52 +0800
committerKevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>2023-12-21 14:53:52 +0800
commit003834e36b145791dd603858c924926be70e1281 (patch)
treef8fff7b26d3d3880259498c2f3ab18738d20184e /src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle
parent66b6be50ed9480d2d6e442c21ad16ed4bd48b2d6 (diff)
downloadSkyblocker-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')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java250
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java158
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java145
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java109
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))};
+ }
+}