From ef1662b144e33f4effbd54d84a9072c674968dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 8 Oct 2026 00:17:00 +0200 Subject: [PATCH] Add bridge and rescue totem jokers and fix dark block displays during the transition --- .../endlessjumper/EndlessJumper.java | 37 +++- .../mhsl/minecraft/endlessjumper/Jokers.java | 80 ++++++++ .../generator/BlockTransition.java | 2 + .../generator/JumpGenerator.java | 181 +++++++++++++++--- 4 files changed, 272 insertions(+), 28 deletions(-) create mode 100644 src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java diff --git a/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java b/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java index 1ba9bc3..3c1e56c 100644 --- a/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java @@ -7,11 +7,17 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; import net.minestom.server.entity.PlayerSkin; import net.minestom.server.event.GlobalEventHandler; +import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.event.player.PlayerSpawnEvent; +import net.minestom.server.event.player.PlayerSwapItemEvent; +import net.minestom.server.event.player.PlayerUseItemEvent; +import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; +import net.minestom.server.item.ItemStack; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; @@ -21,6 +27,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; +import java.util.Map; public class EndlessJumper { public static void main(String[] args) throws IOException { @@ -65,8 +72,7 @@ public class EndlessJumper { JumpGenerator generator = games.get(p); - //reset when falling down - if(p.getPosition().y() < generator.getNextLanding().y()-5 && p.getPosition().y() < generator.getCheckpoint().y()) generator.reset(); + generator.checkFall(); Pos p1 = BlockMath.roundToBlock(event.getNewPosition().sub(0.0,1.0,0.0)); Pos p2 = BlockMath.roundToBlock(event.getNewPosition().sub(0.0, 2.0, 0.0)); @@ -81,14 +87,37 @@ public class EndlessJumper { //if(p.getPosition().z() > target.z()+2) generator.highlight(); }); - eventHandler.addListener(PlayerBlockBreakEvent.class, event -> event.setCancelled(true)); + // nur Brücken- und Rettungsblöcke dürfen abgebaut werden (Notausgang), Sprungblöcke nie + eventHandler.addListener(PlayerBlockBreakEvent.class, event -> { + JumpGenerator generator = games.get(event.getPlayer()); + if(generator == null || !generator.isBreakable(new Pos(event.getBlockPosition()))) event.setCancelled(true); + }); + eventHandler.addListener(PlayerBlockPlaceEvent.class, event -> event.setCancelled(true)); + eventHandler.addListener(ItemDropEvent.class, event -> event.setCancelled(true)); + eventHandler.addListener(PlayerSwapItemEvent.class, event -> event.setCancelled(true)); + + // Rechtsklick auf Luft und auf Blöcke lösen beide aus; kurze Sperre gegen doppelte Nutzung + Map lastJokerUse = new HashMap<>(); + java.util.function.BiConsumer useJoker = (p, item) -> { + JumpGenerator generator = games.get(p); + if(generator == null || !Jokers.isBridge(item)) return; + long now = System.currentTimeMillis(); + if(now - lastJokerUse.getOrDefault(p, 0L) < 500) return; + lastJokerUse.put(p, now); + if(generator.useBridge()) Jokers.takeBridge(p); + }; + eventHandler.addListener(PlayerUseItemEvent.class, event -> useJoker.accept(event.getPlayer(), event.getItemStack())); + eventHandler.addListener(PlayerUseItemOnBlockEvent.class, event -> useJoker.accept(event.getPlayer(), event.getItemStack())); eventHandler.addListener(PlayerSpawnEvent.class, event -> { - if(event.isFirstSpawn()) ScoreTab.join(event.getPlayer()); + if(!event.isFirstSpawn()) return; + ScoreTab.join(event.getPlayer()); + Jokers.giveStart(event.getPlayer()); }); eventHandler.addListener(PlayerDisconnectEvent.class, event -> { games.remove(event.getPlayer()); + lastJokerUse.remove(event.getPlayer()); ScoreTab.leave(event.getPlayer()); }); diff --git a/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java b/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java new file mode 100644 index 0000000..8246f0b --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java @@ -0,0 +1,80 @@ +package eu.mhsl.minecraft.endlessjumper; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; + +/** + * Joker-Items in der Hotbar. Die Anzahl ist die Stapelgröße. + * Brücke: Rechtsklick baut einen Weg bis zum nächsten Checkpoint. + * Totem: rettet automatisch beim Runterfallen (Plattform mit Leiter). + */ +public class Jokers { + public static final int BRIDGE_SLOT = 0; + public static final int TOTEM_SLOT = 8; + + private static final Material BRIDGE = Material.BRICK; + private static final Material TOTEM = Material.TOTEM_OF_UNDYING; + + public static void giveStart(Player player) { + player.getInventory().setItemStack(BRIDGE_SLOT, bridge(1)); + player.getInventory().setItemStack(TOTEM_SLOT, totem(1)); + } + + public static boolean isBridge(ItemStack item) { + return item.material() == BRIDGE; + } + + public static void addBridge(Player player) { + add(player, BRIDGE_SLOT, bridge(1)); + player.sendMessage(Component.text("+1 Brücke", NamedTextColor.GOLD)); + } + + public static void addTotem(Player player) { + add(player, TOTEM_SLOT, totem(1)); + player.sendMessage(Component.text("+1 Rettungstotem", NamedTextColor.GOLD)); + } + + public static boolean takeBridge(Player player) { + return take(player, BRIDGE_SLOT, BRIDGE); + } + + public static boolean takeTotem(Player player) { + return take(player, TOTEM_SLOT, TOTEM); + } + + private static void add(Player player, int slot, ItemStack item) { + ItemStack current = player.getInventory().getItemStack(slot); + if(current.material() == item.material()) { + player.getInventory().setItemStack(slot, current.withAmount(current.amount() + 1)); + } else { + player.getInventory().setItemStack(slot, item); + } + } + + private static boolean take(Player player, int slot, Material material) { + ItemStack current = player.getInventory().getItemStack(slot); + if(current.material() != material || current.amount() <= 0) return false; + player.getInventory().setItemStack(slot, current.amount() > 1 ? current.withAmount(current.amount() - 1) : ItemStack.AIR); + return true; + } + + private static ItemStack bridge(int amount) { + return ItemStack.builder(BRIDGE) + .amount(amount) + .customName(Component.text("Brücke", NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false)) + .lore(Component.text("Rechtsklick: Weg bis zum nächsten Checkpoint", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false)) + .build(); + } + + private static ItemStack totem(int amount) { + return ItemStack.builder(TOTEM) + .amount(amount) + .customName(Component.text("Rettungstotem", NamedTextColor.GOLD).decoration(TextDecoration.ITALIC, false)) + .lore(Component.text("Rettet dich automatisch beim Runterfallen", NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false)) + .build(); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/BlockTransition.java b/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/BlockTransition.java index 893ee45..3ea5072 100644 --- a/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/BlockTransition.java +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/BlockTransition.java @@ -30,6 +30,8 @@ public class BlockTransition { display.setNoGravity(true); display.editEntityMeta(BlockDisplayMeta.class, meta -> { meta.setBlockState(block); + // feste Helligkeit: das Display startet im vorherigen Block, dort ist Lichtstufe 0 (sonst schwarz) + meta.setBrightness(15, 15); meta.setPosRotInterpolationDuration(DURATION_TICKS); meta.setTransformationInterpolationDuration(DURATION_TICKS); // klein und mittig auf der Oberseite des vorherigen Blocks starten diff --git a/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java b/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java index a9f5389..cc61221 100644 --- a/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java @@ -4,16 +4,21 @@ import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.title.Title; +import eu.mhsl.minecraft.endlessjumper.Jokers; import eu.mhsl.minecraft.endlessjumper.ScoreTab; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Player; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.block.Block; import net.minestom.server.network.packet.server.play.ParticlePacket; import net.minestom.server.particle.Particle; import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.Random; public class JumpGenerator { @@ -31,6 +36,16 @@ public class JumpGenerator { private static final int FULL_DIFFICULTY_AT = 1000; + private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS; + private static final int BRIDGE_STEP_TICKS = 4; + private static final Block RESCUE_BLOCK = Block.OAK_PLANKS; + private static final int RESCUE_DEPTH = 6; + + // Rettung durch das Totem: Plattform mit Leiter unter dem Block, von dem man gefallen ist + private final Map rescueBlocks = new HashMap<>(); + private Pos rescueSafe; + private int rescueFloorY; + public JumpGenerator(Pos starting, Player player, InstanceContainer world) { this.player = player; @@ -40,13 +55,7 @@ public class JumpGenerator { } public void generateNext() { - this.generateNext(0); - } - - private void generateNext(int animationDelayTicks) { - boolean isTenth = this.blocks.size() % 10 == 0; boolean standingTenth = (this.blocks.size()-this.preview-1) % 10 == 0; - boolean isHundredth = this.blocks.size() % 100 == 0; float pitch = 1 + (combo * 0.025f); this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, pitch)); @@ -55,21 +64,13 @@ public class JumpGenerator { this.player.setLevel(score); ScoreTab.update(this.player, score); - Random r = new Random(); - int index = this.blocks.size(); - Pos previous = blocks.get(blocks.size()-1); - Pos target = previous.add(this.nextOffset(index, r)); - Block block = isHundredth ? Block.DIAMOND_BLOCK : isTenth ? Block.GOLD_BLOCK : BlockPalette.getRandom(index, r); - this.lastBlock = block; - BlockTransition.animate(this.world, previous, target, block, animationDelayTicks, () -> { - this.world.setBlock(target, block); - this.world.setBlock(target.add(0, 1, 0), Block.LIGHT); - this.player.sendPacket(new ParticlePacket(Particle.CRIT, target, target.add(0.5), 1, 10)); - this.player.sendPacket(new ParticlePacket(Particle.FALLING_NECTAR, target, target.add(0.5), 0.3f, 10)); - }); + if(score > 0 && score % 100 == 0) Jokers.addBridge(this.player); + if(score > 0 && score % 200 == 0) Jokers.addTotem(this.player); + + this.generateBlock(0); if(standingTenth) { - Pos current = this.blocks.get(this.blocks.size()-this.preview-1); + Pos current = this.blocks.get(this.blocks.size()-this.preview-2); this.checkpoint = current; this.player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.BLOCK, 0.3f, 1f)); this.player.showTitle(Title.title(Component.text(""), Component.text("Checkpoint", TextColor.color(0, 255, 0)))); @@ -77,18 +78,33 @@ public class JumpGenerator { } // Abschnitt dauerhaft in der Actionbar (Titel werden von der Combo-Anzeige überschrieben) - int standingIndex = Math.max(0, this.blocks.size()-this.preview-1); - BlockPalette.Stage reachedStage = BlockPalette.stageStartingAt(standingIndex); + BlockPalette.Stage reachedStage = BlockPalette.stageStartingAt(score); if(reachedStage != null && reachedStage.from() > 0) { this.player.playSound(Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.4f, 1f)); this.player.sendActionBar(Component.text(reachedStage.name(), TextColor.color(255, 170, 0))); } else { - this.player.sendActionBar(Component.text(BlockPalette.stageAt(standingIndex).name(), TextColor.color(170, 170, 170))); + this.player.sendActionBar(Component.text(BlockPalette.stageAt(score).name(), TextColor.color(170, 170, 170))); } - this.blocks.add(target); this.combo++; } + /** Erzeugt den nächsten Sprungblock ohne Rückmeldung an den Spieler (Combo, Titel, Sounds). */ + private void generateBlock(int animationDelayTicks) { + int index = this.blocks.size(); + Random r = new Random(); + Pos previous = blocks.getLast(); + Pos target = previous.add(this.nextOffset(index, r)); + Block block = index % 100 == 0 ? Block.DIAMOND_BLOCK : index % 10 == 0 ? Block.GOLD_BLOCK : BlockPalette.getRandom(index, r); + this.lastBlock = block; + BlockTransition.animate(this.world, previous, target, block, animationDelayTicks, () -> { + this.world.setBlock(target, block); + this.world.setBlock(target.add(0, 1, 0), Block.LIGHT); + this.player.sendPacket(new ParticlePacket(Particle.CRIT, target, target.add(0.5), 1, 10)); + this.player.sendPacket(new ParticlePacket(Particle.FALLING_NECTAR, target, target.add(0.5), 0.3f, 10)); + }); + this.blocks.add(target); + } + /** * Abstand zum nächsten Block. Sprünge sind immer mindestens 3 weit (natürliche Sprint-Weite, * kürzere werden überjumpt). Über die ersten {@link #FULL_DIFFICULTY_AT} Sprünge steigen der @@ -118,6 +134,123 @@ public class JumpGenerator { return new Pos(dx, dy, dz); } + /** + * Joker „Brücke“: erzeugt die Sprungblöcke bis zum nächsten Checkpoint und verbindet sie + * mit einem begehbaren Weg. Die übersprungenen Blöcke zählen nicht zur Combo. + */ + public boolean useBridge() { + if(this.rescueSafe != null) return false; + + int standing = this.blocks.size() - this.preview - 2; + int nextCheckpoint = (standing / 10 + 1) * 10; + + // so weit vorgenerieren, dass der Checkpoint der nächste Landeblock ist + int delay = 0; + while(this.blocks.size() - this.preview - 1 < nextCheckpoint) { + this.generateBlock(delay); + delay += BRIDGE_STEP_TICKS; + } + + for(int i = standing; i < nextCheckpoint; i++) { + Pos from = this.blocks.get(i); + Pos to = this.blocks.get(i + 1); + this.world.scheduler().buildTask(() -> this.buildPath(from, to)) + .delay(TaskSchedule.tick(1 + (i - standing) * BRIDGE_STEP_TICKS)) + .schedule(); + } + + this.player.playSound(Sound.sound(SoundEvent.BLOCK_AMETHYST_BLOCK_CHIME, Sound.Source.MASTER, 1f, 1f)); + return true; + } + + /** Weg auf Höhe des Startblocks; bei seitlichem Versatz zwei Blöcke breit, damit er durchgehend ist. */ + private void buildPath(Pos from, Pos to) { + int minX = Math.min(from.blockX(), to.blockX()); + int maxX = Math.max(from.blockX(), to.blockX()); + for(int z = from.blockZ() + 1; z < to.blockZ(); z++) { + for(int x = minX; x <= maxX; x++) { + Pos pos = new Pos(x, from.blockY(), z); + if(this.world.getBlock(pos).compare(Block.AIR)) this.world.setBlock(pos, BRIDGE_BLOCK); + } + } + } + + /** Wird bei jeder Bewegung aufgerufen: Rettung durch Totem oder Reset beim Runterfallen. */ + public void checkFall() { + double y = this.player.getPosition().y(); + + if(this.rescueSafe != null) { + if(y >= this.rescueSafe.y() + 0.9) this.endRescue(); + else if(y < this.rescueFloorY - 3) { + this.endRescue(); + this.reset(); + } + return; + } + + if(y < this.getNextLanding().y() - 5 && y < this.checkpoint.y()) { + if(Jokers.takeTotem(this.player)) this.rescue(); + else this.reset(); + } + } + + /** + * Joker „Rettungstotem“: Plattform unter dem zuletzt erreichten Block, davor eine Stützsäule + * mit Leiter, über die man direkt wieder auf den Block klettert. + */ + private void rescue() { + Pos safe = this.blocks.get(Math.max(0, this.blocks.size() - this.preview - 2)); + int x = safe.blockX(); + int z = safe.blockZ(); + this.rescueSafe = safe; + this.rescueFloorY = safe.blockY() - RESCUE_DEPTH; + + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -3; dz <= -1; dz++) this.placeRescue(new Pos(x + dx, this.rescueFloorY, z + dz), RESCUE_BLOCK); + } + // Säule direkt hinter dem Block bis auf seine Höhe, Leiter davor (hängt an der Säule) + for(int y = this.rescueFloorY + 1; y <= safe.blockY(); y++) { + this.placeRescue(new Pos(x, y, z - 1), RESCUE_BLOCK); + this.placeRescue(new Pos(x, y, z - 2), Block.LADDER.withProperty("facing", "north")); + } + + this.player.setVelocity(Vec.ZERO); + this.player.teleport(new Pos(x + 0.5, this.rescueFloorY + 1, z - 1.5, 0, 0)); + this.player.triggerStatus((byte) 35); + this.player.playSound(Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.MASTER, 0.6f, 1f)); + } + + /** + * Setzt einen Rettungsblock. Brückenglas darf überschrieben werden, sonst könnte ein früherer + * Brückenweg die Leiter von oben versperren. Sprungblöcke liegen nie im Rettungsbereich, da + * Sprünge immer mindestens 3 Blöcke nach vorne gehen und die Rettung nur dahinter baut. + */ + private void placeRescue(Pos pos, Block block) { + Block existing = this.world.getBlock(pos); + if(!existing.compare(Block.AIR) && !existing.compare(BRIDGE_BLOCK)) return; + this.world.setBlock(pos, block); + this.rescueBlocks.put(pos, block); + } + + private void endRescue() { + this.rescueSafe = null; + Map toRemove = new HashMap<>(this.rescueBlocks); + this.rescueBlocks.clear(); + // nur entfernen, was noch unverändert steht (der Spieler darf Rettungsblöcke abbauen) + this.world.scheduler().buildTask(() -> toRemove.forEach((pos, block) -> { + if(this.world.getBlock(pos).compare(block)) this.world.setBlock(pos, Block.AIR); + })).delay(TaskSchedule.seconds(8)).schedule(); + } + + /** Sprungblöcke und ihr Licht sind geschützt, alles andere (Brücke, Rettung) darf abgebaut werden. */ + public boolean isBreakable(Pos pos) { + if(this.world.getBlock(pos).compare(Block.LIGHT)) return false; + for(Pos block : this.blocks) { + if(block.sameBlock(pos)) return false; + } + return true; + } + public Pos getNextLanding() { return this.blocks.get(this.blocks.size()-(preview+1)); } @@ -129,7 +262,7 @@ public class JumpGenerator { this.blocks.add(this.starting); this.world.setBlock(this.starting, Block.DIAMOND_BLOCK); // nacheinander animieren, damit jeder Block aus seinem bereits stehenden Vorgänger wächst - for(int i = 0; i <= preview; i++) generateNext(i * BlockTransition.TOTAL_TICKS); + for(int i = 0; i <= preview; i++) generateBlock(i * BlockTransition.TOTAL_TICKS); } public void reset() {