Add feather fall and spring jokers, drop jokers on checkpoints with 10% chance and only give a totem when starting from zero

This commit is contained in:
MineTec committed 2026-10-10 13:09:02 +02:00
1 parent 555f4024e4
commit 38bb9c6dc8
3 files changed
+54 -21

No files matched your search

@@ -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");
@@ -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) {
@@ -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() {