diff --git a/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java b/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java index c9942ab..02787d2 100644 --- a/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java @@ -19,11 +19,9 @@ 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.instance.Instance; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.block.Block; -import net.minestom.server.item.ItemStack; import java.io.IOException; import java.nio.file.Files; @@ -54,7 +52,7 @@ public class EndlessJumper { Player p = event.getPlayer(); p.setCanPickupItem(true); ScoreTab.join(p); - Jokers.giveStart(p); + Jokers.clear(p); // sobald der Spieler in der Welt steht, geht es los games.put(p, new JumpGenerator(START, Progress.checkpoint(p.getUuid()), p, p.getInstance())); }); @@ -70,9 +68,13 @@ public class EndlessJumper { eventHandler.addListener(ItemDropEvent.class, event -> event.setCancelled(true)); eventHandler.addListener(PlayerSwapItemEvent.class, event -> event.setCancelled(true)); - // Rechtsklick auf Luft und auf Blöcke: useBridge() baut beim zweiten Event nichts doppelt - eventHandler.addListener(PlayerUseItemEvent.class, event -> useJoker(event.getPlayer(), event.getItemStack())); - eventHandler.addListener(PlayerUseItemOnBlockEvent.class, event -> useJoker(event.getPlayer(), event.getItemStack())); + // der Client meldet Rechtsklicks auch auf Blöcke als Item-Nutzung, solange der Block sie nicht + // selbst verarbeitet (auf der Strecke nie) – ein Event genügt, Joker werden nicht doppelt verbraucht + eventHandler.addListener(PlayerUseItemEvent.class, event -> { + JumpGenerator generator = games.get(event.getPlayer()); + Jokers joker = Jokers.of(event.getItemStack()); + if(generator != null && joker != null && generator.useJoker(joker)) joker.take(event.getPlayer()); + }); eventHandler.addListener(PickupItemEvent.class, event -> { Jokers joker = Jokers.of(event.getItemStack()); @@ -119,17 +121,12 @@ public class EndlessJumper { InstanceContainer world = createWorld(); player.setVelocity(Vec.ZERO); player.setInstance(world, SPAWN).thenRun(() -> { - Jokers.giveStart(player); + Jokers.clear(player); games.put(player, new JumpGenerator(START, 0, player, world)); releaseLater(oldWorld); }); } - private static void useJoker(Player player, ItemStack item) { - JumpGenerator generator = games.get(player); - if(generator != null && Jokers.BRIDGE.matches(item) && generator.useBridge()) Jokers.BRIDGE.take(player); - } - // Wie bei Velocity: Secret aus forwarding.secret im Arbeitsverzeichnis, alternativ per Umgebungsvariable private static String readForwardingSecret() throws IOException { String fromEnv = System.getenv("VELOCITY_FORWARDING_SECRET"); diff --git a/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java b/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java index ed561de..ad02e3d 100644 --- a/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/Jokers.java @@ -11,12 +11,13 @@ import net.minestom.server.item.Material; import java.util.Random; /** - * Joker-Items in der Hotbar, die Anzahl ist die Stapelgröße. - * Brücke: Rechtsklick baut einen Weg über die nächsten 10 Blöcke. - * Totem: rettet automatisch beim Runterfallen (Plattform mit Leiter). + * Joker-Items in der Hotbar, die Anzahl ist die Stapelgröße. Es gibt sie zum Einsammeln auf der + * Strecke; wer bei 0 beginnt, bekommt ein Totem. Das Totem wirkt automatisch, die anderen per Rechtsklick (siehe JumpGenerator#useJoker). */ public enum Jokers { BRIDGE(0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg über die nächsten 10 Blöcke"), + FEATHER_FALL(1, Material.FEATHER, "Federfall", NamedTextColor.WHITE, "Rechtsklick: 15 Sekunden langsames Fallen"), + SPRING(2, Material.RABBIT_FOOT, "Sprungfeder", NamedTextColor.GREEN, "Rechtsklick: die nächsten 10 Sprünge mit Sprungkraft"), TOTEM(8, Material.TOTEM_OF_UNDYING, "Rettungstotem", NamedTextColor.GOLD, "Rettet dich automatisch beim Runterfallen"); private final int slot; @@ -52,8 +53,9 @@ public enum Jokers { return true; } - public static void giveStart(Player player) { - for(Jokers joker : values()) player.getInventory().setItemStack(joker.slot, joker.item); + /** Zum Start (und nach /reset) gibt es keine Joker. */ + public static void clear(Player player) { + for(Jokers joker : values()) player.getInventory().setItemStack(joker.slot, ItemStack.AIR); } public static Jokers random(Random random) { 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 40d8337..f84d1b7 100644 --- a/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java +++ b/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java @@ -17,6 +17,8 @@ import net.minestom.server.instance.Instance; 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.potion.Potion; +import net.minestom.server.potion.PotionEffect; import net.minestom.server.sound.SoundEvent; import net.minestom.server.timer.TaskSchedule; @@ -51,6 +53,9 @@ public class JumpGenerator { private static final int BRIDGE_LENGTH = 10; private static final Block RESCUE_BLOCK = Block.OAK_PLANKS; private static final int RESCUE_DEPTH = 6; + private static final int FEATHER_FALL_TICKS = 15 * 20; + private static final int SPRING_JUMPS = 10; + private static final double CHECKPOINT_JOKER_CHANCE = 0.1; private static final TextColor COMBO_COLOR = TextColor.color(255, 69, 0); private static final Title CHECKPOINT_TITLE = Title.title(Component.empty(), Component.text("Checkpoint", NamedTextColor.GREEN)); @@ -59,6 +64,7 @@ public class JumpGenerator { private static final Sound STAGE_SOUND = Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.4f, 1f); private static final Sound BRIDGE_SOUND = Sound.sound(SoundEvent.BLOCK_AMETHYST_BLOCK_CHIME, Sound.Source.MASTER, 1f, 1f); private static final Sound RESCUE_SOUND = Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.MASTER, 0.6f, 1f); + private static final Sound JOKER_SOUND = Sound.sound(SoundEvent.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.MASTER, 0.8f, 1.2f); private static final Sound RESET_SOUND = Sound.sound(SoundEvent.BLOCK_GLASS_BREAK, Sound.Source.BLOCK, 0.3f, 1f); private final Player player; @@ -77,6 +83,9 @@ public class JumpGenerator { private int bridgeStart = -1; private int bridgeEnd = -1; + // Sprungfeder: so viele Sprünge noch mit Sprungkraft + private int springJumpsLeft = 0; + // laufende Rettung durch das Totem: der Block, auf den man über die Leiter zurückklettert private Pos rescueSafe; @@ -94,7 +103,9 @@ public class JumpGenerator { this.addStep(new Step(start, startBlock)); this.player.setLevel(startIndex); ScoreTab.update(this.player, startIndex); + // wer ganz von vorn beginnt, bekommt ein Totem; beim Weiterspielen gibt es keine Joker if(startIndex > 0) this.player.sendMessage(Component.text("Weiter ab Sprung " + startIndex, NamedTextColor.GOLD)); + else Jokers.TOTEM.add(this.player); // nacheinander animieren, damit jeder Block aus seinem bereits stehenden Vorgänger wächst this.ensureGenerated(startIndex + PREVIEW + 1, BlockTransition.TOTAL_TICKS); @@ -139,6 +150,8 @@ public class JumpGenerator { this.player.setLevel(index); ScoreTab.update(this.player, index); + if(this.springJumpsLeft > 0 && --this.springJumpsLeft == 0) this.player.removeEffect(PotionEffect.JUMP_BOOST); + if(index < this.bridgeStart || index > this.bridgeEnd) { this.combo++; this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, 1 + this.combo * 0.025f)); @@ -204,12 +217,12 @@ public class JumpGenerator { 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(isMilestone(index)) this.dropJoker(target); + if(isMilestone(index) || (isCheckpoint(index) && this.random.nextDouble() < CHECKPOINT_JOKER_CHANCE)) this.dropJoker(target); if(afterArrival != null) afterArrival.run(); }); } - /** Auf jedem Diamantblock liegt ein zufälliger Joker zum Einsammeln. */ + /** Auf jedem Diamantblock (und manchem Checkpoint) liegt ein zufälliger Joker zum Einsammeln. */ private void dropJoker(Pos target) { ItemEntity item = new ItemEntity(Jokers.random(this.random).item()); item.setPickupDelay(Duration.ZERO); @@ -244,12 +257,31 @@ public class JumpGenerator { return new Vec(dx, dy, dz); } + /** Rechtsklick mit einem Joker; true, wenn er verbraucht wurde. Das Totem wirkt nur automatisch. */ + public boolean useJoker(Jokers joker) { + boolean used = switch(joker) { + case BRIDGE -> this.useBridge(); + case FEATHER_FALL -> { + this.player.addEffect(new Potion(PotionEffect.SLOW_FALLING, 0, FEATHER_FALL_TICKS)); + yield true; + } + case SPRING -> { + this.springJumpsLeft = SPRING_JUMPS; + this.player.addEffect(new Potion(PotionEffect.JUMP_BOOST, 0, Potion.INFINITE_DURATION)); + yield true; + } + case TOTEM -> false; + }; + if(used) this.player.playSound(JOKER_SOUND); + return used; + } + /** * Joker „Brücke“: verbindet die nächsten {@link #BRIDGE_LENGTH} Blöcke ab der aktuellen Position * mit einem begehbaren Weg, auch über früher schon geschaffte Stücke (z. B. nach einem Reset). * Diese Blöcke zählen nicht zur Combo. Ist der Bereich schon überbrückt, passiert nichts. */ - public boolean useBridge() { + private boolean useBridge() { int target = this.standing + BRIDGE_LENGTH; if(this.rescueSafe != null || (this.standing >= this.bridgeStart && target <= this.bridgeEnd)) return false; this.bridgeStart = this.standing; @@ -334,9 +366,11 @@ public class JumpGenerator { if(existing.compare(Block.AIR) || existing.compare(BRIDGE_BLOCK)) this.world.setBlock(pos, block); } - /** Beendet alles, was an den Spieler gebunden weiterläuft (Partikel, Musik). */ + /** Beendet alles, was an den Spieler gebunden weiterläuft (Partikel, Musik, Joker-Effekte). */ public void close() { this.atmosphere.stop(); + this.player.removeEffect(PotionEffect.SLOW_FALLING); + this.player.removeEffect(PotionEffect.JUMP_BOOST); } private void reset() {