diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..06b6aa0 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,35 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/README.md b/README.md index 6cb7f95..cab2294 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ -## **KillQuest - A Nukkit Quest System** -**KillQuest** is a **feature-rich quest system** for **Nukkit** servers, allowing players to complete **kill and gather quests** for rewards. It includes **multilingual support**, **scoreboard tracking**, and **EconomyAPI integration**. +## **KillQuest - A Nukkit Quest System** v1.0.1 +**KillQuest** is a **feature-rich quest system** for **Nukkit** servers, allowing players to complete **kill and gather quests** for rewards, and challenge themselves with procedurally generated jumping puzzles. It includes **multilingual support**, **scoreboard tracking**, and **EconomyAPI integration**. + +--- + +## Changes to 1.0.1 + +- Added Handling of onPlayerFish event so fishing quests can be created +- Added complete new loging of generating jumping puzzles --- @@ -25,7 +32,11 @@ ✔ **Auto-Saving:** Quest progress is saved to files per player. ✔ **EconomyAPI Integration:** Players earn credits upon completion. ✔ **Multilingual Support:** Uses Minecraft’s translations for entity/item names. - +✔ **Jumping Puzzles**: Procedurally generate jumping puzzles with varying heights and difficulties. +✔ **Puzzle Persistence**: Puzzles are saved and can be reloaded after server restarts. +✔ **Player Movement Tracking**: Detect when players start and complete puzzles. +✔ **Block Restrictions**: Prevent players from modifying puzzle areas. +✔ **Puzzle Management Commands**: Create, list, and remove puzzles dynamically. --- ## **🎮 How to Use** @@ -47,12 +58,22 @@ - **Quest progress resets** - **Scoreboard updates** +### How the Jump Puzzle Works +1. A player generates a puzzle using `/jumpgen`. +2. The puzzle is enclosed with walls and a light-emitting base. +3. Players must jump across blocks that are generated to be challenging but solvable. +4. A tracking system monitors when a player starts and completes the puzzle. +5. Upon completion, the player receives 100 credits via EconomyAPI. + --- ## **📝 Configuration** ### **`quests.yml`** Quests are defined in `plugins/KillQuestPlugin/quests.yml`: +### `puzzles.yml` +Stores all active puzzles, including blocks, start, and end positions, ensuring persistence after restarts. + ```yaml quests: - name: "Zombie Slayer" @@ -95,6 +116,9 @@ translations: |---------|------------| | `/quests` | Opens the quest selection UI | | `/killquest reload` | Reloads quests and translations | +| `/jumpgen ` | Generates a jumping puzzle with a unique name. | +| `/clearpuzzle ` | Clears a specific puzzle. | +| `/listpuzzles` | Lists all active puzzles. | --- @@ -123,7 +147,7 @@ translations: --- ## **💡 Future Improvements** -- ✅ **More Quest Types:** Fishing, Crafting, Mining +- ✅ **More Quest Types:** Crafting - ✅ **Permissions Support** - ✅ **More Customization Options** - ✅ **SQL Database support** diff --git a/pom.xml b/pom.xml index 66bb344..7e69fe2 100644 --- a/pom.xml +++ b/pom.xml @@ -8,21 +8,36 @@ com.digitalwm.killquestplugin KillQuestPlugin - 1.0 + 1.0.1 - install + package ${basedir}/src/main/java org.apache.maven.plugins maven-compiler-plugin + 3.8.1 1.8 1.8 + UTF-8 + + -Xlint:unchecked + -Xlint:deprecation + + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + UTF-8 + + diff --git a/src/main/java/com/digitalwm/killquest/JumpPuzzleGenerator.java b/src/main/java/com/digitalwm/killquest/JumpPuzzleGenerator.java new file mode 100644 index 0000000..434c001 --- /dev/null +++ b/src/main/java/com/digitalwm/killquest/JumpPuzzleGenerator.java @@ -0,0 +1,415 @@ +package com.digitalwm.killquest; + +import cn.nukkit.Player; +import cn.nukkit.block.Block; +import cn.nukkit.level.Level; +import cn.nukkit.math.Vector3; +import cn.nukkit.event.player.PlayerMoveEvent; +import me.onebone.economyapi.EconomyAPI; +import java.util.HashMap; +import java.util.Map; + +import java.util.*; + +public class JumpPuzzleGenerator { + + private final Random random = new Random(); + private final Level level; + private final Vector3 startPos; + private final int length, width, maxHeight; +// private final Player player; + private final KillQuestPlugin plugin; + + private Vector3 puzzleMin; + private Vector3 puzzleMax; + private Vector3 startBlock; + private Vector3 endBlock; + + private final Map playerStartTimes = new HashMap<>(); + + // List to track all blocks generated in the puzzle + + private final Map puzzleBlocks = new HashMap<>(); // ✅ Stores block type + + private final String puzzleName; // ✅ Declare puzzle name + + public JumpPuzzleGenerator(KillQuestPlugin plugin, Level level, Vector3 startPos, String puzzleName, int length, int width, int maxHeight) { + this.level = level; + this.startPos = startPos; + this.length = length; + this.width = width; + this.maxHeight = maxHeight; + this.plugin = plugin; + this.puzzleName = puzzleName; // ✅ Store name + this.setPuzzleBoundaries(this.startPos, this.width, this.length, this.maxHeight); + } + + public void setPuzzleBoundaries(Vector3 startPos, int width, int length, int maxHeight) { + maxHeight++; + puzzleMin = new Vector3(startPos.x - width / 2, startPos.y, startPos.z - length / 2); + puzzleMax = new Vector3(startPos.x + width / 2, startPos.y + maxHeight, startPos.z + length / 2); + plugin.getLogger().info("Puzzle boundaries set: " + puzzleMin + " to " + puzzleMax); + } + + public String getPuzzleName() { + return puzzleName; + } + + public void generate() { + clearArea(); + generateBase(); + generateWalls(); + generatePuzzle(); + } + + private void clearArea() { + plugin.getLogger().info("Clearing area for jump puzzle..."); + + // Get adjusted start position + Vector3 adjustedStart = new Vector3( + startPos.x - width / 2, + startPos.y, + startPos.z - length / 2 + ); + + // ✅ Remove all blocks in the defined area + for (int x = 0; x <= width; x++) { + for (int z = 0; z <= length; z++) { + for (int y = 0; y <= maxHeight; y++) { // Go up to maxHeight + level.setBlock(new Vector3(adjustedStart.x + x, adjustedStart.y + y, adjustedStart.z + z), Block.get(Block.AIR)); + } + } + } + + plugin.getLogger().info("Area cleared successfully!"); + } + + private void generateBase() { + plugin.getLogger().info("Generating puzzle base with light-emitting blocks..."); + + Vector3 baseStart = new Vector3( + startPos.x - width / 2, + startPos.y - 1, + startPos.z - length / 2 + ); + + Vector3 centerBlock = new Vector3( + startPos.x, + startPos.y - 1, + startPos.z + ); + + for (int x = 0; x <= width; x++) { + for (int z = 0; z <= length; z++) { + Vector3 currentPos = new Vector3(baseStart.x + x, baseStart.y, baseStart.z + z); + trackBlock(currentPos, "BASE"); + // ✅ Place a Red Block in the exact center + if (currentPos.x == centerBlock.x && currentPos.z == centerBlock.z) { + level.setBlock(currentPos, Block.get(Block.REDSTONE_BLOCK)); + } else { + level.setBlock(currentPos, Block.get(Block.SEA_LANTERN)); // Light-emitting base + } + } + } + + plugin.getLogger().info("Puzzle base generated with lighting and center marker!"); + } + + private void generateWalls() { + plugin.getLogger().info("Generating walls around the puzzle..."); + + Vector3 baseStart = new Vector3( + startPos.x - width / 2, + startPos.y - 1, + startPos.z - length / 2 + ); + + for (int y = 0; y <= maxHeight; y++) { + for (int x = 0; x <= width; x++) { + Vector3 leftWall = new Vector3(baseStart.x + x, baseStart.y + y, baseStart.z); + Vector3 rightWall = new Vector3(baseStart.x + x, baseStart.y + y, baseStart.z + length); + level.setBlock(leftWall, Block.get(Block.GLASS)); + level.setBlock(rightWall, Block.get(Block.GLASS)); + trackBlock(leftWall, "WALL"); + trackBlock(rightWall, "WALL"); + } + for (int z = 0; z <= length; z++) { + Vector3 frontWall = new Vector3(baseStart.x, baseStart.y + y, baseStart.z + z); + Vector3 backWall = new Vector3(baseStart.x + width, baseStart.y + y, baseStart.z + z); + level.setBlock(frontWall, Block.get(Block.GLASS)); + level.setBlock(backWall, Block.get(Block.GLASS)); + trackBlock(frontWall, "WALL"); + trackBlock(backWall, "WALL"); + } + } + + plugin.getLogger().info("Walls generated successfully!"); + } + +/* private Vector3 getForwardDirection(Vector3 position) { + float yaw = (float) this.player.getYaw(); // Get player's yaw (rotation) + + if (yaw >= -45 && yaw <= 45) { // Facing Z+ + return new Vector3(0, 0, 1); + } else if (yaw > 45 && yaw < 135) { // Facing X- + return new Vector3(-1, 0, 0); + } else if (yaw >= 135 || yaw <= -135) { // Facing Z- + return new Vector3(0, 0, -1); + } else { // Facing X+ + return new Vector3(1, 0, 0); + } + }*/ + + public void clearOnly() { + clearArea(); + } + + private void generatePuzzle() { + plugin.getLogger().info("Generating jump puzzle..."); + + Vector3 lastBlock = new Vector3( + startPos.x - width / 2 + 1, + startPos.y, + startPos.z - length / 2 + 1 + ); + + startBlock = lastBlock.clone(); + level.setBlock(startBlock, Block.get(Block.GOLD_BLOCK)); + trackBlock(startBlock, "START"); // ✅ Track start block + + int currentHeight = 0; + int blocksAtCurrentHeight = 0; + + while (currentHeight < maxHeight) { + boolean validMove = false; + int retryCount = 0; // ✅ Prevent infinite loops + int newX = (int) lastBlock.x, newZ = (int) lastBlock.z; + + do { + retryCount++; + if (retryCount > 50) { // ✅ If we can't find a position after 50 tries, force move + plugin.getLogger().warning("Stuck in loop! Forcing move..."); + break; + } + + int xOffset = random.nextInt(3) - 1; + int zOffset = random.nextInt(3) - 1; + + xOffset *= random.nextBoolean() ? 2 : 1; + zOffset *= random.nextBoolean() ? 2 : 1; + + newX = (int) lastBlock.x + xOffset; + newZ = (int) lastBlock.z + zOffset; + + // ✅ Ensure the new block is within bounds + if (newX <= startPos.x - width / 2 + 1 || newX >= startPos.x + width / 2 - 1) { + continue; + } + if (newZ <= startPos.z - length / 2 + 1 || newZ >= startPos.z + length / 2 - 1) { + continue; + } + + // ✅ Ensure blocks below are air starting from level 3 + if (currentHeight >= 3) { + Vector3 below1 = new Vector3(newX, startPos.y + currentHeight - 1, newZ); + Vector3 below2 = new Vector3(newX, startPos.y + currentHeight - 2, newZ); + + if (level.getBlock(below1).getId() != Block.AIR || level.getBlock(below2).getId() != Block.AIR) { + continue; // Skip this position if blocks are below + } + } + + // ✅ Ensure blocks below are air starting from level 4 + if (currentHeight >= 4) { + Vector3 below1 = new Vector3(newX, startPos.y + currentHeight - 1, newZ); + Vector3 below2 = new Vector3(newX, startPos.y + currentHeight - 2, newZ); + Vector3 below3 = new Vector3(newX, startPos.y + currentHeight - 3, newZ); + + if (level.getBlock(below1).getId() != Block.AIR || level.getBlock(below2).getId() != Block.AIR || level.getBlock(below3).getId() != Block.AIR) { + continue; // Skip this position if blocks are below + } + } + + validMove = true; + + } while (!validMove); + + // ✅ Place the jump block + Vector3 newBlock = new Vector3(newX, startPos.y + currentHeight, newZ); + trackBlock(newBlock, "JUMP"); // ✅ Track jump blocks + level.setBlock(newBlock, Block.get(Block.STONE)); + + plugin.getLogger().debug("Placed block at x: " + newBlock.x + " z: " + newBlock.z); + + blocksAtCurrentHeight++; + + // ✅ Ensure at least 4 blocks are placed per height level before increasing height + if (blocksAtCurrentHeight >= 8) { + currentHeight++; + blocksAtCurrentHeight = 0; + } + + lastBlock = newBlock.clone(); + } + + // ✅ Place end block + endBlock = lastBlock.clone(); + level.setBlock(endBlock, Block.get(Block.DIAMOND_BLOCK)); + trackBlock(endBlock, "END"); // ✅ Track end block + plugin.getLogger().info("Jump puzzle generated successfully!"); + } + + private boolean isInsidePuzzle(Vector3 pos) { + return (pos.x >= puzzleMin.x && pos.x <= puzzleMax.x) && + (pos.y >= puzzleMin.y && pos.y <= puzzleMax.y) && + (pos.z >= puzzleMin.z && pos.z <= puzzleMax.z); + } + + public boolean isPlayerInside(Player player) { + return isInsidePuzzle(player.getPosition()); // ✅ Reuse `isInsidePuzzle` + } + + public void handlePlayerMovement(PlayerMoveEvent event) { + Player player = event.getPlayer(); + Vector3 pos = player.getPosition().floor(); + pos.y = pos.y - 1; + + if (pos.equals(startBlock) && !playerStartTimes.containsKey(player)) { + player.sendMessage("§eYou started the jump puzzle! Reach the end within 15 minutes!"); + playerStartTimes.put(player, System.currentTimeMillis()); + } + + if (playerStartTimes.containsKey(player)) { + long startTime = playerStartTimes.get(player); + if (System.currentTimeMillis() - startTime > 15 * 60 * 1000) { + player.sendMessage("§cYou ran out of time for the jump puzzle!"); + playerStartTimes.remove(player); + return; + } + + if (pos.equals(endBlock)) { + player.sendMessage("§aCongratulations! You completed the jump puzzle!"); + EconomyAPI.getInstance().addMoney(player, 100); + playerStartTimes.remove(player); + } + } + } + + public void removePuzzle() { + plugin.getLogger().info("Removing puzzle '" + puzzleName + "'..."); + + // ✅ Iterate over the saved block positions and remove them + for (Map.Entry entry : puzzleBlocks.entrySet()) { + Vector3 pos = entry.getKey(); + level.setBlock(pos, Block.get(Block.AIR)); + } + + // ✅ Clear the block tracking map + puzzleBlocks.clear(); + + plugin.getLogger().info("Puzzle '" + puzzleName + "' removed successfully!"); + } + + public Map toMap() { + Map data = new HashMap<>(); + data.put("name", puzzleName); + data.put("startX", startPos.x); + data.put("startY", startPos.y); + data.put("startZ", startPos.z); + data.put("length", length); + data.put("width", width); + data.put("maxHeight", maxHeight); + + List> blockList = new ArrayList<>(); + for (Map.Entry entry : puzzleBlocks.entrySet()) { + Map blockData = new HashMap<>(); + blockData.put("x", entry.getKey().x); + blockData.put("y", entry.getKey().y); + blockData.put("z", entry.getKey().z); + blockData.put("type", entry.getValue()); // ✅ Save block type + blockList.add(blockData); + } + data.put("puzzleBlocks", blockList); + + return data; + } + + public static JumpPuzzleGenerator fromMap(KillQuestPlugin plugin, Map data) { + if (!data.containsKey("name") || !data.containsKey("length") || !data.containsKey("width") || + !data.containsKey("maxHeight") || !data.containsKey("startX") || + !data.containsKey("startY") || !data.containsKey("startZ")) { + plugin.getLogger().warning("Skipping puzzle load: Missing required fields."); + return null; // ✅ Skip invalid puzzles + } + + String name = (String) data.get("name"); + int length = ((Number) data.get("length")).intValue(); + int width = ((Number) data.get("width")).intValue(); + int maxHeight = ((Number) data.get("maxHeight")).intValue(); + + int startX = ((Number) data.get("startX")).intValue(); + int startY = ((Number) data.get("startY")).intValue(); + int startZ = ((Number) data.get("startZ")).intValue(); + Vector3 startPos = new Vector3(startX, startY, startZ); // ✅ Load correct position + + // ✅ Get the default level + Level defaultLevel = plugin.getServer().getDefaultLevel(); + if (defaultLevel == null) { + plugin.getLogger().warning("No default level found. Cannot load puzzle: " + name); + return null; + } + + JumpPuzzleGenerator puzzle = new JumpPuzzleGenerator(plugin, defaultLevel, startPos, name, length, width, maxHeight); + + // ✅ Ensure saved blocks exist before accessing them + if (data.containsKey("puzzleBlocks")) { + @SuppressWarnings("unchecked") + List> blocksData = (List>) data.get("puzzleBlocks"); + Map puzzleBlocks = new HashMap<>(); // Create a modifiable map + for (Map blockData : blocksData) { + if (blockData.containsKey("x") && blockData.containsKey("y") && blockData.containsKey("z") && blockData.containsKey("type")) { + int x = ((Number) blockData.get("x")).intValue(); + int y = ((Number) blockData.get("y")).intValue(); + int z = ((Number) blockData.get("z")).intValue(); + String type = (String) blockData.get("type"); + Vector3 blockPos = new Vector3(x, y, z); + puzzleBlocks.put(blockPos, type); // Use the modifiable map + + // ✅ Check if the block is the start or end block + if ("START".equals(type)) { + puzzle.startBlock = blockPos; + } else if ("END".equals(type)) { + puzzle.endBlock = blockPos; + } + } else { + plugin.getLogger().warning("Skipping invalid block entry in puzzle: " + name); + } + } + // Update the puzzle with the modified blocks + puzzle.setPuzzleBlocks(puzzleBlocks); + } else { + plugin.getLogger().warning("Puzzle " + name + " has no saved blocks."); + } + + return puzzle; + } + + private void trackBlock(Vector3 pos, String type) { + puzzleBlocks.put(pos, type); + } + + public Map getPuzzleBlocks() { + return Collections.unmodifiableMap(this.puzzleBlocks); + } + + // Method to update the internal map with new entries + public void updatePuzzleBlocks(Map newBlocks) { + this.puzzleBlocks.putAll(newBlocks); + } + + // Method to clear and update the internal map with new entries + public void setPuzzleBlocks(Map newBlocks) { + this.puzzleBlocks.clear(); + this.puzzleBlocks.putAll(newBlocks); + } +} diff --git a/src/main/java/com/digitalwm/killquest/JumpPuzzleListener.java b/src/main/java/com/digitalwm/killquest/JumpPuzzleListener.java new file mode 100644 index 0000000..4fb47fe --- /dev/null +++ b/src/main/java/com/digitalwm/killquest/JumpPuzzleListener.java @@ -0,0 +1,59 @@ +package com.digitalwm.killquest; + +import cn.nukkit.Player; +import cn.nukkit.event.EventHandler; +import cn.nukkit.event.Listener; +import cn.nukkit.math.Vector3; +import cn.nukkit.event.block.BlockBreakEvent; +import cn.nukkit.event.block.BlockPlaceEvent; +import cn.nukkit.event.player.PlayerMoveEvent; + +public class JumpPuzzleListener implements Listener { + + private final KillQuestPlugin plugin; + + public JumpPuzzleListener(KillQuestPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + + // ✅ Cycle through all active puzzles and trigger their event handling + for (JumpPuzzleGenerator puzzle : plugin.getActiveJumpPuzzles().values()) { // ✅ Fix applied + if (puzzle.isPlayerInside(player)) { + puzzle.handlePlayerMovement(event); + break; // Stop checking after finding one matching puzzle + } + } + } + + @EventHandler + public void onBlockPlace(BlockPlaceEvent event) { + Player player = event.getPlayer(); + Vector3 pos = event.getBlock().getLocation(); + + for (JumpPuzzleGenerator puzzle : plugin.getActiveJumpPuzzles().values()) { // ✅ Fix applied + if (puzzle.isPlayerInside(player)) { + event.setCancelled(true); + player.sendMessage("§cYou cannot place blocks inside the jumping puzzle!"); + plugin.getLogger().info("Blocked " + player.getName() + " from placing a block in the puzzle area."); + } + } + } + + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + Player player = event.getPlayer(); + Vector3 pos = event.getBlock().getLocation(); + + for (JumpPuzzleGenerator puzzle : plugin.getActiveJumpPuzzles().values()) { // ✅ Fix applied + if (puzzle.isPlayerInside(player)) { + event.setCancelled(true); + player.sendMessage("§cYou cannot break blocks inside the jumping puzzle!"); + plugin.getLogger().info("Blocked " + player.getName() + " from breaking a block in the puzzle area."); + } + } + } +} diff --git a/src/main/java/com/digitalwm/killquest/KillQuestPlugin.java b/src/main/java/com/digitalwm/killquest/KillQuestPlugin.java index 4538a3a..04ad006 100644 --- a/src/main/java/com/digitalwm/killquest/KillQuestPlugin.java +++ b/src/main/java/com/digitalwm/killquest/KillQuestPlugin.java @@ -10,16 +10,23 @@ import cn.nukkit.event.EventHandler; import cn.nukkit.event.player.PlayerJoinEvent; import cn.nukkit.event.player.PlayerQuitEvent; +import cn.nukkit.event.player.PlayerFishEvent; import cn.nukkit.event.entity.EntityDeathEvent; import cn.nukkit.event.entity.EntityDamageByEntityEvent; import cn.nukkit.event.inventory.InventoryPickupItemEvent; import cn.nukkit.event.player.PlayerFormRespondedEvent; +import cn.nukkit.block.Block; +import cn.nukkit.level.Level; +import cn.nukkit.math.Vector3; + import cn.nukkit.entity.Entity; import cn.nukkit.Player; import cn.nukkit.item.Item; import cn.nukkit.utils.Config; +import java.io.IOException; + // Import form API classes using the proper packages: import cn.nukkit.form.window.FormWindowSimple; import cn.nukkit.form.response.FormResponseSimple; @@ -65,6 +72,11 @@ public class KillQuestPlugin extends PluginBase implements Listener, CommandExec // Reference to the EconomyAPI instance. private EconomyAPI economy = null; + // Active Jump Puzzles + private final Map activeJumpPuzzles = new HashMap<>(); + + private File puzzlesFile; + public Map getActiveQuests() { return activeQuests; } @@ -106,6 +118,31 @@ public void run() { getServer().getScheduler().scheduleRepeatingTask(this, new QuestScoreboardUpdater(this), 40); getServer().getPluginManager().registerEvents(new ScoreboardListeners(this), this); + getServer().getPluginManager().registerEvents(new JumpPuzzleListener(this), this); + + getLogger().info("Loading saved puzzles..."); + + // Ensure data folder exists + if (!getDataFolder().exists()) { + getDataFolder().mkdirs(); + } + + // Initialize puzzle file if missing (but do NOT overwrite existing ones) + puzzlesFile = new File(getDataFolder(), "puzzles.yml"); + if (!puzzlesFile.exists()) { + try { + puzzlesFile.createNewFile(); + getLogger().info("Created new puzzles.yml file."); + } catch (IOException e) { + getLogger().warning("Failed to create puzzles.yml: " + e.getMessage()); + } + } + + loadJumpPuzzles(); // ✅ Load puzzles AFTER ensuring file exists + } + + public Map getActiveJumpPuzzles() { + return activeJumpPuzzles; } @Override @@ -114,6 +151,8 @@ public void onDisable() { saveActiveQuestProgress(playerName); } getLogger().info("KillQuestPlugin disabled and active quest progress saved."); + saveJumpPuzzles(); + getLogger().info("Puzzles saved."); } private void logBanner() { @@ -266,7 +305,6 @@ public void destroyScoreboard(Player player) { } } - /** * Loads available quests from quests.yml. */ @@ -501,21 +539,122 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage("This command can only be executed by a player."); return true; } + Player player = (Player) sender; - List quests = getRandomQuestsForPlayer(player.getName()); + + // Handle Quest Selection if (command.getName().equalsIgnoreCase("quests")) { + List quests = getRandomQuestsForPlayer(player.getName()); FormWindowSimple form = new FormWindowSimple("Quest Selector", "Select one quest from the list below. Only one active quest is allowed at a time."); for (Quest quest : quests) { - // Create a new ElementButton instead of passing a string. form.addButton(new ElementButton(quest.getName() + "\n" + quest.getDescription())); } player.showFormWindow(form); return true; } + + // Handle Jump Puzzle Generation + if (command.getName().equalsIgnoreCase("jumpgen")) { + if (args.length < 4) { + sender.sendMessage("§cUsage: /jumpgen "); + return false; + } + + int length, width, maxHeight; + + String puzzleName = args[0]; + + // ✅ Check for duplicate names + if (activeJumpPuzzles.containsKey(puzzleName)) { + sender.sendMessage("§cA jump puzzle with this name already exists!"); + return true; + } + + try { + length = Math.max(20, Integer.parseInt(args[1])); // Ensure minimum 20 + width = Math.max(20, Integer.parseInt(args[2])); // Ensure minimum 20 + maxHeight = Math.max(20, Integer.parseInt(args[3])); // Ensure minimum 20 + } catch (NumberFormatException e) { + sender.sendMessage("§cInvalid number format. Use: /jumpgen "); + return true; + } + + // ✅ Use the new class to generate the puzzle + JumpPuzzleGenerator generator = new JumpPuzzleGenerator(this, player.getLevel(), player.getPosition().floor(), puzzleName, length, width, maxHeight); + generator.generate(); + activeJumpPuzzles.put(puzzleName, generator); + saveJumpPuzzles(); // ✅ Save immediately after generation + + sender.sendMessage("§aJump puzzle '" + puzzleName + "' generated!"); + return true; + } + + // ✅ Handle Puzzle Area Clearing + if (command.getName().equalsIgnoreCase("cleararea")) { + if (args.length < 3) { + sender.sendMessage("§cUsage: /cleararea "); + return false; + } + + int length, width, maxHeight; + try { + length = Math.max(20, Integer.parseInt(args[0])); // Ensure minimum 20 + width = Math.max(20, Integer.parseInt(args[1])); // Ensure minimum 20 + maxHeight = Math.max(20, Integer.parseInt(args[2])); // Ensure minimum 20 + } catch (NumberFormatException e) { + sender.sendMessage("§cInvalid number format. Use: /cleararea "); + return true; + } + + getLogger().info("Clearing puzzle area for player " + player.getName() + "..."); + JumpPuzzleGenerator generator = new JumpPuzzleGenerator(this, player.getLevel(), player.getPosition().floor(), "clear", length, width, maxHeight); + generator.clearOnly(); + sender.sendMessage("§aJump puzzle area cleared!"); + getLogger().info("Puzzle area successfully cleared."); + return true; + } + + // Handle Puzzle clear + if (command.getName().equalsIgnoreCase("clearpuzzle")) { + if (args.length < 1) { + sender.sendMessage("§cUsage: /clearpuzzle "); + return false; + } + + String puzzleName = args[0]; + JumpPuzzleGenerator puzzle = activeJumpPuzzles.get(puzzleName); + + if (puzzle == null) { + sender.sendMessage("§cNo puzzle found with that name."); + return true; + } + + puzzle.removePuzzle(); // ✅ Call new remove function + activeJumpPuzzles.remove(puzzleName); + saveJumpPuzzles(); // ✅ Save changes + + sender.sendMessage("§aJump puzzle '" + puzzleName + "' cleared!"); + return true; + } + + // Handle List puzzle + if (command.getName().equalsIgnoreCase("listpuzzle")) { + if (activeJumpPuzzles.isEmpty()) { + sender.sendMessage("§cNo puzzles available."); + } else { + sender.sendMessage("§aActive Jump Puzzles:"); + for (String puzzleName : activeJumpPuzzles.keySet()) { + sender.sendMessage("§6- " + puzzleName); + } + } + return true; + } + return false; } + /** * Handles the player's form response for quest selection. */ @@ -581,6 +720,31 @@ public void run() { } } + @EventHandler + public void onPlayerFish(PlayerFishEvent event) { + Player player = event.getPlayer(); + Item loot = event.getLoot(); // ✅ Get the caught item + + if (loot != null) { + String itemName = normalizeItemName(loot.getName()); + player.sendMessage("§aYou caught a " + itemName + "!"); + + getLogger().info("Player " + player.getName() + " fished up: " + itemName); + + // ✅ Schedule a delayed task to update quest progress + getServer().getScheduler().scheduleDelayedTask(this, new Runnable() { + @Override + public void run() { + updateQuestProgressForPlayer(player); + } + }, 1); // Delay 1 tick to ensure inventory updates + } + } + + private String normalizeItemName(String itemName) { + return itemName.toLowerCase().replace(" ", "_"); + } + /** * Event handler for entity death events. * Tracks kill progress for the player's active quest. @@ -636,4 +800,37 @@ public void onEntityDeath(EntityDeathEvent event) { String getPlayerLanguage(Player player) { return player.getLoginChainData().getLanguageCode(); // ✅ This works! } + + public void saveJumpPuzzles() { + List> puzzleData = new ArrayList<>(); + for (JumpPuzzleGenerator puzzle : activeJumpPuzzles.values()) { + puzzleData.add(puzzle.toMap()); + } + Config config = new Config(puzzlesFile, Config.YAML); + config.set("puzzles", puzzleData); + config.save(); + } + + public void loadJumpPuzzles() { + if (!puzzlesFile.exists()) { + getLogger().info("No saved puzzles found."); + return; + } + + Config config = new Config(puzzlesFile, Config.YAML); + + // ✅ Explicitly cast to List> to ensure type safety + @SuppressWarnings("unchecked") + List> puzzleData = (List>) (List) config.getMapList("puzzles"); + + for (Map data : puzzleData) { + JumpPuzzleGenerator puzzle = JumpPuzzleGenerator.fromMap(this, data); + if (puzzle != null) { // ✅ Ensure puzzle is valid before adding + activeJumpPuzzles.put(puzzle.getPuzzleName(), puzzle); + getLogger().info("Loaded puzzle: " + puzzle.getPuzzleName()); + } else { + getLogger().warning("Skipping invalid puzzle entry."); + } + } + } } \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 39d090b..94961f8 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,8 +1,41 @@ name: KillQuestPlugin -version: 1.0.0 +version: 1.0.1 main: com.digitalwm.killquest.KillQuestPlugin api: [1.1.0] commands: quests: description: "Opens the quest selection form." - usage: "/quests" \ No newline at end of file + usage: "/quests" + jumpgen: + description: "Generates a random jumping puzzle." + usage: "/jumpgen " + permission: killquest.jumpgen + aliases: [jp, jumppuzzle] + cleararea: + description: "Clear an area ahead of the puzzle" + usage: "/cleararea " + permission: killquest.clear + aliases: [ca] + clearpuzzle: + description: "Clear an create puzzle" + usage: "/clearpuzzle " + permission: killquest.clearpuzzle + aliases: [cp] + listpuzzle: + description: "List all generated puzzles" + usage: "/listpuzzle" + permission: killquest:listpuzzle + +permissions: + killquest.jumpgen: + description: "Allows the player to generate a jump puzzle." + default: op + killquest.clear: + description: "Allows the player to clear an area for jump puzzle." + default: op + killquest.clearpuzzle: + description: "Allows the player to clear a create jump puzzle." + default: op + killquest.listpuzzle: + description: "List all generated puzzles." + default: op \ No newline at end of file