Drop a random joker on every diamond block and simplify the generator around an explicit course position
This commit is contained in:
1 parent
ef1662b144
commit
de6b987ffd
7 files changed
+259
-298
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.event.GlobalEventHandler;
|
||||
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.PlayerBlockBreakEvent;
|
||||
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.PlayerUseItemEvent;
|
||||
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.InstanceManager;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
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 {
|
||||
MinecraftServer server = MinecraftServer.init(new Auth.Velocity(readForwardingSecret()));
|
||||
InstanceManager instanceManager = MinecraftServer.getInstanceManager();
|
||||
GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler();
|
||||
PlayerSkin skin = PlayerSkin.fromUsername("Notch");
|
||||
|
||||
HashMap<Player, JumpGenerator> games = new HashMap<>();
|
||||
|
||||
eventHandler.addListener(AsyncPlayerConfigurationEvent.class, event -> {
|
||||
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();
|
||||
world.setGenerator(new TerrainGenerator());
|
||||
|
||||
Pos startingAt = new Pos(0,100,0);
|
||||
world.setBlock(startingAt, Block.DIAMOND_BLOCK);
|
||||
|
||||
world.setBlock(START, Block.DIAMOND_BLOCK);
|
||||
world.defaultClock().pause();
|
||||
world.setTime(18000);
|
||||
|
||||
p.setSkin(skin);
|
||||
|
||||
event.setSpawningInstance(world);
|
||||
|
||||
MinecraftServer.getSchedulerManager().scheduleTask(
|
||||
() -> games.put(p, new JumpGenerator(startingAt, p, world)),
|
||||
TaskSchedule.seconds(3),
|
||||
TaskSchedule.stop()
|
||||
);
|
||||
// an den Spieler gebunden: läuft nicht mehr, wenn er vorher geht
|
||||
p.scheduler().buildTask(() -> games.put(p, new JumpGenerator(START, p, world)))
|
||||
.delay(TaskSchedule.seconds(3))
|
||||
.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 -> {
|
||||
Player p = event.getPlayer();
|
||||
|
||||
if(!games.containsKey(p)) {
|
||||
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();
|
||||
JumpGenerator generator = games.get(event.getPlayer());
|
||||
if(generator == null) event.setCancelled(true);
|
||||
else generator.onMove(event.getNewPosition());
|
||||
});
|
||||
|
||||
// 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 -> {
|
||||
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(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()));
|
||||
// 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()));
|
||||
|
||||
eventHandler.addListener(PlayerSpawnEvent.class, event -> {
|
||||
if(!event.isFirstSpawn()) return;
|
||||
ScoreTab.join(event.getPlayer());
|
||||
Jokers.giveStart(event.getPlayer());
|
||||
eventHandler.addListener(PickupItemEvent.class, event -> {
|
||||
Jokers joker = Jokers.of(event.getItemStack());
|
||||
if(event.getLivingEntity() instanceof Player player && joker != null) joker.add(player);
|
||||
else event.setCancelled(true);
|
||||
});
|
||||
|
||||
eventHandler.addListener(PlayerDisconnectEvent.class, event -> {
|
||||
games.remove(event.getPlayer());
|
||||
lastJokerUse.remove(event.getPlayer());
|
||||
ScoreTab.leave(event.getPlayer());
|
||||
Player p = event.getPlayer();
|
||||
games.remove(p);
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -2,79 +2,68 @@ package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
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;
|
||||
|
||||
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.
|
||||
* 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;
|
||||
public enum Jokers {
|
||||
BRIDGE(0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg bis zum nächsten Checkpoint"),
|
||||
TOTEM(8, Material.TOTEM_OF_UNDYING, "Rettungstotem", NamedTextColor.GOLD, "Rettet dich automatisch beim Runterfallen");
|
||||
|
||||
private static final Material BRIDGE = Material.BRICK;
|
||||
private static final Material TOTEM = Material.TOTEM_OF_UNDYING;
|
||||
private final int slot;
|
||||
private final String name;
|
||||
private final ItemStack item;
|
||||
|
||||
public static void giveStart(Player player) {
|
||||
player.getInventory().setItemStack(BRIDGE_SLOT, bridge(1));
|
||||
player.getInventory().setItemStack(TOTEM_SLOT, totem(1));
|
||||
Jokers(int slot, Material material, String name, TextColor color, String lore) {
|
||||
this.slot = slot;
|
||||
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) {
|
||||
return item.material() == BRIDGE;
|
||||
public ItemStack item() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
public static void addBridge(Player player) {
|
||||
add(player, BRIDGE_SLOT, bridge(1));
|
||||
player.sendMessage(Component.text("+1 Brücke", NamedTextColor.GOLD));
|
||||
public boolean matches(ItemStack stack) {
|
||||
return stack.material() == this.item.material();
|
||||
}
|
||||
|
||||
public static void addTotem(Player player) {
|
||||
add(player, TOTEM_SLOT, totem(1));
|
||||
player.sendMessage(Component.text("+1 Rettungstotem", NamedTextColor.GOLD));
|
||||
public void add(Player player) {
|
||||
player.getInventory().replaceItemStack(this.slot, current -> this.matches(current) ? current.withAmount(a -> a + 1) : this.item);
|
||||
player.sendMessage(Component.text("+1 " + this.name, 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);
|
||||
/** Nimmt einen Joker weg; false, wenn keiner mehr da ist. */
|
||||
public boolean take(Player player) {
|
||||
if(!this.matches(player.getInventory().getItemStack(this.slot))) return false;
|
||||
player.getInventory().replaceItemStack(this.slot, current -> current.consume(1));
|
||||
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();
|
||||
public static void giveStart(Player player) {
|
||||
for(Jokers joker : values()) player.getInventory().setItemStack(joker.slot, joker.item);
|
||||
}
|
||||
|
||||
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();
|
||||
public static Jokers random(Random random) {
|
||||
return values()[random.nextInt(values().length)];
|
||||
}
|
||||
|
||||
public static Jokers of(ItemStack stack) {
|
||||
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.scoreboard.TabList;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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) {
|
||||
TAB_LIST.addViewer(player);
|
||||
scores.put(player, 0);
|
||||
// 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) {
|
||||
if(!scores.containsKey(player)) return;
|
||||
scores.put(player, score);
|
||||
TAB_LIST.updateScore(player, score);
|
||||
if(TAB_LIST.getViewers().contains(player)) TAB_LIST.updateScore(player, score);
|
||||
}
|
||||
|
||||
public static void leave(Player player) {
|
||||
scores.remove(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 static final List<Stage> STAGES = List.of(
|
||||
private static final List<Stage> STAGES = List.of(
|
||||
new Stage(0, "Wiese", List.of(
|
||||
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) {
|
||||
Stage current = STAGES.getFirst();
|
||||
for(Stage stage : STAGES) {
|
||||
if(index >= stage.from()) current = stage;
|
||||
for(Stage stage : STAGES.reversed()) {
|
||||
if(index >= stage.from()) return stage;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
return STAGES.getFirst();
|
||||
}
|
||||
|
||||
public static Block getRandom(int index, Random random) {
|
||||
|
||||
@@ -1,39 +1,47 @@
|
||||
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.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.Vec;
|
||||
import net.minestom.server.entity.ItemEntity;
|
||||
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.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.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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 {
|
||||
private final Player player;
|
||||
private final InstanceContainer world;
|
||||
private final ArrayList<Pos> blocks = new ArrayList<>();
|
||||
private Pos checkpoint;
|
||||
private Pos starting;
|
||||
private record Step(Pos pos, Block block) {
|
||||
}
|
||||
|
||||
private int combo = 0;
|
||||
// Block, von dem der nächste Sprung startet (bestimmt, ob ein Sprung nach oben möglich ist)
|
||||
private Block lastBlock = Block.DIAMOND_BLOCK;
|
||||
|
||||
private final int preview = 3;
|
||||
private record BlockKey(int x, int y, int z) {
|
||||
static BlockKey of(Point point) {
|
||||
return new BlockKey(point.blockX(), point.blockY(), point.blockZ());
|
||||
}
|
||||
}
|
||||
|
||||
private static final int PREVIEW = 3;
|
||||
private static final int FULL_DIFFICULTY_AT = 1000;
|
||||
|
||||
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 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
|
||||
private final Map<Pos, Block> rescueBlocks = new HashMap<>();
|
||||
private Pos rescueSafe;
|
||||
private int rescueFloorY;
|
||||
|
||||
|
||||
public JumpGenerator(Pos starting, Player player, InstanceContainer world) {
|
||||
public JumpGenerator(Pos start, Player player, Instance world) {
|
||||
this.player = player;
|
||||
this.world = world;
|
||||
this.starting = starting;
|
||||
generateStart(starting);
|
||||
this.checkpoint = start;
|
||||
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() {
|
||||
boolean standingTenth = (this.blocks.size()-this.preview-1) % 10 == 0;
|
||||
private static boolean isCheckpoint(int index) {
|
||||
return index % 10 == 0;
|
||||
}
|
||||
|
||||
float pitch = 1 + (combo * 0.025f);
|
||||
this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, pitch));
|
||||
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);
|
||||
private static boolean isMilestone(int index) {
|
||||
return index % 100 == 0;
|
||||
}
|
||||
|
||||
if(score > 0 && score % 100 == 0) Jokers.addBridge(this.player);
|
||||
if(score > 0 && score % 200 == 0) Jokers.addTotem(this.player);
|
||||
private static double height(Block block) {
|
||||
return block.collisionShape().relativeEnd().y();
|
||||
}
|
||||
|
||||
this.generateBlock(0);
|
||||
private Pos landing() {
|
||||
return this.course.get(this.standing + 1).pos();
|
||||
}
|
||||
|
||||
if(standingTenth) {
|
||||
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))));
|
||||
this.player.sendPacket(new ParticlePacket(Particle.CRIT, current, current.add(0.5), 1.5f, 50));
|
||||
/** Wird bei jeder Bewegung aufgerufen: erkennt Landungen und behandelt das Runterfallen. */
|
||||
public void onMove(Pos newPosition) {
|
||||
// Block unter den Füßen; -0.5 trifft auch niedrige Blöcke wie den Endportalrahmen
|
||||
Integer index = this.courseIndex.get(BlockKey.of(newPosition.sub(0, 0.5, 0)));
|
||||
if(index != null && index > this.standing) this.advanceTo(index);
|
||||
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)
|
||||
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)));
|
||||
BlockPalette.Stage stage = BlockPalette.stageAt(index);
|
||||
if(stage != BlockPalette.stageAt(previous)) {
|
||||
this.player.playSound(STAGE_SOUND);
|
||||
this.player.sendActionBar(Component.text(stage.name(), NamedTextColor.GOLD));
|
||||
} 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++;
|
||||
|
||||
this.ensureGenerated(index + PREVIEW + 1, 0);
|
||||
}
|
||||
|
||||
/** 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, () -> {
|
||||
private void ensureGenerated(int upTo, int stepTicks) {
|
||||
int delay = 0;
|
||||
while(this.course.size() <= upTo) {
|
||||
this.generateStep(delay, null);
|
||||
delay += stepTicks;
|
||||
}
|
||||
}
|
||||
|
||||
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.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(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
|
||||
* 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);
|
||||
|
||||
int dz = r.nextDouble() < 0.1 + 0.4 * difficulty ? 4 : 3;
|
||||
|
||||
int dx = r.nextDouble() < 0.1 + 0.57 * difficulty ? (r.nextBoolean() ? 1 : -1) : 0;
|
||||
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;
|
||||
|
||||
double y = this.player.getPosition().y();
|
||||
double roll = r.nextDouble();
|
||||
double roll = this.random.nextDouble();
|
||||
int dy = 0;
|
||||
if(roll < 0.05 + 0.28 * difficulty && y <= 120) 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
|
||||
if(dy == 1 && this.lastBlock.collisionShape().relativeEnd().y() < 1) dy = 0;
|
||||
// die Oberkante darf höchstens einen Block höher liegen (z. B. vom Endportalrahmen aus)
|
||||
if(dy == 1 && height(to) > height(from)) dy = 0;
|
||||
|
||||
// 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 && 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
|
||||
* mit einem begehbaren Weg. Die übersprungenen Blöcke zählen nicht zur Combo.
|
||||
* Joker „Brücke“: erzeugt die Strecke bis zum nächsten Checkpoint und verbindet sie mit einem
|
||||
* begehbaren Weg. Diese Blöcke zählen nicht zur Combo. Mehrfacher Aufruf baut nichts doppelt.
|
||||
*/
|
||||
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;
|
||||
int nextCheckpoint = (standing / 10 + 1) * 10;
|
||||
|
||||
// so weit vorgenerieren, dass der Checkpoint der nächste Landeblock ist
|
||||
// bereits stehende Abschnitte sofort, neue Blöcke bekommen ihren Weg bei Ankunft
|
||||
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());
|
||||
}
|
||||
int delay = 0;
|
||||
while(this.blocks.size() - this.preview - 1 < nextCheckpoint) {
|
||||
this.generateBlock(delay);
|
||||
while(this.course.size() <= nextCheckpoint) {
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
this.player.playSound(BRIDGE_SOUND);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -175,56 +240,52 @@ public class JumpGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/** Wird bei jeder Bewegung aufgerufen: Rettung durch Totem oder Reset beim Runterfallen. */
|
||||
public void checkFall() {
|
||||
private 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) {
|
||||
else if(y < this.rescueSafe.blockY() - RESCUE_DEPTH - 3) {
|
||||
this.endRescue();
|
||||
this.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(y < this.getNextLanding().y() - 5 && y < this.checkpoint.y()) {
|
||||
if(Jokers.takeTotem(this.player)) this.rescue();
|
||||
if(y < this.landing().y() - 5 && y < this.checkpoint.y()) {
|
||||
if(Jokers.TOTEM.take(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.
|
||||
* 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() {
|
||||
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 z = safe.blockZ();
|
||||
int floorY = safe.blockY() - RESCUE_DEPTH;
|
||||
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);
|
||||
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)
|
||||
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 - 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.teleport(new Pos(x + 0.5, floorY + 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));
|
||||
this.player.playSound(RESCUE_SOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** Brückenglas darf überschrieben werden, sonst könnte ein früherer Brückenweg die Leiter versperren. */
|
||||
private void placeRescue(Pos pos, Block block) {
|
||||
Block existing = this.world.getBlock(pos);
|
||||
if(!existing.compare(Block.AIR) && !existing.compare(BRIDGE_BLOCK)) return;
|
||||
@@ -234,62 +295,27 @@ public class JumpGenerator {
|
||||
|
||||
private void endRescue() {
|
||||
this.rescueSafe = null;
|
||||
Map<Pos, Block> toRemove = new HashMap<>(this.rescueBlocks);
|
||||
Map<Pos, Block> placed = Map.copyOf(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);
|
||||
removeLater(this.world, placed);
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
||||
/** 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;
|
||||
/** Streckenblöcke und ihr Licht sind geschützt, alles andere (Brücke, Rettung) darf abgebaut werden. */
|
||||
public boolean isBreakable(Point pos) {
|
||||
return !this.world.getBlock(pos).compare(Block.LIGHT) && !this.courseIndex.containsKey(BlockKey.of(pos));
|
||||
}
|
||||
|
||||
public Pos getNextLanding() {
|
||||
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();
|
||||
*/
|
||||
private void reset() {
|
||||
this.player.teleport(this.checkpoint.add(0.5, 1, 0.5));
|
||||
this.combo = 0;
|
||||
this.player.showTitle(Title.title(Component.text(""), Component.text("")));
|
||||
|
||||
//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;
|
||||
this.player.clearTitle();
|
||||
this.player.playSound(RESET_SOUND);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user