From a18a6b0dcaaa9a75e3472e731c33d79c92b51d0f Mon Sep 17 00:00:00 2001 From: jannis Date: Sun, 25 Oct 2026 00:17:33 +0200 Subject: [PATCH] fixed placement issues in BuildBattle and added isInArea methode to Position.java --- .../buildBattle/BlockPlacementHelper.java | 896 ++++++++++++++++++ .../types/buildBattle/BuildBattle.java | 59 +- .../types/buildBattle/FluidFlowHelper.java | 268 ++++++ .../mhsl/minenet/minigames/util/Position.java | 13 + 4 files changed, 1227 insertions(+), 9 deletions(-) create mode 100644 src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BlockPlacementHelper.java create mode 100644 src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BlockPlacementHelper.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BlockPlacementHelper.java new file mode 100644 index 0000000..8d661a3 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BlockPlacementHelper.java @@ -0,0 +1,896 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import net.minestom.server.coordinate.BlockVec; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockFace; +import net.minestom.server.utils.Direction; + +import java.util.Map; +import java.util.Set; + +public final class BlockPlacementHelper { + private BlockPlacementHelper() {} + + private static final Set SIX_DIR = Set.of( + "observer", "dispenser", "dropper", "piston", "sticky_piston", + "amethyst_cluster", "small_amethyst_bud", "medium_amethyst_bud", "large_amethyst_bud", + "end_rod", "lightning_rod", "shulker_box" + ); + + public static void apply(PlayerBlockPlaceEvent event) { + Block block = event.getBlock(); + BlockFace face = event.getBlockFace(); + BlockVec pos = event.getBlockPosition(); + Point cursor = event.getCursorPosition(); + Player player = event.getPlayer(); + Instance instance = event.getInstance(); + + String path = block.key().value(); + + Block existing = instance.getBlock(pos); + if ((path.endsWith("_candle") || path.equals("candle")) && existing.compare(block)) { + int n = parseIntOr(existing.getProperty("candles"), 1); + event.setBlock(n < 4 ? existing.withProperty("candles", String.valueOf(n + 1)) : existing); + return; + } + if (path.equals("snow") && existing.compare(block)) { + int n = parseIntOr(existing.getProperty("layers"), 1); + event.setBlock(n < 8 ? existing.withProperty("layers", String.valueOf(n + 1)) : existing); + return; + } + if (path.equals("sea_pickle") && existing.compare(block)) { + int n = parseIntOr(existing.getProperty("pickles"), 1); + event.setBlock(n < 4 ? existing.withProperty("pickles", String.valueOf(n + 1)) : existing); + return; + } + if (path.equals("turtle_egg") && existing.compare(block)) { + int n = parseIntOr(existing.getProperty("eggs"), 1); + event.setBlock(n < 4 ? existing.withProperty("eggs", String.valueOf(n + 1)) : existing); + return; + } + + Block oriented = orient(block, path, face, cursor, player, instance, pos); + if (oriented != block) event.setBlock(oriented); + + Block finalOriented = oriented; + String finalPath = finalOriented.key().value(); + + if (path.endsWith("_door")) { + Block lower = event.getBlock(); + String facing = lower.getProperty("facing"); + String hinge = lower.getProperty("hinge"); + Block upper = lower.withProperty("half", "upper").withProperty("facing", facing).withProperty("hinge", hinge); + instance.scheduler().scheduleNextTick(() -> instance.setBlock(pos.add(0, 1, 0), upper)); + } + + if (path.endsWith("_bed")) { + String facing = finalOriented.getProperty("facing"); + Direction d = facingToDirection(facing); + Block head = finalOriented.withProperty("part", "head"); + BlockVec headPos = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + instance.scheduler().scheduleNextTick(() -> instance.setBlock(headPos, head)); + } + + if (path.equals("big_dripleaf")) { + Block upper = finalOriented; + Block lower = Block.BIG_DRIPLEAF_STEM.withProperty("facing", finalOriented.getProperty("facing")); + instance.scheduler().scheduleNextTick(() -> instance.setBlock(pos.add(0, 1, 0), upper.withProperty("half", "upper"))); + } + + if (isConnecting(path)) { + instance.scheduler().scheduleNextTick(() -> { + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + if (isConnecting(nb.key().value())) instance.setBlock(np, computeConnections(nb, nb.key().value(), instance, np)); + } + Block self = instance.getBlock(pos); + if (isConnecting(self.key().value())) instance.setBlock(pos, computeConnections(self, self.key().value(), instance, pos)); + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + if (nb.key().value().endsWith("_fence_gate")) { + instance.setBlock(np, updateFenceGateInWall(nb, instance, np)); + } + } + }); + } + + if (path.endsWith("_fence_gate")) { + instance.scheduler().scheduleNextTick(() -> instance.setBlock(pos, updateFenceGateInWall(instance.getBlock(pos), instance, pos))); + } + + if (path.equals("redstone_wire")) { + instance.scheduler().scheduleNextTick(() -> { + updateRedstoneWire(instance, pos); + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + if (instance.getBlock(np).key().value().equals("redstone_wire")) updateRedstoneWire(instance, np); + } + }); + } + + if (isRail(path)) { + instance.scheduler().scheduleNextTick(() -> updateRailShape(instance, pos)); + } + + if (path.equals("scaffolding")) { + instance.scheduler().scheduleNextTick(() -> updateScaffolding(instance, pos)); + } + + if (finalPath.equals("chest") || finalPath.equals("trapped_chest")) { + instance.scheduler().scheduleNextTick(() -> updateChestPair(instance, pos)); + } + } + + private static Block orient(Block block, String path, BlockFace face, Point cursor, Player player, + Instance instance, BlockVec pos) { + Map props = block.properties(); + Block result = block; + + if (props.containsKey("axis")) { + result = result.withProperty("axis", switch (face) { + case TOP, BOTTOM -> "y"; + case NORTH, SOUTH -> "z"; + case EAST, WEST -> "x"; + }); + } + + if (props.containsKey("type") && path.endsWith("_slab")) { + Block existing = instance.getBlock(pos); + if (existing.compare(block)) { + result = result.withProperty("type", "double"); + } else { + boolean top = face == BlockFace.BOTTOM || (face != BlockFace.TOP && cursor.y() > 0.5); + result = result.withProperty("type", top ? "top" : "bottom"); + } + } + + if (path.endsWith("_stairs")) { + String facing = yawToFacing(player.getPosition().yaw()); + boolean top = face == BlockFace.BOTTOM || (face != BlockFace.TOP && cursor.y() > 0.5); + result = result.withProperty("facing", facing).withProperty("half", top ? "top" : "bottom"); + result = applyStairShape(result, instance, pos, facing, top); + } + + if (path.endsWith("_trapdoor")) { + String facing; + boolean top; + if (face == BlockFace.TOP) { top = false; facing = yawToFacing(player.getPosition().yaw() + 180); } + else if (face == BlockFace.BOTTOM) { top = true; facing = yawToFacing(player.getPosition().yaw() + 180); } + else { top = cursor.y() > 0.5; facing = oppositeFacing(faceName(face)); } + result = result.withProperty("facing", facing).withProperty("half", top ? "top" : "bottom") + .withProperty("open", "false").withProperty("powered", "false"); + } + + if (path.endsWith("_door")) { + String facing = yawToFacing(player.getPosition().yaw()); + String hinge = doorHinge(facing, cursor, instance, pos, path); + result = result.withProperty("facing", facing) + .withProperty("half", "lower") + .withProperty("hinge", hinge) + .withProperty("open", "false") + .withProperty("powered", "false"); + } + + if (path.endsWith("_fence_gate")) { + String facing = yawToFacing(player.getPosition().yaw()); + result = result.withProperty("facing", facing).withProperty("open", "false") + .withProperty("powered", "false").withProperty("in_wall", "false"); + } + + if (path.endsWith("_bed")) { + String facing = yawToFacing(player.getPosition().yaw()); + result = result.withProperty("facing", facing).withProperty("part", "foot").withProperty("occupied", "false"); + } + + if (path.endsWith("_button") || path.equals("lever") || path.equals("grindstone")) { + String attachFace; String facing; + if (face == BlockFace.TOP) { attachFace = "floor"; facing = yawToFacing(player.getPosition().yaw()); } + else if (face == BlockFace.BOTTOM) { attachFace = "ceiling"; facing = yawToFacing(player.getPosition().yaw()); } + else { attachFace = "wall"; facing = oppositeFacing(faceName(face)); } + result = result.withProperty("face", attachFace).withProperty("facing", facing); + } + + if (path.equals("bell")) { + String attachment; String facing; + if (face == BlockFace.TOP) { attachment = "floor"; facing = yawToFacing(player.getPosition().yaw()); } + else if (face == BlockFace.BOTTOM) { attachment = "ceiling"; facing = yawToFacing(player.getPosition().yaw()); } + else { + attachment = "single_wall"; + facing = oppositeFacing(faceName(face)); + } + result = result.withProperty("attachment", attachment).withProperty("facing", facing); + } + + if (path.equals("lantern") || path.equals("soul_lantern")) { + result = result.withProperty("hanging", face == BlockFace.BOTTOM ? "true" : "false"); + } + + if (path.equals("tripwire_hook")) { + String facing = face != BlockFace.TOP && face != BlockFace.BOTTOM + ? oppositeFacing(faceName(face)) + : yawToFacing(player.getPosition().yaw()); + result = result.withProperty("facing", facing).withProperty("attached", "false").withProperty("powered", "false"); + } + + if (path.equals("hopper")) { + String facing = face == BlockFace.TOP ? "down" : (face == BlockFace.BOTTOM ? "down" : oppositeFacing(faceName(face))); + result = result.withProperty("facing", facing).withProperty("enabled", "true"); + } + + if (path.equals("campfire") || path.equals("soul_campfire")) { + Block below = instance.getBlock(new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ())); + String signalFire = below.key().value().equals("hay_block") ? "true" : "false"; + result = result.withProperty("facing", yawToFacing(player.getPosition().yaw())) + .withProperty("lit", "true").withProperty("signal_fire", signalFire); + } + + if (path.equals("pointed_dripstone")) { + String dir = face == BlockFace.BOTTOM ? "down" : "up"; + result = result.withProperty("vertical_direction", dir).withProperty("thickness", "tip"); + } + + if (path.equals("chorus_flower")) { + result = result.withProperty("age", "0"); + } + + if (path.equals("end_portal_frame") || path.equals("respawn_anchor")) { + if (props.containsKey("facing")) { + result = result.withProperty("facing", yawToFacing(player.getPosition().yaw())); + } + if (props.containsKey("eye")) result = result.withProperty("eye", "false"); + } + + if ((path.equals("torch") || path.equals("soul_torch") || path.equals("redstone_torch") || path.equals("copper_torch")) + && face != BlockFace.TOP && face != BlockFace.BOTTOM) { + Block wall = switch (path) { + case "torch" -> Block.WALL_TORCH; + case "soul_torch" -> Block.SOUL_WALL_TORCH; + case "redstone_torch" -> Block.REDSTONE_WALL_TORCH; + case "copper_torch" -> Block.COPPER_WALL_TORCH; + default -> null; + }; + if (wall != null) result = wall.withProperty("facing", oppositeFacing(faceName(face))); + } + + if (path.endsWith("_skull") || path.endsWith("_head")) { + if (face != BlockFace.TOP && face != BlockFace.BOTTOM) { + String wallName = path.endsWith("_skull") ? path.replace("_skull", "_wall_skull") : path.replace("_head", "_wall_head"); + Block wall = Block.fromKey("minecraft:" + wallName); + if (wall != null) result = wall.withProperty("facing", oppositeFacing(faceName(face))); + } else if (face == BlockFace.TOP && result.properties().containsKey("rotation")) { + int rot = (int) Math.floor(((player.getPosition().yaw() + 180.0) * 16.0 / 360.0) + 0.5) & 15; + result = result.withProperty("rotation", String.valueOf(rot)); + } + } + + if (path.endsWith("_sign") && !path.endsWith("_wall_sign") && !path.contains("hanging") + && face != BlockFace.TOP && face != BlockFace.BOTTOM) { + Block wallSign = Block.fromKey("minecraft:" + path.replace("_sign", "_wall_sign")); + if (wallSign != null) result = wallSign.withProperty("facing", oppositeFacing(faceName(face))); + } else if (path.endsWith("_sign") && !path.endsWith("_wall_sign") && !path.contains("hanging") + && face == BlockFace.TOP) { + int rot = (int) Math.floor(((player.getPosition().yaw() + 180.0) * 16.0 / 360.0) + 0.5) & 15; + if (result.properties().containsKey("rotation")) result = result.withProperty("rotation", String.valueOf(rot)); + } + + if (path.endsWith("_hanging_sign") && !path.contains("_wall_") + && face != BlockFace.TOP && face != BlockFace.BOTTOM) { + Block wallHang = Block.fromKey("minecraft:" + path.replace("_hanging_sign", "_wall_hanging_sign")); + if (wallHang != null) result = wallHang.withProperty("facing", oppositeFacing(faceName(face))); + } else if (path.endsWith("_hanging_sign") && !path.contains("_wall_")) { + int rot = (int) Math.floor(((player.getPosition().yaw() + 180.0) * 16.0 / 360.0) + 0.5) & 15; + if (result.properties().containsKey("rotation")) result = result.withProperty("rotation", String.valueOf(rot)); + if (result.properties().containsKey("attached")) result = result.withProperty("attached", face == BlockFace.BOTTOM ? "true" : "false"); + } + + if (isBanner(path)) { + if (face != BlockFace.TOP && face != BlockFace.BOTTOM) { + Block wallBanner = Block.fromKey("minecraft:" + path.replace("_banner", "_wall_banner")); + if (wallBanner != null) result = wallBanner.withProperty("facing", oppositeFacing(faceName(face))); + } else if (face == BlockFace.TOP) { + int rot = (int) Math.floor(((player.getPosition().yaw() + 180.0) * 16.0 / 360.0) + 0.5) & 15; + if (result.properties().containsKey("rotation")) result = result.withProperty("rotation", String.valueOf(rot)); + } + } + + if (path.equals("vine")) { + Block existing = instance.getBlock(pos); + Block base = existing.compare(block) ? existing : result.withProperty("up", "false") + .withProperty("north", "false").withProperty("south", "false") + .withProperty("east", "false").withProperty("west", "false"); + String side = switch (face) { + case BOTTOM -> "up"; + case NORTH -> "south"; + case SOUTH -> "north"; + case EAST -> "west"; + case WEST -> "east"; + default -> null; + }; + result = side != null ? base.withProperty(side, "true") : base; + } + + if (path.equals("glow_lichen") || path.equals("sculk_vein") || path.equals("resin_clump")) { + Block existing = instance.getBlock(pos); + Block base = existing.compare(block) ? existing : result + .withProperty("up", "false").withProperty("down", "false") + .withProperty("north", "false").withProperty("south", "false") + .withProperty("east", "false").withProperty("west", "false"); + String side = switch (face) { + case TOP -> "down"; + case BOTTOM -> "up"; + case NORTH -> "south"; + case SOUTH -> "north"; + case EAST -> "west"; + case WEST -> "east"; + }; + result = base.withProperty(side, "true"); + } + + if (path.equals("red_mushroom_block") || path.equals("brown_mushroom_block") || path.equals("mushroom_stem")) { + result = result.withProperty("up", "true").withProperty("down", "true") + .withProperty("north", "true").withProperty("south", "true") + .withProperty("east", "true").withProperty("west", "true"); + String hiddenFace = faceName(face); + if (result.properties().containsKey(hiddenFace)) result = result.withProperty(hiddenFace, "false"); + } + + if (props.containsKey("facing") && !path.endsWith("_stairs") && !path.endsWith("_door") + && !path.endsWith("_trapdoor") && !path.endsWith("_fence_gate") && !path.endsWith("_button") + && !path.equals("lever") && !path.equals("grindstone") && !path.equals("bell") + && !path.equals("hopper") && !path.equals("campfire") && !path.equals("soul_campfire") + && !path.equals("tripwire_hook") + && !path.endsWith("wall_torch") && !path.endsWith("_bed") && !path.endsWith("_sign") + && !path.endsWith("_hanging_sign") && !path.endsWith("_banner") && !path.endsWith("_wall_banner") + && !path.endsWith("_skull") && !path.endsWith("_head") + && !path.equals("end_portal_frame") && !path.equals("respawn_anchor")) { + Direction fdir = faceDirection(face); + String facing; + if (fdir != null && SIX_DIR.contains(path)) { + facing = fdir.opposite().name().toLowerCase(); + } else if (path.equals("repeater") || path.equals("comparator")) { + facing = oppositeFacing(yawToFacing(player.getPosition().yaw())); + } else { + facing = yawToFacing(player.getPosition().yaw()); + } + try { result = result.withProperty("facing", facing); } catch (Exception ignored) {} + } + + if (isConnecting(path)) result = computeConnections(result, path, instance, pos); + + if (props.containsKey("waterlogged") && Block.WATER.compare(instance.getBlock(pos))) { + try { result = result.withProperty("waterlogged", "true"); } catch (Exception ignored) {} + } + + return result; + } + + private static boolean isBanner(String path) { + return path.endsWith("_banner") && !path.endsWith("_wall_banner"); + } + + private static Block applyStairShape(Block stair, Instance instance, BlockVec pos, String facing, boolean top) { + Direction f = facingToDirection(facing); + Block front = instance.getBlock(new BlockVec(pos.blockX() + f.normalX(), pos.blockY(), pos.blockZ() + f.normalZ())); + if (front.key().value().endsWith("_stairs") && top == isTopStair(front)) { + Direction frontFacing = facingToDirection(front.getProperty("facing")); + if (frontFacing != f && frontFacing != f.opposite()) { + if (frontFacing == rotateCW(f)) return stair.withProperty("shape", "outer_right"); + else return stair.withProperty("shape", "outer_left"); + } + } + Block back = instance.getBlock(new BlockVec(pos.blockX() - f.normalX(), pos.blockY(), pos.blockZ() - f.normalZ())); + if (back.key().value().endsWith("_stairs") && top == isTopStair(back)) { + Direction backFacing = facingToDirection(back.getProperty("facing")); + if (backFacing != f && backFacing != f.opposite()) { + if (backFacing == rotateCW(f)) return stair.withProperty("shape", "inner_right"); + else return stair.withProperty("shape", "inner_left"); + } + } + return stair.withProperty("shape", "straight"); + } + + private static boolean isTopStair(Block b) { return "top".equals(b.getProperty("half")); } + + private static Block computeConnections(Block block, String path, Instance instance, BlockVec pos) { + boolean isWall = path.endsWith("_wall") && !path.endsWith("head_wall") && !path.endsWith("_wall_sign") && !path.endsWith("_wall_banner"); + if (path.endsWith("_fence_gate")) return block; + + Block result = block; + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + boolean connect = canConnectTo(nb, path, d); + String propName = d.name().toLowerCase(); + if (isWall) { + boolean tall = connect && isSolidAbove(instance, pos); + result = result.withProperty(propName, connect ? (tall ? "tall" : "low") : "none"); + } else { + result = result.withProperty(propName, connect ? "true" : "false"); + } + } + if (isWall && result.properties().containsKey("up")) { + boolean up = computeWallUp(result, instance, pos); + result = result.withProperty("up", up ? "true" : "false"); + } + return result; + } + + private static boolean isSolidAbove(Instance instance, BlockVec pos) { + Block above = instance.getBlock(new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ())); + return above.isSolid(); + } + + private static boolean computeWallUp(Block wall, Instance instance, BlockVec pos) { + boolean n = !"none".equals(wall.getProperty("north")); + boolean s = !"none".equals(wall.getProperty("south")); + boolean e = !"none".equals(wall.getProperty("east")); + boolean w = !"none".equals(wall.getProperty("west")); + boolean straight = (n && s && !e && !w) || (!n && !s && e && w); + if (!straight) return true; + return isSolidAbove(instance, pos); + } + + private static boolean canConnectTo(Block neighbor, String selfPath, Direction toward) { + if (neighbor.isAir()) return false; + String np = neighbor.key().value(); + if (np.endsWith("_fence_gate")) { + String facing = neighbor.getProperty("facing"); + if (facing != null) { + Direction f = facingToDirection(facing); + return toward != f && toward != f.opposite(); + } + return true; + } + if (selfPath.endsWith("_fence") && np.endsWith("_fence")) { + boolean selfNether = selfPath.contains("nether_brick"); + boolean nNether = np.contains("nether_brick"); + return selfNether == nNether; + } + if (selfPath.endsWith("_wall") && np.endsWith("_wall")) return true; + if ((selfPath.equals("glass_pane") || selfPath.endsWith("_stained_glass_pane") || selfPath.equals("iron_bars")) + && (np.equals("glass_pane") || np.endsWith("_stained_glass_pane") || np.equals("iron_bars"))) return true; + return neighbor.isSolid(); + } + + private static boolean isConnecting(String path) { + return path.endsWith("_fence") || (path.endsWith("_wall") && !path.endsWith("head_wall") + && !path.endsWith("_wall_sign") && !path.endsWith("_wall_banner") && !path.endsWith("_wall_torch") + && !path.endsWith("_wall_skull") && !path.endsWith("_wall_head") && !path.endsWith("_wall_hanging_sign")) + || path.equals("iron_bars") || path.equals("glass_pane") || path.endsWith("_stained_glass_pane"); + } + + private static Block updateFenceGateInWall(Block gate, Instance instance, BlockVec pos) { + if (!gate.key().value().endsWith("_fence_gate")) return gate; + String facing = gate.getProperty("facing"); + if (facing == null) return gate; + Direction f = facingToDirection(facing); + Direction left = rotateCCW(f); + Direction right = rotateCW(f); + Block l = instance.getBlock(new BlockVec(pos.blockX() + left.normalX(), pos.blockY(), pos.blockZ() + left.normalZ())); + Block r = instance.getBlock(new BlockVec(pos.blockX() + right.normalX(), pos.blockY(), pos.blockZ() + right.normalZ())); + boolean inWall = l.key().value().endsWith("_wall") || r.key().value().endsWith("_wall"); + return gate.withProperty("in_wall", inWall ? "true" : "false"); + } + + private static void updateRedstoneWire(Instance instance, BlockVec pos) { + Block wire = instance.getBlock(pos); + if (!wire.key().value().equals("redstone_wire")) return; + Block result = wire; + for (Direction d : Direction.HORIZONTAL) { + BlockVec side = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block sideBlock = instance.getBlock(side); + Block upBlock = instance.getBlock(new BlockVec(side.blockX(), side.blockY() + 1, side.blockZ())); + String state; + if (upBlock.key().value().equals("redstone_wire")) state = "up"; + else if (isRedstoneConnectable(sideBlock) + || instance.getBlock(new BlockVec(side.blockX(), side.blockY() - 1, side.blockZ())).key().value().equals("redstone_wire")) state = "side"; + else state = "none"; + result = result.withProperty(d.name().toLowerCase(), state); + } + instance.setBlock(pos, result); + } + + private static boolean isRedstoneConnectable(Block b) { + String p = b.key().value(); + return p.equals("redstone_wire") || p.equals("lever") || p.endsWith("_button") + || p.equals("repeater") || p.equals("comparator") || p.equals("redstone_torch") + || p.equals("redstone_wall_torch") || p.equals("observer") || p.equals("daylight_detector") + || p.endsWith("_pressure_plate") || p.equals("target") || p.equals("redstone_block") + || p.equals("tripwire_hook"); + } + + private static boolean isRail(String path) { + return path.equals("rail") || path.equals("powered_rail") || path.equals("detector_rail") || path.equals("activator_rail"); + } + + private static void updateRailShape(Instance instance, BlockVec pos) { + Block rail = instance.getBlock(pos); + String path = rail.key().value(); + if (!isRail(path)) return; + boolean canCurve = path.equals("rail"); + boolean[] adj = new boolean[4]; + boolean[] ascAdj = new boolean[4]; + Direction[] dirs = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + for (int i = 0; i < 4; i++) { + BlockVec np = new BlockVec(pos.blockX() + dirs[i].normalX(), pos.blockY(), pos.blockZ() + dirs[i].normalZ()); + adj[i] = isRail(instance.getBlock(np).key().value()); + BlockVec upNp = new BlockVec(np.blockX(), np.blockY() + 1, np.blockZ()); + ascAdj[i] = isRail(instance.getBlock(upNp).key().value()); + } + String shape; + if (canCurve) { + if (adj[0] && adj[1]) shape = "north_east"; + else if (adj[1] && adj[2]) shape = "south_east"; + else if (adj[2] && adj[3]) shape = "south_west"; + else if (adj[3] && adj[0]) shape = "north_west"; + else if (ascAdj[0]) shape = "ascending_north"; + else if (ascAdj[1]) shape = "ascending_east"; + else if (ascAdj[2]) shape = "ascending_south"; + else if (ascAdj[3]) shape = "ascending_west"; + else if (adj[1] || adj[3]) shape = "east_west"; + else shape = "north_south"; + } else { + if (ascAdj[0]) shape = "ascending_north"; + else if (ascAdj[1]) shape = "ascending_east"; + else if (ascAdj[2]) shape = "ascending_south"; + else if (ascAdj[3]) shape = "ascending_west"; + else if (adj[1] || adj[3]) shape = "east_west"; + else shape = "north_south"; + } + instance.setBlock(pos, rail.withProperty("shape", shape)); + } + + private static void updateScaffolding(Instance instance, BlockVec pos) { + Block s = instance.getBlock(pos); + if (!s.key().value().equals("scaffolding")) return; + Block below = instance.getBlock(new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ())); + boolean bottom; + int distance; + if (below.isSolid() && !below.key().value().equals("scaffolding")) { bottom = false; distance = 0; } + else if (below.key().value().equals("scaffolding")) { bottom = false; distance = 0; } + else { + int min = 7; + for (Direction d : Direction.HORIZONTAL) { + Block n = instance.getBlock(new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ())); + if (n.key().value().equals("scaffolding")) min = Math.min(min, parseIntOr(n.getProperty("distance"), 7) + 1); + } + bottom = true; + distance = min; + } + instance.setBlock(pos, s.withProperty("bottom", String.valueOf(bottom)).withProperty("distance", String.valueOf(distance))); + } + + private static void updateChestPair(Instance instance, BlockVec pos) { + Block chest = instance.getBlock(pos); + String path = chest.key().value(); + if (!(path.equals("chest") || path.equals("trapped_chest"))) return; + String facing = chest.getProperty("facing"); + if (facing == null) return; + Direction f = facingToDirection(facing); + Direction left = rotateCCW(f); + Direction right = rotateCW(f); + BlockVec leftPos = new BlockVec(pos.blockX() + left.normalX(), pos.blockY(), pos.blockZ() + left.normalZ()); + BlockVec rightPos = new BlockVec(pos.blockX() + right.normalX(), pos.blockY(), pos.blockZ() + right.normalZ()); + Block leftBlock = instance.getBlock(leftPos); + Block rightBlock = instance.getBlock(rightPos); + if (leftBlock.key().value().equals(path) && facing.equals(leftBlock.getProperty("facing")) + && "single".equals(leftBlock.getProperty("type"))) { + instance.setBlock(pos, chest.withProperty("type", "right")); + instance.setBlock(leftPos, leftBlock.withProperty("type", "left")); + return; + } + if (rightBlock.key().value().equals(path) && facing.equals(rightBlock.getProperty("facing")) + && "single".equals(rightBlock.getProperty("type"))) { + instance.setBlock(pos, chest.withProperty("type", "left")); + instance.setBlock(rightPos, rightBlock.withProperty("type", "right")); + } + } + + private static String yawToFacing(float yaw) { + float y = ((yaw % 360) + 360) % 360; + if (y >= 315 || y < 45) return "south"; + if (y < 135) return "west"; + if (y < 225) return "north"; + return "east"; + } + + private static String faceName(BlockFace face) { + return switch (face) { + case NORTH -> "north"; + case SOUTH -> "south"; + case EAST -> "east"; + case WEST -> "west"; + case TOP -> "up"; + case BOTTOM -> "down"; + }; + } + + private static String oppositeFacing(String f) { + return switch (f) { + case "north" -> "south"; + case "south" -> "north"; + case "east" -> "west"; + case "west" -> "east"; + case "up" -> "down"; + case "down" -> "up"; + default -> f; + }; + } + + private static Direction faceDirection(BlockFace face) { + return switch (face) { + case NORTH -> Direction.NORTH; + case SOUTH -> Direction.SOUTH; + case EAST -> Direction.EAST; + case WEST -> Direction.WEST; + case TOP -> Direction.UP; + case BOTTOM -> Direction.DOWN; + }; + } + + private static Direction facingToDirection(String f) { + if (f == null) return Direction.NORTH; + return switch (f) { + case "north" -> Direction.NORTH; + case "south" -> Direction.SOUTH; + case "east" -> Direction.EAST; + case "west" -> Direction.WEST; + case "up" -> Direction.UP; + case "down" -> Direction.DOWN; + default -> Direction.NORTH; + }; + } + + private static Direction rotateCW(Direction d) { + return switch (d) { + case NORTH -> Direction.EAST; + case EAST -> Direction.SOUTH; + case SOUTH -> Direction.WEST; + case WEST -> Direction.NORTH; + default -> d; + }; + } + + private static Direction rotateCCW(Direction d) { + return switch (d) { + case NORTH -> Direction.WEST; + case WEST -> Direction.SOUTH; + case SOUTH -> Direction.EAST; + case EAST -> Direction.NORTH; + default -> d; + }; + } + + private static String doorHinge(String facing, Point cursor, Instance instance, BlockVec pos, String path) { + Direction f = facingToDirection(facing); + Direction left = rotateCCW(f); + BlockVec leftPos = new BlockVec(pos.blockX() + left.normalX(), pos.blockY(), pos.blockZ() + left.normalZ()); + Block leftBlock = instance.getBlock(leftPos); + if (leftBlock.key().value().equals(path) && facing.equals(leftBlock.getProperty("facing"))) { + return "right"; + } + double x = cursor.x() - 0.5; + double z = cursor.z() - 0.5; + return switch (facing) { + case "north" -> x < 0 ? "right" : "left"; + case "south" -> x > 0 ? "right" : "left"; + case "west" -> z > 0 ? "right" : "left"; + case "east" -> z < 0 ? "right" : "left"; + default -> "left"; + }; + } + + public static void onInteract(PlayerBlockInteractEvent event) { + Block block = event.getBlock(); + String path = block.key().value(); + Instance instance = event.getInstance(); + BlockVec pos = event.getBlockPosition(); + + if (path.endsWith("_door")) { + String half = block.getProperty("half"); + BlockVec lowerPos = "upper".equals(half) + ? new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()) + : pos; + BlockVec upperPos = new BlockVec(lowerPos.blockX(), lowerPos.blockY() + 1, lowerPos.blockZ()); + Block lower = instance.getBlock(lowerPos); + Block upper = instance.getBlock(upperPos); + if (!lower.key().value().equals(path) || !upper.key().value().equals(path)) return; + boolean open = !"true".equals(lower.getProperty("open")); + instance.setBlock(lowerPos, lower.withProperty("open", String.valueOf(open))); + instance.setBlock(upperPos, upper.withProperty("open", String.valueOf(open))); + + String facing = lower.getProperty("facing"); + String hinge = lower.getProperty("hinge"); + if (facing != null && hinge != null) { + Direction f = facingToDirection(facing); + Direction side = "left".equals(hinge) ? rotateCW(f) : rotateCCW(f); + BlockVec partnerPos = new BlockVec(lowerPos.blockX() + side.normalX(), lowerPos.blockY(), lowerPos.blockZ() + side.normalZ()); + Block partner = instance.getBlock(partnerPos); + if (partner.key().value().equals(path) && facing.equals(partner.getProperty("facing")) + && !hinge.equals(partner.getProperty("hinge"))) { + Block partnerUpper = instance.getBlock(new BlockVec(partnerPos.blockX(), partnerPos.blockY() + 1, partnerPos.blockZ())); + instance.setBlock(partnerPos, partner.withProperty("open", String.valueOf(open))); + if (partnerUpper.key().value().equals(path)) { + instance.setBlock(new BlockVec(partnerPos.blockX(), partnerPos.blockY() + 1, partnerPos.blockZ()), + partnerUpper.withProperty("open", String.valueOf(open))); + } + } + } + event.setBlockingItemUse(true); + return; + } + + if (path.endsWith("_fence_gate")) { + boolean open = !"true".equals(block.getProperty("open")); + Block updated = block.withProperty("open", String.valueOf(open)); + if (open) { + String facing = block.getProperty("facing"); + if (facing != null) { + Direction f = facingToDirection(facing); + Direction playerDir = horizontalFacingToDirection(yawToFacing(event.getPlayer().getPosition().yaw())); + if (playerDir == f) updated = updated.withProperty("facing", oppositeFacing(facing)); + } + } + instance.setBlock(pos, updated); + event.setBlockingItemUse(true); + return; + } + + if (path.endsWith("_trapdoor")) { + boolean open = !"true".equals(block.getProperty("open")); + instance.setBlock(pos, block.withProperty("open", String.valueOf(open))); + event.setBlockingItemUse(true); + } + } + + private static Direction horizontalFacingToDirection(String f) { + return facingToDirection(f); + } + + public static void onBreak(PlayerBlockBreakEvent event) { + Instance instance = event.getInstance(); + BlockVec pos = event.getBlockPosition(); + Block broken = event.getBlock(); + String path = broken.key().value(); + + if (path.endsWith("_door")) { + String half = broken.getProperty("half"); + BlockVec other = "upper".equals(half) + ? new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()) + : new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ()); + instance.scheduler().scheduleNextTick(() -> { + Block ob = instance.getBlock(other); + if (ob.key().value().equals(path)) instance.setBlock(other, Block.AIR); + }); + } + + if (path.endsWith("_bed")) { + String part = broken.getProperty("part"); + String facing = broken.getProperty("facing"); + if (facing != null) { + Direction d = facingToDirection(facing); + if ("foot".equals(part)) { + BlockVec headPos = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + instance.scheduler().scheduleNextTick(() -> { + if (instance.getBlock(headPos).key().value().equals(path)) instance.setBlock(headPos, Block.AIR); + }); + } else if ("head".equals(part)) { + BlockVec footPos = new BlockVec(pos.blockX() - d.normalX(), pos.blockY(), pos.blockZ() - d.normalZ()); + instance.scheduler().scheduleNextTick(() -> { + if (instance.getBlock(footPos).key().value().equals(path)) instance.setBlock(footPos, Block.AIR); + }); + } + } + } + + if (path.equals("big_dripleaf") || path.equals("big_dripleaf_stem")) { + String half = broken.getProperty("half"); + BlockVec other = "upper".equals(half) + ? new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()) + : new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ()); + instance.scheduler().scheduleNextTick(() -> { + String op = instance.getBlock(other).key().value(); + if (op.equals("big_dripleaf") || op.equals("big_dripleaf_stem")) instance.setBlock(other, Block.AIR); + }); + } + + instance.scheduler().scheduleNextTick(() -> { + if (isConnecting(path)) { + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + if (isConnecting(nb.key().value())) instance.setBlock(np, computeConnections(nb, nb.key().value(), instance, np)); + if (nb.key().value().endsWith("_fence_gate")) instance.setBlock(np, updateFenceGateInWall(nb, instance, np)); + } + } + + if (path.endsWith("_wall")) { + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + if (nb.key().value().endsWith("_fence_gate")) instance.setBlock(np, updateFenceGateInWall(nb, instance, np)); + } + } + + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + String npath = nb.key().value(); + if (npath.equals("redstone_wire")) updateRedstoneWire(instance, np); + if (isRail(npath)) updateRailShape(instance, np); + } + BlockVec above = new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ()); + if (instance.getBlock(above).key().value().equals("redstone_wire")) updateRedstoneWire(instance, above); + BlockVec belowRail = new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()); + if (isRail(instance.getBlock(belowRail).key().value())) updateRailShape(instance, belowRail); + + if (path.equals("scaffolding") || isAnyScaffoldingAround(instance, pos)) { + recomputeScaffoldingColumn(instance, pos); + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + if (instance.getBlock(np).key().value().equals("scaffolding")) recomputeScaffoldingColumn(instance, np); + } + } + + if (path.equals("chest") || path.equals("trapped_chest")) { + String facing = broken.getProperty("facing"); + if (facing != null) { + Direction f = facingToDirection(facing); + for (Direction d : new Direction[]{rotateCW(f), rotateCCW(f)}) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + if (nb.key().value().equals(path) && facing.equals(nb.getProperty("facing"))) { + String t = nb.getProperty("type"); + if ("left".equals(t) || "right".equals(t)) instance.setBlock(np, nb.withProperty("type", "single")); + } + } + } + } + + if (isStairLike(path)) { + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = instance.getBlock(np); + if (nb.key().value().endsWith("_stairs")) { + String nf = nb.getProperty("facing"); + boolean nt = isTopStair(nb); + instance.setBlock(np, applyStairShape(nb, instance, np, nf, nt)); + } + } + } + }); + } + + private static boolean isStairLike(String path) { return path.endsWith("_stairs"); } + + private static boolean isAnyScaffoldingAround(Instance instance, BlockVec pos) { + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + if (instance.getBlock(np).key().value().equals("scaffolding")) return true; + } + BlockVec above = new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ()); + return instance.getBlock(above).key().value().equals("scaffolding"); + } + + private static void recomputeScaffoldingColumn(Instance instance, BlockVec start) { + int x = start.blockX(), z = start.blockZ(); + for (int y = start.blockY(); y < start.blockY() + 32; y++) { + BlockVec p = new BlockVec(x, y, z); + if (!instance.getBlock(p).key().value().equals("scaffolding")) break; + updateScaffolding(instance, p); + } + } + + private static int parseIntOr(String s, int d) { + if (s == null) return d; + try { return Integer.parseInt(s); } catch (NumberFormatException e) { return d; } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java index 8145ad8..03c6db2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java @@ -11,8 +11,10 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.event.player.PlayerUseItemEvent; +import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.timer.TaskSchedule; @@ -61,6 +63,10 @@ public class BuildBattle extends StatelessGame { this.seconds = seconds; this.setGenerator(new BuildBattleChunkgenerator(height)); this.eventNode().addListener(PlayerUseItemEvent.class, this::onRatingItemUse); + this.eventNode().addListener(PlayerUseItemOnBlockEvent.class, e -> FluidFlowHelper.onBucketUse(e, p -> this.isInArea(p, e.getPlayer()))); + this.eventNode().addListener(PlayerBlockInteractEvent.class, e -> { + if (this.isInArea(e.getBlockPosition(), e.getPlayer())) BlockPlacementHelper.onInteract(e); + }); } @Override @@ -76,6 +82,21 @@ public class BuildBattle extends StatelessGame { this.getPlayers().forEach(player -> player.setGameMode(GameMode.CREATIVE)); this.scheduler().scheduleTask(this::endBuildPhase, TaskSchedule.seconds(this.seconds), TaskSchedule.stop()); + this.scheduleCountdown(); + } + + private void scheduleCountdown() { + int[] marks = {30, 10, 5, 4, 3, 2, 1}; + for (int m : marks) { + if (this.seconds <= m) continue; + int remaining = m; + this.scheduler().scheduleTask(() -> { + NamedTextColor color = remaining <= 5 ? NamedTextColor.RED : (remaining <= 10 ? NamedTextColor.GOLD : NamedTextColor.YELLOW); + this.sendMessage(Component.text("Noch ", NamedTextColor.GRAY) + .append(Component.text(remaining, color)) + .append(Component.text(" Sekunde" + (remaining == 1 ? "" : "n") + " Bauzeit!", NamedTextColor.GRAY))); + }, TaskSchedule.seconds(this.seconds - m), TaskSchedule.stop()); + } } private void endBuildPhase() { @@ -109,12 +130,34 @@ public class BuildBattle extends StatelessGame { @Override protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) { - event.setCancelled(!this.isInArea(event.getBlockPosition(), event.getPlayer())); + if (!this.isInArea(event.getBlockPosition(), event.getPlayer())) { + event.setCancelled(true); + return; + } + BlockPlacementHelper.onBreak(event); + FluidFlowHelper.onBreak(event); } @Override protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent event) { - event.setCancelled(!this.isInArea(event.getBlockPosition(), event.getPlayer())); + if (!this.isInArea(event.getBlockPosition(), event.getPlayer())) { + event.setCancelled(true); + return; + } + BlockPlacementHelper.apply(event); + FluidFlowHelper.onPlace(event); + } + + private Pos safeSpawn(Player owner) { + Pos center = this.spawnPositions.get(owner); + int topY = center.blockY(); + for (int y = 300; y >= center.blockY(); y--) { + if (!this.getBlock(center.blockX(), y, center.blockZ()).isAir()) { + topY = y; + break; + } + } + return new Pos(center.x(), topY + 3, center.z()).withView(center.yaw(), center.pitch()); } private void rating() { @@ -136,7 +179,7 @@ public class BuildBattle extends StatelessGame { this.currentOwner = owner; this.currentVotes.clear(); - Pos plot = this.spawnPositions.get(owner).add(0, 1.5, 0); + Pos plot = this.safeSpawn(owner); this.sendMessage(Component.text("Bewerte das Grundstück von " + owner.getUsername() + "!")); this.getPlayers().forEach(p -> { p.teleport(plot); @@ -176,11 +219,9 @@ public class BuildBattle extends StatelessGame { private boolean isInArea(Point block, Player player) { Pos center = this.spawnPositions.get(player); - double dx = block.x() - center.x(); - double dy = block.y() - center.y(); - double dz = block.z() - center.z(); - return dx >= -plotHalf && dx < plotHalf - && dy >= 1 && dy <= 300 - && dz >= -plotHalf && dz < plotHalf; + if (center == null) return false; + Pos min = new Pos(center.x() - plotHalf, center.y() + 1, center.z() - plotHalf); + Pos max = new Pos(center.x() + plotHalf - 1, 300, center.z() + plotHalf - 1); + return eu.mhsl.minenet.minigames.util.Position.isInArea(block, min, max); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java new file mode 100644 index 0000000..598cbc6 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java @@ -0,0 +1,268 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import net.minestom.server.coordinate.BlockVec; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.Material; +import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.utils.Direction; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +public final class FluidFlowHelper { + private FluidFlowHelper() {} + + private static final int WATER_TICK = 5; + private static final int LAVA_TICK = 30; + private static final int WATER_RANGE = 7; + private static final int LAVA_RANGE = 3; + private static final int FALLING = 8; + + public static void onBucketUse(PlayerUseItemOnBlockEvent event, Predicate inArea) { + Material m = event.getItemStack().material(); + Instance inst = event.getPlayer().getInstance(); + if (inst == null) return; + Point hit = event.getPosition(); + var face = event.getBlockFace(); + BlockVec placePos = new BlockVec( + hit.blockX() + face.toDirection().normalX(), + hit.blockY() + face.toDirection().normalY(), + hit.blockZ() + face.toDirection().normalZ() + ); + if (!inArea.test(placePos)) return; + + if (m == Material.WATER_BUCKET) { + inst.setBlock(placePos, Block.WATER.withProperty("level", "0")); + scheduleFlow(inst, placePos, true); + } else if (m == Material.LAVA_BUCKET) { + inst.setBlock(placePos, Block.LAVA.withProperty("level", "0")); + scheduleFlow(inst, placePos, false); + } else if (m == Material.BUCKET) { + Block existing = inst.getBlock(placePos); + if (isWater(existing) || isLava(existing)) inst.setBlock(placePos, Block.AIR); + } + } + + public static void onPlace(PlayerBlockPlaceEvent event) { + Block b = event.getBlock(); + if (isWater(b) || isLava(b)) { + Instance inst = event.getInstance(); + BlockVec pos = event.getBlockPosition(); + scheduleFlow(inst, pos, isWater(b)); + } + triggerNeighbors(event.getInstance(), event.getBlockPosition()); + } + + public static void onBreak(PlayerBlockBreakEvent event) { + triggerNeighbors(event.getInstance(), event.getBlockPosition()); + } + + private static void triggerNeighbors(Instance inst, BlockVec pos) { + for (Direction d : Direction.values()) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY() + d.normalY(), pos.blockZ() + d.normalZ()); + Block nb = inst.getBlock(np); + if (isWater(nb) || isLava(nb)) scheduleFlow(inst, np, isWater(nb)); + } + } + + private static void scheduleFlow(Instance inst, BlockVec pos, boolean water) { + int delay = water ? WATER_TICK : LAVA_TICK; + inst.scheduler().buildTask(() -> step(inst, pos, water)).delay(TaskSchedule.tick(delay)).schedule(); + } + + private static void step(Instance inst, BlockVec pos, boolean water) { + Block self = inst.getBlock(pos); + if (water ? !isWater(self) : !isLava(self)) return; + + int level = parseLevel(self); + boolean source = level == 0; + boolean falling = level >= FALLING; + int flowLevel = source ? 1 : (falling ? 1 : level + 1); + int maxRange = water ? WATER_RANGE : LAVA_RANGE; + + if (!source && !falling) { + int newLevel = computeIncomingLevel(inst, pos, water, maxRange); + if (newLevel == -1) { + inst.setBlock(pos, Block.AIR); + triggerNeighbors(inst, pos); + return; + } + if (newLevel != level) { + inst.setBlock(pos, (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(newLevel))); + level = newLevel; + flowLevel = level + 1; + } + } + + if (water && !source && countAdjacentSources(inst, pos, true) >= 2 + && hasSolidBelow(inst, pos)) { + inst.setBlock(pos, Block.WATER.withProperty("level", "0")); + scheduleFlow(inst, pos, true); + triggerNeighbors(inst, pos); + return; + } + + BlockVec below = new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()); + Block belowBlock = inst.getBlock(below); + if (isReplaceable(belowBlock) || (water ? isWater(belowBlock) : isLava(belowBlock))) { + if (handleFluidCollision(inst, below, water)) { + triggerNeighbors(inst, below); + return; + } + Block target = (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(FALLING)); + if (!belowBlock.compare(target) || !target.getProperty("level").equals(belowBlock.getProperty("level"))) { + if (!(isWater(belowBlock) && parseLevel(belowBlock) == 0) + && !(isLava(belowBlock) && parseLevel(belowBlock) == 0)) { + inst.setBlock(below, target); + scheduleFlow(inst, below, water); + triggerNeighbors(inst, below); + } + } + if (!hasSolidBelow(inst, pos) && !source) return; + } + + if (flowLevel > maxRange) return; + + Set targets = findFlowTargets(inst, pos, water, maxRange - (source ? 0 : level)); + for (Direction d : Direction.HORIZONTAL) { + if (!targets.contains(d)) continue; + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = inst.getBlock(np); + if (handleFluidCollision(inst, np, water)) continue; + if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) continue; + int existingLevel = (water ? isWater(nb) : isLava(nb)) ? parseLevel(nb) : Integer.MAX_VALUE; + if (existingLevel == 0) continue; + if (flowLevel < existingLevel) { + inst.setBlock(np, (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(flowLevel))); + scheduleFlow(inst, np, water); + triggerNeighbors(inst, np); + } + } + } + + private static int computeIncomingLevel(Instance inst, BlockVec pos, boolean water, int maxRange) { + int min = Integer.MAX_VALUE; + BlockVec above = new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ()); + Block ab = inst.getBlock(above); + if ((water ? isWater(ab) : isLava(ab))) return FALLING; + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = inst.getBlock(np); + if (water ? isWater(nb) : isLava(nb)) { + int nl = parseLevel(nb); + if (nl >= FALLING) nl = 0; + if (nl < min) min = nl; + } + } + if (min == Integer.MAX_VALUE) return -1; + int result = min + 1; + if (result > maxRange) return -1; + return result; + } + + private static Set findFlowTargets(Instance inst, BlockVec pos, boolean water, int remaining) { + Set result = new HashSet<>(); + if (remaining <= 0) return result; + int bestDist = Integer.MAX_VALUE; + int[] dist = new int[4]; + Direction[] dirs = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + for (int i = 0; i < 4; i++) { + BlockVec np = new BlockVec(pos.blockX() + dirs[i].normalX(), pos.blockY(), pos.blockZ() + dirs[i].normalZ()); + Block nb = inst.getBlock(np); + if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) { dist[i] = Integer.MAX_VALUE; continue; } + dist[i] = bfsDownhill(inst, np, water, remaining); + if (dist[i] < bestDist) bestDist = dist[i]; + } + if (bestDist == Integer.MAX_VALUE) { + for (int i = 0; i < 4; i++) { + if (dist[i] < Integer.MAX_VALUE) result.add(dirs[i]); + } + } else { + for (int i = 0; i < 4; i++) { + if (dist[i] == bestDist) result.add(dirs[i]); + } + } + return result; + } + + private static int bfsDownhill(Instance inst, BlockVec start, boolean water, int remaining) { + ArrayDeque queue = new ArrayDeque<>(); + Set visited = new HashSet<>(); + queue.add(new int[]{start.blockX(), start.blockY(), start.blockZ(), 0}); + visited.add(key(start.blockX(), start.blockY(), start.blockZ())); + while (!queue.isEmpty()) { + int[] c = queue.poll(); + BlockVec cp = new BlockVec(c[0], c[1], c[2]); + Block below = inst.getBlock(new BlockVec(c[0], c[1] - 1, c[2])); + if (isReplaceable(below) && !(water ? isWater(below) : isLava(below))) return c[3]; + if (c[3] >= remaining) continue; + for (Direction d : Direction.HORIZONTAL) { + int nx = c[0] + d.normalX(), nz = c[2] + d.normalZ(); + long k = key(nx, c[1], nz); + if (visited.contains(k)) continue; + Block nb = inst.getBlock(new BlockVec(nx, c[1], nz)); + if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) continue; + visited.add(k); + queue.add(new int[]{nx, c[1], nz, c[3] + 1}); + } + } + return Integer.MAX_VALUE; + } + + private static long key(int x, int y, int z) { + return ((long) x & 0x3FFFFFFL) | (((long) z & 0x3FFFFFFL) << 26) | (((long) y & 0xFFFL) << 52); + } + + private static int countAdjacentSources(Instance inst, BlockVec pos, boolean water) { + int c = 0; + for (Direction d : Direction.HORIZONTAL) { + Block nb = inst.getBlock(new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ())); + if ((water ? isWater(nb) : isLava(nb)) && parseLevel(nb) == 0) c++; + } + return c; + } + + private static boolean hasSolidBelow(Instance inst, BlockVec pos) { + Block below = inst.getBlock(new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ())); + return below.isSolid(); + } + + private static boolean handleFluidCollision(Instance inst, BlockVec pos, boolean incomingIsWater) { + Block target = inst.getBlock(pos); + if (incomingIsWater && isLava(target)) { + boolean lavaSource = parseLevel(target) == 0; + inst.setBlock(pos, lavaSource ? Block.OBSIDIAN : Block.COBBLESTONE); + return true; + } + if (!incomingIsWater && isWater(target)) { + inst.setBlock(pos, Block.STONE); + return true; + } + return false; + } + + private static boolean isReplaceable(Block b) { + if (b.isAir()) return true; + String p = b.key().value(); + return p.equals("short_grass") || p.equals("tall_grass") || p.equals("fern") || p.equals("large_fern") + || p.equals("dead_bush") || p.equals("seagrass") || p.equals("tall_seagrass") || p.equals("vine") + || p.equals("snow") || p.equals("fire") || p.equals("soul_fire"); + } + + private static boolean isWater(Block b) { return b.compare(Block.WATER); } + private static boolean isLava(Block b) { return b.compare(Block.LAVA); } + + private static int parseLevel(Block b) { + String s = b.getProperty("level"); + if (s == null) return 0; + try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/Position.java b/src/main/java/eu/mhsl/minenet/minigames/util/Position.java index bb5dc2e..e9d0d50 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/Position.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/Position.java @@ -44,4 +44,17 @@ public class Position { public static boolean hasPositionChanged(Pos oldPos, Pos newPos) { return !oldPos.withView(0, 0).equals(newPos.withView(0, 0)); } + + public static boolean isInArea(Point point, Point pos1, Point pos2) { + double minX = Math.min(pos1.x(), pos2.x()); + double maxX = Math.max(pos1.x(), pos2.x()); + double minY = Math.min(pos1.y(), pos2.y()); + double maxY = Math.max(pos1.y(), pos2.y()); + double minZ = Math.min(pos1.z(), pos2.z()); + double maxZ = Math.max(pos1.z(), pos2.z()); + + return point.x() >= minX && point.x() <= maxX + && point.y() >= minY && point.y() <= maxY + && point.z() >= minZ && point.z() <= maxZ; + } }