Add bridge and rescue totem jokers and fix dark block displays during the transition

This commit is contained in:
MineTec committed 2026-10-08 00:17:00 +02:00
1 parent f56a3765c6
commit ef1662b144
4 files changed
+272 -28

No files matched your search

@@ -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<Player, Long> lastJokerUse = new HashMap<>();
java.util.function.BiConsumer<Player, ItemStack> 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());
});
@@ -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();
}
}
@@ -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
@@ -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<Pos, Block> 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<Pos, Block> 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() {