diff --git a/build.gradle b/build.gradle index a216060..9a890d3 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ version '1.0-SNAPSHOT' repositories { mavenCentral() + maven { url = 'https://mvn.everbuild.org/public' } } java { @@ -38,6 +39,9 @@ dependencies { //PvP implementation 'io.github.togar2:MinestomPvP:2026.05.30-26.1.1' + + //Block placement + implementation 'org.everbuild.blocksandstuff:blocksandstuff-blocks:1.10.2-SNAPSHOT' } tasks { 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..ccb2a25 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; @@ -60,7 +62,12 @@ public class BuildBattle extends StatelessGame { super(Dimension.THE_END.key, "BuildBattle", new PointsWinScore()); this.seconds = seconds; this.setGenerator(new BuildBattleChunkgenerator(height)); + BuildBattleBlockRules.register(); 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())) BuildBattleBlockRules.onInteract(e); + }); } @Override @@ -76,6 +83,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 +131,32 @@ 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; + } + 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; + } + 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 +178,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 +218,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/BuildBattleBlockRules.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleBlockRules.java new file mode 100644 index 0000000..5ab090c --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleBlockRules.java @@ -0,0 +1,85 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.instance.block.rule.BlockPlacementRule; +import org.everbuild.blocksandstuff.blocks.group.VanillaBlockBehaviour; +import org.everbuild.blocksandstuff.blocks.group.VanillaPlacementRules; +import org.everbuild.blocksandstuff.blocks.group.behaviour.BehaviourGroup; +import org.everbuild.blocksandstuff.blocks.group.placement.PlacementGroup; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public final class BuildBattleBlockRules { + private BuildBattleBlockRules() {} + + private static final ThreadLocal interactingInBuildBattle = ThreadLocal.withInitial(() -> false); + private static final Map behaviours = new HashMap<>(); + private static boolean registered = false; + + public static synchronized void register() { + if (registered) return; + registered = true; + + var blockManager = MinecraftServer.getBlockManager(); + for (PlacementGroup group : VanillaPlacementRules.INSTANCE.getALL()) { + for (Block block : group.getBlockGroup().allMatching()) { + blockManager.registerBlockPlacementRule(new BuildBattleOnlyRule(group.createRule(block))); + } + } + + for (BehaviourGroup group : VanillaBlockBehaviour.INSTANCE.getALL()) { + for (Block block : group.getBlockGroup().allMatching()) { + behaviours.put(block.id(), group.createHandler(block)); + } + } + + MinecraftServer.getGlobalEventHandler().addListener(PlayerBlockInteractEvent.class, + e -> interactingInBuildBattle.set(e.getInstance() instanceof BuildBattle)); + } + + public static void onInteract(PlayerBlockInteractEvent event) { + BlockHandler handler = behaviours.get(event.getBlock().id()); + if (handler == null) return; + boolean continueItemUse = handler.onInteract(new BlockHandler.Interaction( + event.getBlock(), event.getInstance(), event.getBlockFace(), event.getBlockPosition(), + event.getCursorPosition(), event.getPlayer(), event.getHand() + )); + if (!continueItemUse) event.setBlockingItemUse(true); + } + + private static final class BuildBattleOnlyRule extends BlockPlacementRule { + private final BlockPlacementRule delegate; + + private BuildBattleOnlyRule(BlockPlacementRule delegate) { + super(delegate.getBlock()); + this.delegate = delegate; + } + + @Override + public @Nullable Block blockPlace(PlacementState placementState) { + if (!(placementState.instance() instanceof BuildBattle)) return placementState.block(); + return this.delegate.blockPlace(placementState); + } + + @Override + public Block blockUpdate(UpdateState updateState) { + if (!(updateState.instance() instanceof BuildBattle)) return updateState.currentBlock(); + return this.delegate.blockUpdate(updateState); + } + + @Override + public boolean isSelfReplaceable(Replacement replacement) { + return interactingInBuildBattle.get() && this.delegate.isSelfReplaceable(replacement); + } + + @Override + public int maxUpdateDistance() { + return this.delegate.maxUpdateDistance(); + } + } +} 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..2e80a38 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java @@ -0,0 +1,267 @@ +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.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; + } }