Drop a random joker on every diamond block and simplify the generator around an explicit course position

This commit is contained in:
MineTec committed 2026-10-08 00:23:17 +02:00
1 parent ef1662b144
commit de6b987ffd
7 files changed
+260 -299

No files matched your search

@@ -1,9 +0,0 @@
package eu.mhsl.minecraft.endlessjumper;
import net.minestom.server.coordinate.Pos;
public class BlockMath {
public static Pos roundToBlock(Pos p) {
return new Pos(p.blockX(), p.blockY(), p.blockZ());
}
}
@@ -8,6 +8,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerSkin; import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.event.GlobalEventHandler; import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.event.player.PlayerBlockPlaceEvent;
@@ -17,112 +18,94 @@ import net.minestom.server.event.player.PlayerSpawnEvent;
import net.minestom.server.event.player.PlayerSwapItemEvent; import net.minestom.server.event.player.PlayerSwapItemEvent;
import net.minestom.server.event.player.PlayerUseItemEvent; import net.minestom.server.event.player.PlayerUseItemEvent;
import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; import net.minestom.server.event.player.PlayerUseItemOnBlockEvent;
import net.minestom.server.item.ItemStack; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack;
import net.minestom.server.timer.TaskSchedule; import net.minestom.server.timer.TaskSchedule;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class EndlessJumper { public class EndlessJumper {
private static final Pos START = new Pos(0, 100, 0);
private static final Map<Player, JumpGenerator> games = new ConcurrentHashMap<>();
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
MinecraftServer server = MinecraftServer.init(new Auth.Velocity(readForwardingSecret())); MinecraftServer server = MinecraftServer.init(new Auth.Velocity(readForwardingSecret()));
InstanceManager instanceManager = MinecraftServer.getInstanceManager(); InstanceManager instanceManager = MinecraftServer.getInstanceManager();
GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler(); GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler();
PlayerSkin skin = PlayerSkin.fromUsername("Notch"); PlayerSkin skin = PlayerSkin.fromUsername("Notch");
HashMap<Player, JumpGenerator> games = new HashMap<>();
eventHandler.addListener(AsyncPlayerConfigurationEvent.class, event -> { eventHandler.addListener(AsyncPlayerConfigurationEvent.class, event -> {
final Player p = event.getPlayer(); final Player p = event.getPlayer();
p.setRespawnPoint(new Pos(0.5,101.5,0.5)); p.setRespawnPoint(START.add(0.5, 1.5, 0.5));
final InstanceContainer world = instanceManager.createInstanceContainer(); final InstanceContainer world = instanceManager.createInstanceContainer();
world.setGenerator(new TerrainGenerator()); world.setBlock(START, Block.DIAMOND_BLOCK);
Pos startingAt = new Pos(0,100,0);
world.setBlock(startingAt, Block.DIAMOND_BLOCK);
world.defaultClock().pause(); world.defaultClock().pause();
world.setTime(18000); world.setTime(18000);
p.setSkin(skin); p.setSkin(skin);
event.setSpawningInstance(world); event.setSpawningInstance(world);
MinecraftServer.getSchedulerManager().scheduleTask( // an den Spieler gebunden: läuft nicht mehr, wenn er vorher geht
() -> games.put(p, new JumpGenerator(startingAt, p, world)), p.scheduler().buildTask(() -> games.put(p, new JumpGenerator(START, p, world)))
TaskSchedule.seconds(3), .delay(TaskSchedule.seconds(3))
TaskSchedule.stop() .schedule();
); });
eventHandler.addListener(PlayerSpawnEvent.class, event -> {
if(!event.isFirstSpawn()) return;
event.getPlayer().setCanPickupItem(true);
ScoreTab.join(event.getPlayer());
Jokers.giveStart(event.getPlayer());
}); });
eventHandler.addListener(PlayerMoveEvent.class, event -> { eventHandler.addListener(PlayerMoveEvent.class, event -> {
Player p = event.getPlayer(); JumpGenerator generator = games.get(event.getPlayer());
if(generator == null) event.setCancelled(true);
if(!games.containsKey(p)) { else generator.onMove(event.getNewPosition());
event.setCancelled(true);
return;
}
JumpGenerator generator = games.get(p);
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));
for(int i = 0; i <= generator.getPreview(); i++) {
Pos target = generator.getBlocks().get(generator.getBlocks().size()-i-1);
if(p1.sameBlock(target) || p2.sameBlock(target)) generator.generateNext();
}
//if(p.getPosition().z() > target.z()+2) generator.highlight();
}); });
// nur Brücken- und Rettungsblöcke dürfen abgebaut werden (Notausgang), Sprungblöcke nie // nur Brücken- und Rettungsblöcke dürfen abgebaut werden (Notausgang), Streckenblöcke nie
eventHandler.addListener(PlayerBlockBreakEvent.class, event -> { eventHandler.addListener(PlayerBlockBreakEvent.class, event -> {
JumpGenerator generator = games.get(event.getPlayer()); JumpGenerator generator = games.get(event.getPlayer());
if(generator == null || !generator.isBreakable(new Pos(event.getBlockPosition()))) event.setCancelled(true); if(generator == null || !generator.isBreakable(event.getBlockPosition())) event.setCancelled(true);
}); });
eventHandler.addListener(PlayerBlockPlaceEvent.class, event -> event.setCancelled(true)); eventHandler.addListener(PlayerBlockPlaceEvent.class, event -> event.setCancelled(true));
eventHandler.addListener(ItemDropEvent.class, event -> event.setCancelled(true)); eventHandler.addListener(ItemDropEvent.class, event -> event.setCancelled(true));
eventHandler.addListener(PlayerSwapItemEvent.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 // Rechtsklick auf Luft und auf Blöcke: useBridge() baut beim zweiten Event nichts doppelt
Map<Player, Long> lastJokerUse = new HashMap<>(); eventHandler.addListener(PlayerUseItemEvent.class, event -> useJoker(event.getPlayer(), event.getItemStack()));
java.util.function.BiConsumer<Player, ItemStack> useJoker = (p, item) -> { eventHandler.addListener(PlayerUseItemOnBlockEvent.class, event -> useJoker(event.getPlayer(), event.getItemStack()));
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 -> { eventHandler.addListener(PickupItemEvent.class, event -> {
if(!event.isFirstSpawn()) return; Jokers joker = Jokers.of(event.getItemStack());
ScoreTab.join(event.getPlayer()); if(event.getLivingEntity() instanceof Player player && joker != null) joker.add(player);
Jokers.giveStart(event.getPlayer()); else event.setCancelled(true);
}); });
eventHandler.addListener(PlayerDisconnectEvent.class, event -> { eventHandler.addListener(PlayerDisconnectEvent.class, event -> {
games.remove(event.getPlayer()); Player p = event.getPlayer();
lastJokerUse.remove(event.getPlayer()); games.remove(p);
ScoreTab.leave(event.getPlayer()); ScoreTab.leave(p);
// jede Partie hat eine eigene Welt: nach dem Verlassen freigeben
Instance world = p.getInstance();
if(world != null) MinecraftServer.getSchedulerManager().scheduleNextTick(() -> instanceManager.unregisterInstance(world));
}); });
server.start("0.0.0.0", 25501); server.start("0.0.0.0", 25501);
}
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 // Wie bei Velocity: Secret aus forwarding.secret im Arbeitsverzeichnis, alternativ per Umgebungsvariable
@@ -2,79 +2,68 @@ package eu.mhsl.minecraft.endlessjumper;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
import java.util.Random;
/** /**
* Joker-Items in der Hotbar. Die Anzahl ist die Stapelgröße. * Joker-Items in der Hotbar, die Anzahl ist die Stapelgröße.
* Brücke: Rechtsklick baut einen Weg bis zum nächsten Checkpoint. * Brücke: Rechtsklick baut einen Weg bis zum nächsten Checkpoint.
* Totem: rettet automatisch beim Runterfallen (Plattform mit Leiter). * Totem: rettet automatisch beim Runterfallen (Plattform mit Leiter).
*/ */
public class Jokers { public enum Jokers {
public static final int BRIDGE_SLOT = 0; BRIDGE(0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg bis zum nächsten Checkpoint"),
public static final int TOTEM_SLOT = 8; TOTEM(8, Material.TOTEM_OF_UNDYING, "Rettungstotem", NamedTextColor.GOLD, "Rettet dich automatisch beim Runterfallen");
private static final Material BRIDGE = Material.BRICK; private final int slot;
private static final Material TOTEM = Material.TOTEM_OF_UNDYING; private final String name;
private final ItemStack item;
public static void giveStart(Player player) { Jokers(int slot, Material material, String name, TextColor color, String lore) {
player.getInventory().setItemStack(BRIDGE_SLOT, bridge(1)); this.slot = slot;
player.getInventory().setItemStack(TOTEM_SLOT, totem(1)); this.name = name;
this.item = ItemStack.builder(material)
.customName(Component.text(name, color).decoration(TextDecoration.ITALIC, false))
.lore(Component.text(lore, NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false))
.build();
} }
public static boolean isBridge(ItemStack item) { public ItemStack item() {
return item.material() == BRIDGE; return this.item;
} }
public static void addBridge(Player player) { public boolean matches(ItemStack stack) {
add(player, BRIDGE_SLOT, bridge(1)); return stack.material() == this.item.material();
player.sendMessage(Component.text("+1 Brücke", NamedTextColor.GOLD));
} }
public static void addTotem(Player player) { public void add(Player player) {
add(player, TOTEM_SLOT, totem(1)); player.getInventory().replaceItemStack(this.slot, current -> this.matches(current) ? current.withAmount(a -> a + 1) : this.item);
player.sendMessage(Component.text("+1 Rettungstotem", NamedTextColor.GOLD)); player.sendMessage(Component.text("+1 " + this.name, NamedTextColor.GOLD));
} }
public static boolean takeBridge(Player player) { /** Nimmt einen Joker weg; false, wenn keiner mehr da ist. */
return take(player, BRIDGE_SLOT, BRIDGE); public boolean take(Player player) {
} if(!this.matches(player.getInventory().getItemStack(this.slot))) return false;
player.getInventory().replaceItemStack(this.slot, current -> current.consume(1));
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; return true;
} }
private static ItemStack bridge(int amount) { public static void giveStart(Player player) {
return ItemStack.builder(BRIDGE) for(Jokers joker : values()) player.getInventory().setItemStack(joker.slot, joker.item);
.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) { public static Jokers random(Random random) {
return ItemStack.builder(TOTEM) return values()[random.nextInt(values().length)];
.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)) public static Jokers of(ItemStack stack) {
.build(); for(Jokers joker : values()) {
if(joker.matches(stack)) return joker;
}
return null;
} }
} }
@@ -4,31 +4,24 @@ import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.play.ScoreboardObjectivePacket; import net.minestom.server.network.packet.server.play.ScoreboardObjectivePacket;
import net.minestom.server.scoreboard.TabList; import net.minestom.server.scoreboard.TabList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* Zeigt den aktuellen Fortschritt jedes Spielers als Zahl in der Tab-Liste. * Zeigt den aktuellen Fortschritt jedes Spielers als Zahl in der Tab-Liste.
* Der Fortschritt ist zugleich das Level des Spielers, daher braucht es keinen eigenen Speicher.
*/ */
public class ScoreTab { public class ScoreTab {
private static final TabList TAB_LIST = new TabList("jumps", ScoreboardObjectivePacket.Type.INTEGER); private static final TabList TAB_LIST = new TabList("jumps", ScoreboardObjectivePacket.Type.INTEGER);
private static final Map<Player, Integer> scores = new ConcurrentHashMap<>();
public static void join(Player player) { public static void join(Player player) {
TAB_LIST.addViewer(player); TAB_LIST.addViewer(player);
scores.put(player, 0);
// dem neuen Spieler auch die Scores aller anderen zeigen // dem neuen Spieler auch die Scores aller anderen zeigen
scores.forEach(TAB_LIST::updateScore); TAB_LIST.getViewers().forEach(viewer -> TAB_LIST.updateScore(viewer, viewer.getLevel()));
} }
public static void update(Player player, int score) { public static void update(Player player, int score) {
if(!scores.containsKey(player)) return; if(TAB_LIST.getViewers().contains(player)) TAB_LIST.updateScore(player, score);
scores.put(player, score);
TAB_LIST.updateScore(player, score);
} }
public static void leave(Player player) { public static void leave(Player player) {
scores.remove(player);
TAB_LIST.removeViewer(player); TAB_LIST.removeViewer(player);
} }
} }
@@ -1,12 +0,0 @@
package eu.mhsl.minecraft.endlessjumper;
import net.minestom.server.instance.generator.GenerationUnit;
import net.minestom.server.instance.generator.Generator;
import org.jetbrains.annotations.NotNull;
public class TerrainGenerator implements Generator {
@Override
public void generate(@NotNull GenerationUnit unit) {
}
}
@@ -13,7 +13,7 @@ public class BlockPalette {
public record Stage(int from, String name, List<Block> blocks) { public record Stage(int from, String name, List<Block> blocks) {
} }
public static final List<Stage> STAGES = List.of( private static final List<Stage> STAGES = List.of(
new Stage(0, "Wiese", List.of( new Stage(0, "Wiese", List.of(
Block.GRASS_BLOCK, Block.GRASS_BLOCK, Block.MOSS_BLOCK, Block.PODZOL, Block.ROOTED_DIRT Block.GRASS_BLOCK, Block.GRASS_BLOCK, Block.MOSS_BLOCK, Block.PODZOL, Block.ROOTED_DIRT
)), )),
@@ -59,19 +59,10 @@ public class BlockPalette {
); );
public static Stage stageAt(int index) { public static Stage stageAt(int index) {
Stage current = STAGES.getFirst(); for(Stage stage : STAGES.reversed()) {
for(Stage stage : STAGES) { if(index >= stage.from()) return stage;
if(index >= stage.from()) current = stage;
} }
return current; return STAGES.getFirst();
}
/** Liefert die Stufe, die genau bei diesem Index beginnt, sonst null. */
public static Stage stageStartingAt(int index) {
for(Stage stage : STAGES) {
if(stage.from() == index) return stage;
}
return null;
} }
public static Block getRandom(int index, Random random) { public static Block getRandom(int index, Random random) {
@@ -1,39 +1,47 @@
package eu.mhsl.minecraft.endlessjumper.generator; package eu.mhsl.minecraft.endlessjumper.generator;
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.Jokers;
import eu.mhsl.minecraft.endlessjumper.ScoreTab; import eu.mhsl.minecraft.endlessjumper.ScoreTab;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.network.packet.server.play.ParticlePacket; import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle; import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent; import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule; import net.minestom.server.timer.TaskSchedule;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
/**
* Eine Partie Jump'n'Run. Die Strecke ist eine Liste von Schritten; {@link #standing} ist der Index
* des zuletzt erreichten Blocks und zugleich der Score. Vorgeneriert werden {@link #PREVIEW} Blöcke
* über den nächsten Landeblock hinaus.
*/
public class JumpGenerator { public class JumpGenerator {
private final Player player; private record Step(Pos pos, Block block) {
private final InstanceContainer world; }
private final ArrayList<Pos> blocks = new ArrayList<>();
private Pos checkpoint;
private Pos starting;
private int combo = 0; private record BlockKey(int x, int y, int z) {
// Block, von dem der nächste Sprung startet (bestimmt, ob ein Sprung nach oben möglich ist) static BlockKey of(Point point) {
private Block lastBlock = Block.DIAMOND_BLOCK; return new BlockKey(point.blockX(), point.blockY(), point.blockZ());
}
private final int preview = 3; }
private static final int PREVIEW = 3;
private static final int FULL_DIFFICULTY_AT = 1000; private static final int FULL_DIFFICULTY_AT = 1000;
private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS; private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS;
@@ -41,68 +49,130 @@ public class JumpGenerator {
private static final Block RESCUE_BLOCK = Block.OAK_PLANKS; private static final Block RESCUE_BLOCK = Block.OAK_PLANKS;
private static final int RESCUE_DEPTH = 6; private static final int RESCUE_DEPTH = 6;
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));
private static final Sound CHECKPOINT_SOUND = Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.BLOCK, 0.3f, 1f);
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 RESET_SOUND = Sound.sound(SoundEvent.BLOCK_GLASS_BREAK, Sound.Source.BLOCK, 0.3f, 1f);
private final Player player;
private final Instance world;
private final Random random = new Random();
private final List<Step> course = new ArrayList<>();
private final Map<BlockKey, Integer> courseIndex = new HashMap<>();
private int standing = 0;
private int combo = 0;
private Pos checkpoint;
// bis zu diesem Index wurde per Brücke gebaut: diese Blöcke zählen nicht zur Combo
private int bridgeEnd = -1;
// Rettung durch das Totem: Plattform mit Leiter unter dem Block, von dem man gefallen ist // Rettung durch das Totem: Plattform mit Leiter unter dem Block, von dem man gefallen ist
private final Map<Pos, Block> rescueBlocks = new HashMap<>(); private final Map<Pos, Block> rescueBlocks = new HashMap<>();
private Pos rescueSafe; private Pos rescueSafe;
private int rescueFloorY;
public JumpGenerator(Pos start, Player player, Instance world) {
public JumpGenerator(Pos starting, Player player, InstanceContainer world) {
this.player = player; this.player = player;
this.world = world; this.world = world;
this.starting = starting; this.checkpoint = start;
generateStart(starting); this.addStep(new Step(start, Block.DIAMOND_BLOCK));
// nacheinander animieren, damit jeder Block aus seinem bereits stehenden Vorgänger wächst
this.ensureGenerated(PREVIEW + 1, BlockTransition.TOTAL_TICKS);
} }
public void generateNext() { private static boolean isCheckpoint(int index) {
boolean standingTenth = (this.blocks.size()-this.preview-1) % 10 == 0; return index % 10 == 0;
}
float pitch = 1 + (combo * 0.025f); private static boolean isMilestone(int index) {
this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, pitch)); return index % 100 == 0;
this.player.showTitle(Title.title(Component.text(""), Component.text((combo)+"", TextColor.color(255, 69, 0)))); }
int score = Math.max(0, this.blocks.size()-this.preview-1);
this.player.setLevel(score);
ScoreTab.update(this.player, score);
if(score > 0 && score % 100 == 0) Jokers.addBridge(this.player); private static double height(Block block) {
if(score > 0 && score % 200 == 0) Jokers.addTotem(this.player); return block.collisionShape().relativeEnd().y();
}
this.generateBlock(0); private Pos landing() {
return this.course.get(this.standing + 1).pos();
}
if(standingTenth) { /** Wird bei jeder Bewegung aufgerufen: erkennt Landungen und behandelt das Runterfallen. */
Pos current = this.blocks.get(this.blocks.size()-this.preview-2); public void onMove(Pos newPosition) {
this.checkpoint = current; // Block unter den Füßen; -0.5 trifft auch niedrige Blöcke wie den Endportalrahmen
this.player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.BLOCK, 0.3f, 1f)); Integer index = this.courseIndex.get(BlockKey.of(newPosition.sub(0, 0.5, 0)));
this.player.showTitle(Title.title(Component.text(""), Component.text("Checkpoint", TextColor.color(0, 255, 0)))); if(index != null && index > this.standing) this.advanceTo(index);
this.player.sendPacket(new ParticlePacket(Particle.CRIT, current, current.add(0.5), 1.5f, 50)); this.checkFall();
}
private void advanceTo(int index) {
int previous = this.standing;
this.standing = index;
this.player.setLevel(index);
ScoreTab.update(this.player, index);
if(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));
this.player.showTitle(Title.title(Component.empty(), Component.text(this.combo, COMBO_COLOR)));
}
if(isCheckpoint(index)) {
this.checkpoint = this.course.get(index).pos();
this.player.playSound(CHECKPOINT_SOUND);
this.player.showTitle(CHECKPOINT_TITLE);
this.player.sendPacket(new ParticlePacket(Particle.CRIT, this.checkpoint, this.checkpoint.add(0.5), 1.5f, 50));
} }
// Abschnitt dauerhaft in der Actionbar (Titel werden von der Combo-Anzeige überschrieben) // Abschnitt dauerhaft in der Actionbar (Titel werden von der Combo-Anzeige überschrieben)
BlockPalette.Stage reachedStage = BlockPalette.stageStartingAt(score); BlockPalette.Stage stage = BlockPalette.stageAt(index);
if(reachedStage != null && reachedStage.from() > 0) { if(stage != BlockPalette.stageAt(previous)) {
this.player.playSound(Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.4f, 1f)); this.player.playSound(STAGE_SOUND);
this.player.sendActionBar(Component.text(reachedStage.name(), TextColor.color(255, 170, 0))); this.player.sendActionBar(Component.text(stage.name(), NamedTextColor.GOLD));
} else { } else {
this.player.sendActionBar(Component.text(BlockPalette.stageAt(score).name(), TextColor.color(170, 170, 170))); this.player.sendActionBar(Component.text(stage.name(), NamedTextColor.GRAY));
}
this.combo++;
} }
/** Erzeugt den nächsten Sprungblock ohne Rückmeldung an den Spieler (Combo, Titel, Sounds). */ this.ensureGenerated(index + PREVIEW + 1, 0);
private void generateBlock(int animationDelayTicks) { }
int index = this.blocks.size();
Random r = new Random(); private void ensureGenerated(int upTo, int stepTicks) {
Pos previous = blocks.getLast(); int delay = 0;
Pos target = previous.add(this.nextOffset(index, r)); while(this.course.size() <= upTo) {
Block block = index % 100 == 0 ? Block.DIAMOND_BLOCK : index % 10 == 0 ? Block.GOLD_BLOCK : BlockPalette.getRandom(index, r); this.generateStep(delay, null);
this.lastBlock = block; delay += stepTicks;
BlockTransition.animate(this.world, previous, target, block, animationDelayTicks, () -> { }
}
private void addStep(Step step) {
this.courseIndex.put(BlockKey.of(step.pos()), this.course.size());
this.course.add(step);
}
/** Erzeugt den nächsten Streckenblock; {@code afterArrival} läuft, sobald er tatsächlich steht. */
private void generateStep(int animationDelayTicks, Runnable afterArrival) {
int index = this.course.size();
Step previous = this.course.getLast();
Block block = isMilestone(index) ? Block.DIAMOND_BLOCK : isCheckpoint(index) ? Block.GOLD_BLOCK : BlockPalette.getRandom(index, this.random);
Pos target = previous.pos().add(this.nextOffset(index, previous.block(), block));
this.addStep(new Step(target, block));
BlockTransition.animate(this.world, previous.pos(), target, block, animationDelayTicks, () -> {
this.world.setBlock(target, block); this.world.setBlock(target, block);
this.world.setBlock(target.add(0, 1, 0), Block.LIGHT); 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.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.player.sendPacket(new ParticlePacket(Particle.FALLING_NECTAR, target, target.add(0.5), 0.3f, 10));
if(isMilestone(index)) this.dropJoker(target);
if(afterArrival != null) afterArrival.run();
}); });
this.blocks.add(target); }
/** Auf jedem Diamantblock 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);
item.setInstance(this.world, target.add(0.5, 1, 0.5));
} }
/** /**
@@ -111,55 +181,50 @@ public class JumpGenerator {
* Anteil an 4er-Sprüngen, seitlichen Versätzen und Sprüngen nach oben bis zum bisherigen * Anteil an 4er-Sprüngen, seitlichen Versätzen und Sprüngen nach oben bis zum bisherigen
* Niveau und bleiben danach dort. * Niveau und bleiben danach dort.
*/ */
private Pos nextOffset(int index, Random r) { private Vec nextOffset(int index, Block from, Block to) {
double difficulty = Math.min(1.0, (double) index / FULL_DIFFICULTY_AT); double difficulty = Math.min(1.0, (double) index / FULL_DIFFICULTY_AT);
int dz = r.nextDouble() < 0.1 + 0.4 * difficulty ? 4 : 3; int dz = this.random.nextDouble() < 0.1 + 0.4 * difficulty ? 4 : 3;
int dx = this.random.nextDouble() < 0.1 + 0.57 * difficulty ? (this.random.nextBoolean() ? 1 : -1) : 0;
int dx = r.nextDouble() < 0.1 + 0.57 * difficulty ? (r.nextBoolean() ? 1 : -1) : 0;
double y = this.player.getPosition().y(); double y = this.player.getPosition().y();
double roll = r.nextDouble(); double roll = this.random.nextDouble();
int dy = 0; int dy = 0;
if(roll < 0.05 + 0.28 * difficulty && y <= 120) dy = 1; if(roll < 0.05 + 0.28 * difficulty && y <= 120) dy = 1;
else if(roll > 1 - (0.2 + 0.13 * difficulty) && y >= 80) dy = -1; else if(roll > 1 - (0.2 + 0.13 * difficulty) && y >= 80) dy = -1;
// von niedrigen Blöcken (z. B. Endportalrahmen, 13/16 hoch) ist ein Block höher nicht erreichbar // die Oberkante darf höchstens einen Block höher liegen (z. B. vom Endportalrahmen aus)
if(dy == 1 && this.lastBlock.collisionShape().relativeEnd().y() < 1) dy = 0; if(dy == 1 && height(to) > height(from)) dy = 0;
// weite Sprünge nach oben erst ab mittlerer, diagonal erst ab hoher Schwierigkeit // weite Sprünge nach oben erst ab mittlerer, diagonal erst ab hoher Schwierigkeit
if(dy == 1 && dz == 4 && difficulty < 0.4) dz = 3; if(dy == 1 && dz == 4 && difficulty < 0.4) dz = 3;
if(dy == 1 && dz == 4 && dx != 0 && difficulty < 0.7) dx = 0; if(dy == 1 && dz == 4 && dx != 0 && difficulty < 0.7) dx = 0;
return new Pos(dx, dy, dz); return new Vec(dx, dy, dz);
} }
/** /**
* Joker „Brücke“: erzeugt die Sprungblöcke bis zum nächsten Checkpoint und verbindet sie * Joker „Brücke“: erzeugt die Strecke bis zum nächsten Checkpoint und verbindet sie mit einem
* mit einem begehbaren Weg. Die übersprungenen Blöcke zählen nicht zur Combo. * begehbaren Weg. Diese Blöcke zählen nicht zur Combo. Mehrfacher Aufruf baut nichts doppelt.
*/ */
public boolean useBridge() { public boolean useBridge() {
if(this.rescueSafe != null) return false; int nextCheckpoint = (this.standing / 10 + 1) * 10;
if(this.rescueSafe != null || this.bridgeEnd >= nextCheckpoint) return false;
this.bridgeEnd = nextCheckpoint;
int standing = this.blocks.size() - this.preview - 2; // bereits stehende Abschnitte sofort, neue Blöcke bekommen ihren Weg bei Ankunft
int nextCheckpoint = (standing / 10 + 1) * 10; for(int i = this.standing + 1; i < this.course.size() && i <= nextCheckpoint; i++) {
this.buildPath(this.course.get(i - 1).pos(), this.course.get(i).pos());
// so weit vorgenerieren, dass der Checkpoint der nächste Landeblock ist }
int delay = 0; int delay = 0;
while(this.blocks.size() - this.preview - 1 < nextCheckpoint) { while(this.course.size() <= nextCheckpoint) {
this.generateBlock(delay); Pos from = this.course.getLast().pos();
int index = this.course.size();
this.generateStep(delay, () -> this.buildPath(from, this.course.get(index).pos()));
delay += BRIDGE_STEP_TICKS; delay += BRIDGE_STEP_TICKS;
} }
for(int i = standing; i < nextCheckpoint; i++) { this.player.playSound(BRIDGE_SOUND);
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; return true;
} }
@@ -175,56 +240,52 @@ public class JumpGenerator {
} }
} }
/** Wird bei jeder Bewegung aufgerufen: Rettung durch Totem oder Reset beim Runterfallen. */ private void checkFall() {
public void checkFall() {
double y = this.player.getPosition().y(); double y = this.player.getPosition().y();
if(this.rescueSafe != null) { if(this.rescueSafe != null) {
if(y >= this.rescueSafe.y() + 0.9) this.endRescue(); if(y >= this.rescueSafe.y() + 0.9) this.endRescue();
else if(y < this.rescueFloorY - 3) { else if(y < this.rescueSafe.blockY() - RESCUE_DEPTH - 3) {
this.endRescue(); this.endRescue();
this.reset(); this.reset();
} }
return; return;
} }
if(y < this.getNextLanding().y() - 5 && y < this.checkpoint.y()) { if(y < this.landing().y() - 5 && y < this.checkpoint.y()) {
if(Jokers.takeTotem(this.player)) this.rescue(); if(Jokers.TOTEM.take(this.player)) this.rescue();
else this.reset(); else this.reset();
} }
} }
/** /**
* Joker „Rettungstotem“: Plattform unter dem zuletzt erreichten Block, davor eine Stützsäule * Joker „Rettungstotem“: Plattform unter dem zuletzt erreichten Block, davor eine Stützsäule
* mit Leiter, über die man direkt wieder auf den Block klettert. * mit Leiter, über die man direkt wieder auf den Block klettert. Die Strecke geht immer
* mindestens 3 Blöcke nach vorne, die Rettung baut nur dahinter und darunter.
*/ */
private void rescue() { private void rescue() {
Pos safe = this.blocks.get(Math.max(0, this.blocks.size() - this.preview - 2)); Pos safe = this.course.get(this.standing).pos();
int x = safe.blockX(); int x = safe.blockX();
int z = safe.blockZ(); int z = safe.blockZ();
int floorY = safe.blockY() - RESCUE_DEPTH;
this.rescueSafe = safe; this.rescueSafe = safe;
this.rescueFloorY = safe.blockY() - RESCUE_DEPTH;
for(int dx = -1; dx <= 1; dx++) { 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); for(int dz = -3; dz <= -1; dz++) this.placeRescue(new Pos(x + dx, floorY, z + dz), RESCUE_BLOCK);
} }
// Säule direkt hinter dem Block bis auf seine Höhe, Leiter davor (hängt an der Säule) // 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++) { for(int y = floorY + 1; y <= safe.blockY(); y++) {
this.placeRescue(new Pos(x, y, z - 1), RESCUE_BLOCK); this.placeRescue(new Pos(x, y, z - 1), RESCUE_BLOCK);
this.placeRescue(new Pos(x, y, z - 2), Block.LADDER.withProperty("facing", "north")); this.placeRescue(new Pos(x, y, z - 2), Block.LADDER.withProperty("facing", "north"));
} }
this.player.setVelocity(Vec.ZERO); this.player.setVelocity(Vec.ZERO);
this.player.teleport(new Pos(x + 0.5, this.rescueFloorY + 1, z - 1.5, 0, 0)); this.player.teleport(new Pos(x + 0.5, floorY + 1, z - 1.5, 0, 0));
this.player.triggerStatus((byte) 35); this.player.triggerStatus((byte) 35);
this.player.playSound(Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.MASTER, 0.6f, 1f)); this.player.playSound(RESCUE_SOUND);
} }
/** /** Brückenglas darf überschrieben werden, sonst könnte ein früherer Brückenweg die Leiter versperren. */
* 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) { private void placeRescue(Pos pos, Block block) {
Block existing = this.world.getBlock(pos); Block existing = this.world.getBlock(pos);
if(!existing.compare(Block.AIR) && !existing.compare(BRIDGE_BLOCK)) return; if(!existing.compare(Block.AIR) && !existing.compare(BRIDGE_BLOCK)) return;
@@ -234,62 +295,27 @@ public class JumpGenerator {
private void endRescue() { private void endRescue() {
this.rescueSafe = null; this.rescueSafe = null;
Map<Pos, Block> toRemove = new HashMap<>(this.rescueBlocks); Map<Pos, Block> placed = Map.copyOf(this.rescueBlocks);
this.rescueBlocks.clear(); this.rescueBlocks.clear();
// nur entfernen, was noch unverändert steht (der Spieler darf Rettungsblöcke abbauen) removeLater(this.world, placed);
this.world.scheduler().buildTask(() -> toRemove.forEach((pos, block) -> { }
if(this.world.getBlock(pos).compare(block)) this.world.setBlock(pos, Block.AIR);
/** Entfernt nur, was noch unverändert steht (Rettungsblöcke dürfen abgebaut werden). */
private static void removeLater(Instance world, Map<Pos, Block> placed) {
world.scheduler().buildTask(() -> placed.forEach((pos, block) -> {
if(world.getBlock(pos).compare(block)) world.setBlock(pos, Block.AIR);
})).delay(TaskSchedule.seconds(8)).schedule(); })).delay(TaskSchedule.seconds(8)).schedule();
} }
/** Sprungblöcke und ihr Licht sind geschützt, alles andere (Brücke, Rettung) darf abgebaut werden. */ /** Streckenblöcke und ihr Licht sind geschützt, alles andere (Brücke, Rettung) darf abgebaut werden. */
public boolean isBreakable(Pos pos) { public boolean isBreakable(Point pos) {
if(this.world.getBlock(pos).compare(Block.LIGHT)) return false; return !this.world.getBlock(pos).compare(Block.LIGHT) && !this.courseIndex.containsKey(BlockKey.of(pos));
for(Pos block : this.blocks) {
if(block.sameBlock(pos)) return false;
}
return true;
} }
public Pos getNextLanding() { private void reset() {
return this.blocks.get(this.blocks.size()-(preview+1));
}
public void generateStart(Pos start) {
this.starting = start;
this.checkpoint = start;
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++) generateBlock(i * BlockTransition.TOTAL_TICKS);
}
public void reset() {
/*
for(Pos p : this.blocks) {
this.world.setBlock(p, Block.AIR);
}
this.blocks.clear();
*/
this.player.teleport(this.checkpoint.add(0.5, 1, 0.5)); this.player.teleport(this.checkpoint.add(0.5, 1, 0.5));
this.combo = 0; this.combo = 0;
this.player.showTitle(Title.title(Component.text(""), Component.text(""))); this.player.clearTitle();
this.player.playSound(RESET_SOUND);
//generateStart(last);
this.player.playSound(Sound.sound(SoundEvent.BLOCK_GLASS_BREAK, Sound.Source.BLOCK, 0.3f, 1f));
}
public Pos getCheckpoint() {
return checkpoint;
}
public ArrayList<Pos> getBlocks() {
return blocks;
}
public int getPreview() {
return preview;
} }
} }