Compare commits
23
Commits
0faff2bd0c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbce781099 | ||
|
|
05f2fcb9da | ||
|
|
d4465063d4 | ||
|
|
c9da982285 | ||
|
|
f2b7602321 | ||
|
|
a262010c82 | ||
|
|
38bb9c6dc8 | ||
|
|
555f4024e4 | ||
|
|
7e85a6d229 | ||
|
|
40bedc3c43 | ||
|
|
de3578eafe | ||
|
|
249cd0c08a | ||
|
|
e2c574bf0b | ||
|
|
115064cafa | ||
|
|
806d76e560 | ||
|
|
137e162ea4 | ||
|
|
de6b987ffd | ||
|
|
ef1662b144 | ||
|
|
f56a3765c6 | ||
|
|
9e1edbfc7c | ||
|
|
8ba25758be | ||
|
|
b1950dd664 | ||
|
|
452f2f3ef7 |
No files matched your search
@@ -4,3 +4,6 @@ target/
|
|||||||
*.iml
|
*.iml
|
||||||
.idea/workspace.xml
|
.idea/workspace.xml
|
||||||
forwarding.secret
|
forwarding.secret
|
||||||
|
progress.properties
|
||||||
|
progress.properties.tmp
|
||||||
|
admins.txt
|
||||||
+10
-2
@@ -29,12 +29,20 @@ shadowJar {
|
|||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hochladen auf den Limbo: ./gradlew uploadLimbo, danach `sudo systemctl restart limbo`
|
// Hochladen auf den Limbo: ./gradlew uploadLimbo, danach `sudo systemctl restart limbo`.
|
||||||
|
// Erst in eine Temp-Datei, Prüfsumme vergleichen, dann atomar umbenennen: ein abgebrochener Upload
|
||||||
|
// lässt das laufende JAR unberührt, und der laufende Prozess behält bis zum Neustart sein altes JAR.
|
||||||
tasks.register('uploadLimbo') {
|
tasks.register('uploadLimbo') {
|
||||||
dependsOn shadowJar
|
dependsOn shadowJar
|
||||||
doLast {
|
doLast {
|
||||||
|
def jar = shadowJar.archiveFile.get().asFile
|
||||||
|
def checksum = jar.bytes.digest('SHA-256')
|
||||||
providers.exec {
|
providers.exec {
|
||||||
commandLine 'scp', shadowJar.archiveFile.get().asFile.absolutePath, 'minecraft@10.20.9.3:/home/minecraft/limbo/limbo.jar'
|
commandLine 'scp', jar.absolutePath, 'minecraft@10.20.9.3:/home/minecraft/limbo/limbo.jar.new'
|
||||||
|
}.result.get()
|
||||||
|
providers.exec {
|
||||||
|
commandLine 'ssh', 'minecraft@10.20.9.3',
|
||||||
|
"cd ~/limbo && echo '${checksum} limbo.jar.new' | sha256sum -c --quiet && mv -f limbo.jar.new limbo.jar"
|
||||||
}.result.get()
|
}.result.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.mhsl.minecraft.endlessjumper;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admins aus admins.txt im Arbeitsverzeichnis: eine UUID pro Zeile, „#“ leitet einen Kommentar ein.
|
||||||
|
* Der Limbo kennt kein LuckPerms, die Datei wird aus dessen Admin-Gruppen erzeugt.
|
||||||
|
*/
|
||||||
|
public class Admins {
|
||||||
|
private static final Path FILE = Path.of("admins.txt");
|
||||||
|
private static Set<UUID> admins = Set.of();
|
||||||
|
|
||||||
|
public static void load() throws IOException {
|
||||||
|
if(!Files.exists(FILE)) return;
|
||||||
|
admins = Files.readAllLines(FILE).stream()
|
||||||
|
.map(line -> line.replaceFirst("#.*", "").trim())
|
||||||
|
.filter(line -> !line.isEmpty())
|
||||||
|
.map(UUID::fromString)
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean is(Player player) {
|
||||||
|
return admins.contains(player.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,89 +3,141 @@ package eu.mhsl.minecraft.endlessjumper;
|
|||||||
import eu.mhsl.minecraft.endlessjumper.generator.JumpGenerator;
|
import eu.mhsl.minecraft.endlessjumper.generator.JumpGenerator;
|
||||||
import net.minestom.server.Auth;
|
import net.minestom.server.Auth;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.command.builder.Command;
|
||||||
|
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||||
|
import net.minestom.server.command.builder.arguments.ArgumentWord;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
import net.minestom.server.coordinate.Pos;
|
import net.minestom.server.coordinate.Pos;
|
||||||
import net.minestom.server.entity.Player;
|
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.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.PlayerChatEvent;
|
||||||
import net.minestom.server.event.player.PlayerDisconnectEvent;
|
import net.minestom.server.event.player.PlayerDisconnectEvent;
|
||||||
import net.minestom.server.event.player.PlayerMoveEvent;
|
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.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.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
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.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class EndlessJumper {
|
public class EndlessJumper {
|
||||||
|
private static final Pos START = new Pos(0, 100, 0);
|
||||||
|
private static final Pos SPAWN = START.add(0.5, 1, 0.5);
|
||||||
|
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();
|
Progress.load();
|
||||||
|
Admins.load();
|
||||||
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(SPAWN);
|
||||||
|
|
||||||
final InstanceContainer world = instanceManager.createInstanceContainer();
|
|
||||||
world.setGenerator(new TerrainGenerator());
|
|
||||||
|
|
||||||
Pos startingAt = new Pos(0,100,0);
|
|
||||||
world.setBlock(startingAt, Block.DIAMOND_BLOCK);
|
|
||||||
|
|
||||||
world.defaultClock().pause();
|
|
||||||
world.setTime(18000);
|
|
||||||
|
|
||||||
p.setSkin(skin);
|
p.setSkin(skin);
|
||||||
|
event.setSpawningInstance(createWorld());
|
||||||
|
});
|
||||||
|
|
||||||
event.setSpawningInstance(world);
|
eventHandler.addListener(PlayerSpawnEvent.class, event -> {
|
||||||
|
if(!event.isFirstSpawn()) return;
|
||||||
MinecraftServer.getSchedulerManager().scheduleTask(
|
Player p = event.getPlayer();
|
||||||
() -> games.put(p, new JumpGenerator(startingAt, p, world)),
|
p.setCanPickupItem(true);
|
||||||
TaskSchedule.seconds(3),
|
ScoreTab.join(p);
|
||||||
TaskSchedule.stop()
|
Jokers.clear(p);
|
||||||
);
|
// sobald der Spieler in der Welt steht, geht es los
|
||||||
|
games.put(p, new JumpGenerator(START, Progress.checkpoint(p.getUuid()), p, p.getInstance()));
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
//reset when falling down
|
|
||||||
if(p.getPosition().y() < generator.getNextLanding().y()-5 && p.getPosition().y() < generator.getCheckpoint().y()) generator.reset();
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
eventHandler.addListener(PlayerBlockBreakEvent.class, event -> event.setCancelled(true));
|
eventHandler.addListener(PlayerBlockBreakEvent.class, event -> 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));
|
||||||
|
// den Chat verteilt Velocity (chat-bridge) an das ganze Netzwerk, hier würde er doppelt stehen
|
||||||
|
eventHandler.addListener(PlayerChatEvent.class, event -> event.setCancelled(true));
|
||||||
|
|
||||||
eventHandler.addListener(PlayerDisconnectEvent.class, event -> games.remove(event.getPlayer()));
|
// der Client meldet Rechtsklicks auch auf Blöcke als Item-Nutzung, solange der Block sie nicht
|
||||||
|
// selbst verarbeitet (auf der Strecke nie) – ein Event genügt, Joker werden nicht doppelt verbraucht
|
||||||
|
eventHandler.addListener(PlayerUseItemEvent.class, event -> {
|
||||||
|
JumpGenerator generator = games.get(event.getPlayer());
|
||||||
|
Jokers joker = Jokers.of(event.getItemStack());
|
||||||
|
if(generator != null && joker != null && generator.useJoker(joker)) joker.take(event.getPlayer());
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler.addListener(PickupItemEvent.class, event -> {
|
||||||
|
Jokers joker = Jokers.of(event.getItemStack());
|
||||||
|
if(event.getLivingEntity() instanceof Player player && joker != null) joker.add(player);
|
||||||
|
else event.setCancelled(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler.addListener(PlayerDisconnectEvent.class, event -> {
|
||||||
|
Player p = event.getPlayer();
|
||||||
|
games.remove(p);
|
||||||
|
ScoreTab.leave(p);
|
||||||
|
// jede Partie hat eine eigene Welt: nach dem Verlassen freigeben
|
||||||
|
releaseLater(p.getInstance());
|
||||||
|
});
|
||||||
|
|
||||||
|
Command reset = new Command("reset");
|
||||||
|
reset.setDefaultExecutor((sender, context) -> {
|
||||||
|
if(sender instanceof Player player) resetGame(player);
|
||||||
|
});
|
||||||
|
MinecraftServer.getCommandManager().register(reset);
|
||||||
|
|
||||||
|
// nur für Admins (admins.txt): Joker zum Testen direkt geben
|
||||||
|
Command joker = new Command("joker");
|
||||||
|
joker.setCondition((sender, commandString) -> sender instanceof Player player && Admins.is(player));
|
||||||
|
ArgumentWord type = ArgumentType.Word("typ").from(Jokers.keys());
|
||||||
|
joker.addSyntax((sender, context) -> Jokers.byKey(context.get(type)).add((Player) sender), type);
|
||||||
|
MinecraftServer.getCommandManager().register(joker);
|
||||||
|
|
||||||
server.start("0.0.0.0", 25501);
|
server.start("0.0.0.0", 25501);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InstanceContainer createWorld() {
|
||||||
|
InstanceContainer world = MinecraftServer.getInstanceManager().createInstanceContainer();
|
||||||
|
world.setBlock(START, Block.DIAMOND_BLOCK);
|
||||||
|
// Tageszeit steuert der jeweilige Abschnitt
|
||||||
|
world.defaultClock().pause();
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void releaseLater(Instance world) {
|
||||||
|
if(world != null) MinecraftServer.getSchedulerManager().scheduleNextTick(() -> MinecraftServer.getInstanceManager().unregisterInstance(world));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** /reset: Fortschritt, Strecke, Score und Joker zurücksetzen und in einer frischen Welt bei 0 beginnen. */
|
||||||
|
private static void resetGame(Player player) {
|
||||||
|
JumpGenerator old = games.remove(player);
|
||||||
|
if(old != null) old.close();
|
||||||
|
Progress.reset(player.getUuid());
|
||||||
|
|
||||||
|
Instance oldWorld = player.getInstance();
|
||||||
|
InstanceContainer world = createWorld();
|
||||||
|
player.setVelocity(Vec.ZERO);
|
||||||
|
player.setInstance(world, SPAWN).thenRun(() -> {
|
||||||
|
Jokers.clear(player);
|
||||||
|
games.put(player, new JumpGenerator(START, 0, player, world));
|
||||||
|
releaseLater(oldWorld);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wie bei Velocity: Secret aus forwarding.secret im Arbeitsverzeichnis, alternativ per Umgebungsvariable
|
// Wie bei Velocity: Secret aus forwarding.secret im Arbeitsverzeichnis, alternativ per Umgebungsvariable
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
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. Es gibt sie zum Einsammeln auf der
|
||||||
|
* Strecke; wer bei 0 beginnt, bekommt ein Totem. Das Totem wirkt automatisch, die anderen per Rechtsklick (siehe JumpGenerator#useJoker).
|
||||||
|
*/
|
||||||
|
public enum Jokers {
|
||||||
|
BRIDGE("bruecke", 0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg über die nächsten 10 Blöcke"),
|
||||||
|
FEATHER_FALL("federfall", 1, Material.FEATHER, "Federfall", NamedTextColor.WHITE, "Rechtsklick: 15 Sekunden langsames Fallen"),
|
||||||
|
SPRING("sprungfeder", 2, Material.RABBIT_FOOT, "Sprungfeder", NamedTextColor.GREEN, "Rechtsklick: die nächsten 10 Sprünge mit Sprungkraft"),
|
||||||
|
TOTEM("totem", 8, Material.TOTEM_OF_UNDYING, "Rettungstotem", NamedTextColor.GOLD, "Rettet dich automatisch beim Runterfallen");
|
||||||
|
|
||||||
|
// Name im Befehl /joker (Brigadier erlaubt keine Umlaute)
|
||||||
|
private final String key;
|
||||||
|
private final int slot;
|
||||||
|
private final String name;
|
||||||
|
private final ItemStack item;
|
||||||
|
|
||||||
|
Jokers(String key, int slot, Material material, String name, TextColor color, String lore) {
|
||||||
|
this.key = key;
|
||||||
|
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 ItemStack item() {
|
||||||
|
return this.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(ItemStack stack) {
|
||||||
|
return stack.material() == this.item.material();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zum Start (und nach /reset) gibt es keine Joker. */
|
||||||
|
public static void clear(Player player) {
|
||||||
|
for(Jokers joker : values()) player.getInventory().setItemStack(joker.slot, ItemStack.AIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Jokers random(Random random) {
|
||||||
|
return values()[random.nextInt(values().length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] keys() {
|
||||||
|
String[] keys = new String[values().length];
|
||||||
|
for(Jokers joker : values()) keys[joker.ordinal()] = joker.key;
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Jokers byKey(String key) {
|
||||||
|
for(Jokers joker : values()) {
|
||||||
|
if(joker.key.equals(key)) return joker;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Jokers of(ItemStack stack) {
|
||||||
|
for(Jokers joker : values()) {
|
||||||
|
if(joker.matches(stack)) return joker;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package eu.mhsl.minecraft.endlessjumper;
|
||||||
|
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Letzter erreichter Checkpoint pro Spieler, damit man beim nächsten Besuch dort weiterspielt.
|
||||||
|
* Liegt als progress.properties im Arbeitsverzeichnis; für ein neues Projekt einfach löschen.
|
||||||
|
*/
|
||||||
|
public class Progress {
|
||||||
|
private static final Path FILE = Path.of("progress.properties");
|
||||||
|
private static final Path TEMP = Path.of("progress.properties.tmp");
|
||||||
|
// Properties ist synchronisiert; geschrieben wird nacheinander außerhalb des Server-Ticks
|
||||||
|
private static final Properties checkpoints = new Properties();
|
||||||
|
private static final ExecutorService writer = Executors.newSingleThreadExecutor(Thread.ofVirtual().factory());
|
||||||
|
|
||||||
|
public static void load() throws IOException {
|
||||||
|
if(!Files.exists(FILE)) return;
|
||||||
|
try(Reader reader = Files.newBufferedReader(FILE)) {
|
||||||
|
checkpoints.load(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int checkpoint(UUID player) {
|
||||||
|
return Integer.parseInt(checkpoints.getProperty(player.toString(), "0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(UUID player, int checkpoint) {
|
||||||
|
checkpoints.setProperty(player.toString(), String.valueOf(checkpoint));
|
||||||
|
writer.execute(Progress::write);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reset(UUID player) {
|
||||||
|
checkpoints.remove(player.toString());
|
||||||
|
writer.execute(Progress::write);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erst in eine Temp-Datei, dann atomar umbenennen: ein Absturz hinterlässt nie eine halbe Datei. */
|
||||||
|
private static void write() {
|
||||||
|
try {
|
||||||
|
try(Writer out = Files.newBufferedWriter(TEMP)) {
|
||||||
|
checkpoints.store(out, "EndlessJumper: letzter Checkpoint pro Spieler");
|
||||||
|
}
|
||||||
|
Files.move(TEMP, FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} catch(IOException e) {
|
||||||
|
MinecraftServer.getExceptionManager().handleException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package eu.mhsl.minecraft.endlessjumper;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.Player;
|
||||||
|
import net.minestom.server.network.packet.server.play.ScoreboardObjectivePacket;
|
||||||
|
import net.minestom.server.scoreboard.TabList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
public static void join(Player player) {
|
||||||
|
TAB_LIST.addViewer(player);
|
||||||
|
// dem neuen Spieler auch die Scores aller anderen zeigen
|
||||||
|
TAB_LIST.getViewers().forEach(viewer -> TAB_LIST.updateScore(viewer, viewer.getLevel()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void update(Player player, int score) {
|
||||||
|
if(TAB_LIST.getViewers().contains(player)) TAB_LIST.updateScore(player, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void leave(Player 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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +1,111 @@
|
|||||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||||
|
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.particle.Particle;
|
||||||
|
import net.minestom.server.sound.SoundEvent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block-Themen entlang des Fortschritts: einmal durch das komplette Spiel,
|
* Abschnitte entlang des Fortschritts: einmal durch das komplette Spiel, von der Wiese über
|
||||||
* von der Wiese über Untergrund, Nether und Stronghold bis ins End (ab Sprung 1000).
|
* Untergrund, Nether und Stronghold bis ins End; ab Sprung 1000 geht es endlos weiter, alle 50 Sprünge
|
||||||
|
* mit einem zufälligen Thema aus diesen Abschnitten. Jeder Abschnitt hat eigene
|
||||||
|
* Blöcke und eine eigene Stimmung (Tageszeit, Partikel, Musik).
|
||||||
*/
|
*/
|
||||||
public class BlockPalette {
|
public class BlockPalette {
|
||||||
public record Stage(int from, String name, List<Block> blocks) {
|
/** @param ambient Partikel um den Spieler, {@code null} für keine */
|
||||||
|
public record Stage(int from, String name, long time, Particle ambient, SoundEvent music, List<Block> blocks) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final List<Stage> STAGES = List.of(
|
private static final long MORNING = 1000, NOON = 6000, NIGHT = 18000;
|
||||||
new Stage(0, "Wiese", List.of(
|
|
||||||
|
private static final List<Stage> STAGES = List.of(
|
||||||
|
new Stage(0, "Wiese", MORNING, null, SoundEvent.MUSIC_OVERWORLD_MEADOW, 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
|
||||||
)),
|
)),
|
||||||
new Stage(75, "Wald", List.of(
|
new Stage(70, "Wald", NOON, Particle.CHERRY_LEAVES, SoundEvent.MUSIC_OVERWORLD_FOREST, List.of(
|
||||||
Block.OAK_LOG, Block.BIRCH_LOG, Block.SPRUCE_LOG, Block.CHERRY_LOG,
|
Block.OAK_LOG, Block.BIRCH_LOG, Block.SPRUCE_LOG, Block.CHERRY_LOG,
|
||||||
Block.OAK_LEAVES, Block.BIRCH_LEAVES, Block.SPRUCE_LEAVES, Block.CHERRY_LEAVES
|
Block.OAK_LEAVES, Block.BIRCH_LEAVES, Block.SPRUCE_LEAVES, Block.CHERRY_LEAVES
|
||||||
)),
|
)),
|
||||||
new Stage(150, "Untergrund", List.of(
|
new Stage(140, "Untergrund", NIGHT, null, SoundEvent.MUSIC_OVERWORLD_DRIPSTONE_CAVES, List.of(
|
||||||
Block.DIRT, Block.COARSE_DIRT, Block.STONE, Block.STONE, Block.ANDESITE,
|
Block.DIRT, Block.COARSE_DIRT, Block.STONE, Block.STONE, Block.ANDESITE,
|
||||||
Block.GRANITE, Block.DIORITE, Block.COBBLESTONE
|
Block.GRANITE, Block.DIORITE, Block.COBBLESTONE
|
||||||
)),
|
)),
|
||||||
new Stage(250, "Höhlen", List.of(
|
new Stage(220, "Höhlen", NIGHT, Particle.DRIPPING_WATER, SoundEvent.MUSIC_OVERWORLD_LUSH_CAVES, List.of(
|
||||||
Block.STONE, Block.COAL_ORE, Block.IRON_ORE, Block.COPPER_ORE,
|
Block.STONE, Block.COAL_ORE, Block.IRON_ORE, Block.COPPER_ORE,
|
||||||
Block.GOLD_ORE, Block.REDSTONE_ORE, Block.LAPIS_ORE
|
Block.GOLD_ORE, Block.REDSTONE_ORE, Block.LAPIS_ORE
|
||||||
)),
|
)),
|
||||||
new Stage(350, "Tiefenschiefer", List.of(
|
new Stage(300, "Tiefenschiefer", NIGHT, Particle.SCULK_SOUL, SoundEvent.MUSIC_OVERWORLD_DEEP_DARK, List.of(
|
||||||
Block.DEEPSLATE, Block.DEEPSLATE, Block.TUFF, Block.DEEPSLATE_DIAMOND_ORE,
|
Block.DEEPSLATE, Block.DEEPSLATE, Block.TUFF, Block.DEEPSLATE_DIAMOND_ORE,
|
||||||
Block.DEEPSLATE_IRON_ORE, Block.AMETHYST_BLOCK
|
Block.DEEPSLATE_IRON_ORE, Block.AMETHYST_BLOCK
|
||||||
)),
|
)),
|
||||||
new Stage(450, "Netherportal", List.of(
|
new Stage(390, "Netherportal", NIGHT, Particle.PORTAL, SoundEvent.MUSIC_NETHER_NETHER_WASTES, List.of(
|
||||||
Block.OBSIDIAN, Block.OBSIDIAN, Block.CRYING_OBSIDIAN
|
Block.OBSIDIAN, Block.OBSIDIAN, Block.CRYING_OBSIDIAN
|
||||||
)),
|
)),
|
||||||
new Stage(475, "Nether", List.of(
|
new Stage(410, "Nether", NIGHT, Particle.CRIMSON_SPORE, SoundEvent.MUSIC_NETHER_CRIMSON_FOREST, List.of(
|
||||||
Block.NETHERRACK, Block.NETHERRACK, Block.CRIMSON_NYLIUM, Block.WARPED_NYLIUM,
|
Block.NETHERRACK, Block.NETHERRACK, Block.CRIMSON_NYLIUM, Block.WARPED_NYLIUM,
|
||||||
Block.SOUL_SOIL, Block.GLOWSTONE, Block.NETHER_QUARTZ_ORE, Block.NETHER_GOLD_ORE
|
Block.SOUL_SOIL, Block.GLOWSTONE, Block.NETHER_QUARTZ_ORE, Block.NETHER_GOLD_ORE
|
||||||
)),
|
)),
|
||||||
new Stage(625, "Festung & Bastion", List.of(
|
new Stage(540, "Festung & Bastion", NIGHT, Particle.WHITE_ASH, SoundEvent.MUSIC_NETHER_BASALT_DELTAS, List.of(
|
||||||
Block.NETHER_BRICKS, Block.RED_NETHER_BRICKS, Block.BLACKSTONE,
|
Block.NETHER_BRICKS, Block.RED_NETHER_BRICKS, Block.BLACKSTONE,
|
||||||
Block.POLISHED_BLACKSTONE_BRICKS, Block.GILDED_BLACKSTONE, Block.BASALT
|
Block.POLISHED_BLACKSTONE_BRICKS, Block.GILDED_BLACKSTONE, Block.BASALT
|
||||||
)),
|
)),
|
||||||
new Stage(750, "Stronghold", List.of(
|
new Stage(660, "Stronghold", NIGHT, Particle.ENCHANT, SoundEvent.MUSIC_DISC_MELLOHI, List.of(
|
||||||
Block.STONE_BRICKS, Block.MOSSY_STONE_BRICKS, Block.CRACKED_STONE_BRICKS,
|
Block.STONE_BRICKS, Block.MOSSY_STONE_BRICKS, Block.CRACKED_STONE_BRICKS,
|
||||||
Block.CHISELED_STONE_BRICKS, Block.BOOKSHELF
|
Block.CHISELED_STONE_BRICKS, Block.BOOKSHELF
|
||||||
)),
|
)),
|
||||||
new Stage(925, "Portalraum", List.of(
|
new Stage(800, "Portalraum", NIGHT, Particle.REVERSE_PORTAL, SoundEvent.MUSIC_DRAGON, List.of(
|
||||||
Block.END_PORTAL_FRAME, Block.END_PORTAL_FRAME.withProperty("eye", "true"),
|
Block.END_PORTAL_FRAME, Block.END_PORTAL_FRAME.withProperty("eye", "true"),
|
||||||
Block.STONE_BRICKS, Block.MOSSY_STONE_BRICKS
|
Block.STONE_BRICKS, Block.MOSSY_STONE_BRICKS
|
||||||
)),
|
)),
|
||||||
new Stage(1000, "End", List.of(
|
new Stage(850, "End", NIGHT, Particle.END_ROD, SoundEvent.MUSIC_END, List.of(
|
||||||
Block.END_STONE, Block.END_STONE, Block.END_STONE_BRICKS,
|
Block.END_STONE, Block.END_STONE, Block.END_STONE_BRICKS,
|
||||||
Block.PURPUR_BLOCK, Block.PURPUR_PILLAR, Block.OBSIDIAN
|
Block.PURPUR_BLOCK, Block.PURPUR_PILLAR, Block.OBSIDIAN
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ab hier endlos: alle ENDLESS_SEGMENT Sprünge ein zufälliges Thema aus den Abschnitten oben
|
||||||
|
private static final int ENDLESS_FROM = 1000;
|
||||||
|
private static final int ENDLESS_SEGMENT = 50;
|
||||||
|
private static final Map<Integer, Stage> endlessStages = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public static Stage stageAt(int index) {
|
public static Stage stageAt(int index) {
|
||||||
Stage current = STAGES.getFirst();
|
if(index >= ENDLESS_FROM) return endlessStage((index - ENDLESS_FROM) / ENDLESS_SEGMENT);
|
||||||
for(Stage stage : STAGES) {
|
for(Stage stage : STAGES.reversed()) {
|
||||||
if(index >= stage.from()) current = stage;
|
if(index >= stage.from()) return stage;
|
||||||
}
|
}
|
||||||
return current;
|
return STAGES.getFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Liefert die Stufe, die genau bei diesem Index beginnt, sonst null. */
|
public static boolean isEndless(Stage stage) {
|
||||||
public static Stage stageStartingAt(int index) {
|
return stage.from() >= ENDLESS_FROM;
|
||||||
for(Stage stage : STAGES) {
|
}
|
||||||
if(stage.from() == index) return stage;
|
|
||||||
|
/**
|
||||||
|
* Thema eines endlosen Abschnitts. Fest an die Abschnittsnummer gebunden (gleiches Thema beim
|
||||||
|
* Weiterspielen) und nie zweimal dasselbe hintereinander. Zwischengespeichert, damit jeder
|
||||||
|
* Abschnitt genau ein Stage-Objekt hat (Abschnittswechsel werden per Identität erkannt).
|
||||||
|
*/
|
||||||
|
private static Stage endlessStage(int segment) {
|
||||||
|
return endlessStages.computeIfAbsent(segment, n -> {
|
||||||
|
Stage theme = themeOf(n);
|
||||||
|
return new Stage(ENDLESS_FROM + n * ENDLESS_SEGMENT, "Unendlich · " + theme.name(), theme.time(), theme.ambient(), theme.music(), theme.blocks());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Würfelt die Themen ab Abschnitt 0 der Reihe nach, damit „nie zweimal hintereinander“ sicher gilt. */
|
||||||
|
private static Stage themeOf(int segment) {
|
||||||
|
int previous = -1;
|
||||||
|
int pick = 0;
|
||||||
|
for(int n = 0; n <= segment; n++) {
|
||||||
|
pick = new Random(n * 31L + 7).nextInt(STAGES.size());
|
||||||
|
if(pick == previous) pick = (pick + 1) % STAGES.size();
|
||||||
|
previous = pick;
|
||||||
}
|
}
|
||||||
return null;
|
return STAGES.get(pick);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Block getRandom(int index, Random random) {
|
public static Block getRandom(int index, Random random) {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||||
|
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
|
import net.minestom.server.entity.Entity;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.entity.metadata.display.BlockDisplayMeta;
|
||||||
|
import net.minestom.server.instance.Instance;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lässt einen neuen Block als Block-Display klein oben aus dem vorherigen Block herauswachsen
|
||||||
|
* und zu seiner Zielposition gleiten. Erst bei Ankunft wird der echte Block gesetzt.
|
||||||
|
*/
|
||||||
|
public class BlockTransition {
|
||||||
|
public static final int DURATION_TICKS = 8;
|
||||||
|
/** Zeit vom Start bis der echte Block steht (Spawn-Vorlauf + Animation + Puffer). */
|
||||||
|
public static final int TOTAL_TICKS = DURATION_TICKS + 3;
|
||||||
|
private static final double START_SCALE = 0.3;
|
||||||
|
// minimal kleiner als der Block: sobald der echte Block steht, verdeckt er das Display ohne Z-Fighting
|
||||||
|
private static final double END_SCALE = 0.998;
|
||||||
|
// der Client braucht nach dem Setzen ein, zwei Frames bis der Block sichtbar ist: so lange bleibt das Display
|
||||||
|
private static final int OVERLAP_TICKS = 4;
|
||||||
|
|
||||||
|
public static void animate(Instance world, Pos from, Pos to, Block block, int delayTicks, Runnable onArrival) {
|
||||||
|
world.scheduler().buildTask(() -> spawn(world, from, to, block, onArrival))
|
||||||
|
.delay(TaskSchedule.tick(Math.max(1, delayTicks)))
|
||||||
|
.schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void spawn(Instance world, Pos from, Pos to, Block block, Runnable onArrival) {
|
||||||
|
Entity display = new Entity(EntityType.BLOCK_DISPLAY);
|
||||||
|
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
|
||||||
|
meta.setScale(new Vec(START_SCALE));
|
||||||
|
meta.setTranslation(new Vec((1 - START_SCALE) / 2, 1, (1 - START_SCALE) / 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
display.setInstance(world, from).thenRun(() -> {
|
||||||
|
// kurz warten, damit der Client das Display kennt, bevor die Interpolation startet
|
||||||
|
display.scheduler().buildTask(() -> {
|
||||||
|
display.editEntityMeta(BlockDisplayMeta.class, meta -> {
|
||||||
|
meta.setTransformationInterpolationStartDelta(0);
|
||||||
|
meta.setScale(new Vec(END_SCALE));
|
||||||
|
meta.setTranslation(new Vec((1 - END_SCALE) / 2));
|
||||||
|
});
|
||||||
|
display.teleport(to);
|
||||||
|
}).delay(TaskSchedule.tick(2)).schedule();
|
||||||
|
|
||||||
|
display.scheduler().buildTask(onArrival).delay(TaskSchedule.tick(TOTAL_TICKS)).schedule();
|
||||||
|
display.scheduler().buildTask(display::remove).delay(TaskSchedule.tick(TOTAL_TICKS + OVERLAP_TICKS)).schedule();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||||
|
|
||||||
|
import net.kyori.adventure.util.RGBLike;
|
||||||
|
import net.minestom.server.color.Color;
|
||||||
|
import net.minestom.server.component.DataComponents;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
|
import net.minestom.server.entity.Entity;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.entity.metadata.projectile.FireworkRocketMeta;
|
||||||
|
import net.minestom.server.instance.Instance;
|
||||||
|
import net.minestom.server.item.ItemStack;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
import net.minestom.server.item.component.FireworkExplosion;
|
||||||
|
import net.minestom.server.item.component.FireworkList;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/** Steigt eine Feuerwerksrakete auf und explodiert in zufälliger Form und Farbe. */
|
||||||
|
class Fireworks {
|
||||||
|
private static final List<RGBLike> COLORS = List.of(
|
||||||
|
new Color(255, 85, 85), new Color(255, 170, 0), new Color(255, 255, 85),
|
||||||
|
new Color(85, 255, 85), new Color(85, 255, 255), new Color(85, 85, 255), new Color(255, 85, 255)
|
||||||
|
);
|
||||||
|
private static final int FLIGHT_TICKS = 20;
|
||||||
|
|
||||||
|
static void launch(Instance world, Pos at, Random random) {
|
||||||
|
FireworkExplosion.Shape[] shapes = FireworkExplosion.Shape.values();
|
||||||
|
FireworkExplosion explosion = new FireworkExplosion(
|
||||||
|
shapes[random.nextInt(shapes.length)],
|
||||||
|
List.of(COLORS.get(random.nextInt(COLORS.size())), COLORS.get(random.nextInt(COLORS.size()))),
|
||||||
|
List.of(COLORS.get(random.nextInt(COLORS.size()))),
|
||||||
|
random.nextBoolean(),
|
||||||
|
random.nextBoolean()
|
||||||
|
);
|
||||||
|
ItemStack rocketItem = ItemStack.of(Material.FIREWORK_ROCKET).with(DataComponents.FIREWORKS, new FireworkList(1, List.of(explosion)));
|
||||||
|
|
||||||
|
Entity rocket = new Entity(EntityType.FIREWORK_ROCKET);
|
||||||
|
rocket.setNoGravity(true);
|
||||||
|
rocket.editEntityMeta(FireworkRocketMeta.class, meta -> meta.setFireworkInfo(rocketItem));
|
||||||
|
rocket.setVelocity(new Vec(0, 15, 0));
|
||||||
|
rocket.setInstance(world, at).thenRun(() -> rocket.scheduler().buildTask(() -> {
|
||||||
|
// Status 17: der Client zeigt die Explosion
|
||||||
|
rocket.triggerStatus((byte) 17);
|
||||||
|
rocket.remove();
|
||||||
|
}).delay(TaskSchedule.tick(FLIGHT_TICKS)).schedule());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +1,408 @@
|
|||||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||||
|
|
||||||
|
import eu.mhsl.minecraft.endlessjumper.Jokers;
|
||||||
|
import eu.mhsl.minecraft.endlessjumper.Progress;
|
||||||
|
import eu.mhsl.minecraft.endlessjumper.ScoreTab;
|
||||||
import net.kyori.adventure.sound.Sound;
|
import net.kyori.adventure.sound.Sound;
|
||||||
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.TextColor;
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
import net.kyori.adventure.title.Title;
|
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.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.Chunk;
|
||||||
|
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.potion.Potion;
|
||||||
|
import net.minestom.server.potion.PotionEffect;
|
||||||
import net.minestom.server.sound.SoundEvent;
|
import net.minestom.server.sound.SoundEvent;
|
||||||
|
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.List;
|
||||||
|
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 final int preview = 2;
|
|
||||||
|
|
||||||
private static final int FULL_DIFFICULTY_AT = 1000;
|
|
||||||
|
|
||||||
|
|
||||||
public JumpGenerator(Pos starting, Player player, InstanceContainer world) {
|
|
||||||
this.player = player;
|
|
||||||
this.world = world;
|
|
||||||
this.starting = starting;
|
|
||||||
generateStart(starting);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void generateNext() {
|
private record BlockKey(int x, int y, int z) {
|
||||||
boolean isTenth = this.blocks.size() % 10 == 0;
|
static BlockKey of(Point point) {
|
||||||
boolean standingTenth = (this.blocks.size()-this.preview-1) % 10 == 0;
|
return new BlockKey(point.blockX(), point.blockY(), point.blockZ());
|
||||||
boolean isHundredth = this.blocks.size() % 100 == 0;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float pitch = 1 + (combo * 0.025f);
|
private static final int PREVIEW = 3;
|
||||||
this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, pitch));
|
private static final int FULL_DIFFICULTY_AT = 1000;
|
||||||
this.player.showTitle(Title.title(Component.text(""), Component.text((combo)+"", TextColor.color(255, 69, 0))));
|
private static final int FINALE_AT = 1000;
|
||||||
this.player.setLevel((this.blocks.size()-this.preview-1));
|
private static final int FIREWORKS_NEAR = 6, FIREWORKS_FAR = 14, FIREWORKS_SPAN_TICKS = 100;
|
||||||
|
|
||||||
Random r = new Random();
|
private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS;
|
||||||
int index = this.blocks.size();
|
private static final int BRIDGE_STEP_TICKS = 4;
|
||||||
Pos target = blocks.get(blocks.size()-1).add(this.nextOffset(index, r));
|
private static final int BRIDGE_LENGTH = 10;
|
||||||
this.world.setBlock(target, isHundredth ? Block.DIAMOND_BLOCK : isTenth ? Block.GOLD_BLOCK : BlockPalette.getRandom(index, r));
|
private static final Block RESCUE_BLOCK = Block.OAK_PLANKS;
|
||||||
|
private static final int RESCUE_DEPTH = 6;
|
||||||
|
private static final int FEATHER_FALL_TICKS = 15 * 20;
|
||||||
|
private static final int SPRING_JUMPS = 10;
|
||||||
|
private static final double CHECKPOINT_JOKER_CHANCE = 0.1;
|
||||||
|
|
||||||
if(standingTenth) {
|
private static final TextColor COMBO_COLOR = TextColor.color(255, 69, 0);
|
||||||
Pos current = this.blocks.get(this.blocks.size()-this.preview-1);
|
private static final Title CHECKPOINT_TITLE = Title.title(Component.empty(), Component.text("Checkpoint", NamedTextColor.GREEN));
|
||||||
this.checkpoint = current;
|
private static final Title FINALE_TITLE = Title.title(Component.text("Geschafft!", NamedTextColor.GOLD), Component.text("Durchgespielt – ab jetzt geht es endlos weiter", NamedTextColor.YELLOW));
|
||||||
this.player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.BLOCK, 0.3f, 1f));
|
private static final Sound CHECKPOINT_SOUND = 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))));
|
private static final Sound STAGE_SOUND = Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.4f, 1f);
|
||||||
this.player.sendPacket(new ParticlePacket(Particle.CRIT, current, current.add(0.5), 1.5f, 50));
|
private static final Sound BRIDGE_SOUND = Sound.sound(SoundEvent.BLOCK_AMETHYST_BLOCK_CHIME, Sound.Source.MASTER, 1f, 1f);
|
||||||
|
private static final Sound RESCUE_SOUND = Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.MASTER, 0.6f, 1f);
|
||||||
|
private static final Sound JOKER_SOUND = Sound.sound(SoundEvent.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.MASTER, 0.8f, 1.2f);
|
||||||
|
private static final Sound RESET_SOUND = Sound.sound(SoundEvent.BLOCK_GLASS_BREAK, Sound.Source.BLOCK, 0.3f, 1f);
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final Instance world;
|
||||||
|
private final Random random = new Random();
|
||||||
|
private final StageAtmosphere atmosphere;
|
||||||
|
|
||||||
|
// Index des ersten Schritts in course (beim Weiterspielen der gespeicherte Checkpoint)
|
||||||
|
private final int base;
|
||||||
|
private final List<Step> course = new ArrayList<>();
|
||||||
|
private final Map<BlockKey, Integer> courseIndex = new HashMap<>();
|
||||||
|
private int standing;
|
||||||
|
private int combo = 0;
|
||||||
|
private Pos checkpoint;
|
||||||
|
// zuletzt per Brücke überbrückter Bereich: diese Blöcke zählen nicht zur Combo
|
||||||
|
private int bridgeStart = -1;
|
||||||
|
private int bridgeEnd = -1;
|
||||||
|
|
||||||
|
// Sprungfeder: so viele Sprünge noch mit Sprungkraft
|
||||||
|
private int springJumpsLeft = 0;
|
||||||
|
|
||||||
|
// laufende Rettung durch das Totem: der Block, auf den man über die Leiter zurückklettert
|
||||||
|
private Pos rescueSafe;
|
||||||
|
|
||||||
|
/** @param startIndex Fortschritt, ab dem gespielt wird (0 oder ein gespeicherter Checkpoint) */
|
||||||
|
public JumpGenerator(Pos start, int startIndex, Player player, Instance world) {
|
||||||
|
this.player = player;
|
||||||
|
this.world = world;
|
||||||
|
this.base = startIndex;
|
||||||
|
this.standing = startIndex;
|
||||||
|
this.checkpoint = start;
|
||||||
|
this.atmosphere = new StageAtmosphere(player, world, BlockPalette.stageAt(startIndex));
|
||||||
|
|
||||||
|
Block startBlock = startIndex == 0 || isMilestone(startIndex) ? Block.DIAMOND_BLOCK : Block.GOLD_BLOCK;
|
||||||
|
this.world.setBlock(start, startBlock);
|
||||||
|
this.addStep(new Step(start, startBlock));
|
||||||
|
this.player.setLevel(startIndex);
|
||||||
|
ScoreTab.update(this.player, startIndex);
|
||||||
|
// wer ganz von vorn beginnt, bekommt ein Totem; beim Weiterspielen gibt es keine Joker
|
||||||
|
if(startIndex > 0) this.player.sendMessage(Component.text("Weiter ab Sprung " + startIndex, NamedTextColor.GOLD));
|
||||||
|
else Jokers.TOTEM.add(this.player);
|
||||||
|
|
||||||
|
// nacheinander animieren, damit jeder Block aus seinem bereits stehenden Vorgänger wächst
|
||||||
|
this.ensureGenerated(startIndex + PREVIEW + 1, BlockTransition.TOTAL_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Step step(int index) {
|
||||||
|
return this.course.get(index - this.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Index des nächsten zu erzeugenden Schritts. */
|
||||||
|
private int generated() {
|
||||||
|
return this.base + this.course.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCheckpoint(int index) {
|
||||||
|
return index % 10 == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMilestone(int index) {
|
||||||
|
return index % 100 == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double height(Block block) {
|
||||||
|
return block.collisionShape().relativeEnd().y();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pos landing() {
|
||||||
|
return this.step(this.standing + 1).pos();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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(this.springJumpsLeft > 0 && --this.springJumpsLeft == 0) this.player.removeEffect(PotionEffect.JUMP_BOOST);
|
||||||
|
|
||||||
|
if(index < this.bridgeStart || index > this.bridgeEnd) {
|
||||||
|
this.combo++;
|
||||||
|
this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, 1 + this.combo * 0.025f));
|
||||||
|
this.player.showTitle(Title.title(Component.empty(), Component.text(this.combo, COMBO_COLOR)));
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockPalette.Stage reachedStage = BlockPalette.stageStartingAt(this.blocks.size()-this.preview-1);
|
// auch ein übersprungener Checkpoint zählt: der letzte zwischen altem und neuem Stand
|
||||||
if(reachedStage != null && reachedStage.from() > 0) {
|
int reachedCheckpoint = index / 10 * 10;
|
||||||
this.player.playSound(Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.4f, 1f));
|
if(reachedCheckpoint > previous) {
|
||||||
this.player.showTitle(Title.title(Component.text(reachedStage.name(), TextColor.color(255, 170, 0)), Component.text("")));
|
this.checkpoint = this.step(reachedCheckpoint).pos();
|
||||||
|
Progress.save(this.player.getUuid(), reachedCheckpoint);
|
||||||
|
this.unloadChunksBehind();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// neuer Abschnitt: Stimmung wechseln (die Actionbar zeigt der Abschnitt selbst dauerhaft)
|
||||||
|
BlockPalette.Stage stage = BlockPalette.stageAt(index);
|
||||||
|
if(stage != BlockPalette.stageAt(previous)) {
|
||||||
|
this.atmosphere.enter(stage);
|
||||||
|
this.player.playSound(STAGE_SOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(previous < FINALE_AT && index >= FINALE_AT) this.celebrate();
|
||||||
|
|
||||||
|
this.ensureGenerated(index + PREVIEW + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Durchgespielt (Beginn von „Unendlich“): Feuerwerk rund um den Spieler. */
|
||||||
|
private void celebrate() {
|
||||||
|
this.player.showTitle(FINALE_TITLE);
|
||||||
|
|
||||||
|
// in Blickrichtung: ein paar Raketen nah vor dem Spieler, die meisten in der Ferne links, mittig und rechts
|
||||||
|
Pos origin = this.player.getPosition();
|
||||||
|
Vec forward = origin.direction().withY(0).normalize();
|
||||||
|
Vec right = new Vec(-forward.z(), 0, forward.x());
|
||||||
|
for(int i = 0; i < FIREWORKS_NEAR + FIREWORKS_FAR; i++) {
|
||||||
|
boolean near = i < FIREWORKS_NEAR;
|
||||||
|
double distance = near ? this.random.nextDouble(3, 10) : this.random.nextDouble(20, 45);
|
||||||
|
double side = near ? this.random.nextDouble(-6, 6) : this.random.nextDouble(-25, 25);
|
||||||
|
Pos at = origin.add(forward.mul(distance)).add(right.mul(side)).add(0, near ? 0 : 4, 0);
|
||||||
|
this.world.scheduler().buildTask(() -> Fireworks.launch(this.world, at, this.random))
|
||||||
|
.delay(TaskSchedule.tick(1 + this.random.nextInt(FIREWORKS_SPAN_TICKS)))
|
||||||
|
.schedule();
|
||||||
}
|
}
|
||||||
this.world.setBlock(target.add(0, 1 ,0), Block.LIGHT);
|
|
||||||
this.blocks.add(target);
|
|
||||||
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.combo++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstand zum nächsten Block. Die Schwierigkeit steigt über die ersten {@link #FULL_DIFFICULTY_AT}
|
* Die Strecke wandert immer in +z. Chunks hinter dem Checkpoint und außerhalb der Sichtweite
|
||||||
* Sprünge von leicht (2–3 weit, kaum seitlich/aufwärts) bis zum bisherigen Niveau (3–4 weit,
|
* werden nie wieder gebraucht (Reset und Rettung liegen davor): freigeben, sonst sammeln sich
|
||||||
* oft seitlich und aufwärts) und bleibt danach dort.
|
* bei langen Läufen tausende an.
|
||||||
*/
|
*/
|
||||||
private Pos nextOffset(int index, Random r) {
|
private void unloadChunksBehind() {
|
||||||
|
int playerChunk = this.player.getPosition().blockZ() >> 4;
|
||||||
|
int checkpointChunk = this.checkpoint.blockZ() >> 4;
|
||||||
|
int keepFrom = Math.min(checkpointChunk, playerChunk - this.player.effectiveViewDistance()) - 1;
|
||||||
|
for(Chunk chunk : List.copyOf(this.world.getChunks())) {
|
||||||
|
if(chunk.getChunkZ() < keepFrom) this.world.unloadChunk(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureGenerated(int upTo, int stepTicks) {
|
||||||
|
int delay = 0;
|
||||||
|
while(this.generated() <= upTo) {
|
||||||
|
this.generateStep(delay, null);
|
||||||
|
delay += stepTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStep(Step step) {
|
||||||
|
this.courseIndex.put(BlockKey.of(step.pos()), this.generated());
|
||||||
|
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.generated();
|
||||||
|
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) || (isCheckpoint(index) && this.random.nextDouble() < CHECKPOINT_JOKER_CHANCE)) this.dropJoker(target);
|
||||||
|
if(afterArrival != null) afterArrival.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Auf jedem Diamantblock (und manchem Checkpoint) liegt ein zufälliger Joker zum Einsammeln. */
|
||||||
|
private void dropJoker(Pos target) {
|
||||||
|
ItemEntity item = new ItemEntity(Jokers.random(this.random).item());
|
||||||
|
item.setPickupDelay(Duration.ZERO);
|
||||||
|
item.setInstance(this.world, target.add(0.5, 1, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* Anteil an 4er-Sprüngen, seitlichen Versätzen und Sprüngen nach oben bis zum bisherigen
|
||||||
|
* Niveau und bleiben danach dort.
|
||||||
|
*/
|
||||||
|
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 = 2;
|
int dz = this.random.nextDouble() < 0.1 + 0.4 * difficulty ? 4 : 3;
|
||||||
if(r.nextDouble() < 0.5 + 0.5 * difficulty) dz++;
|
int dx = this.random.nextDouble() < 0.1 + 0.57 * difficulty ? (this.random.nextBoolean() ? 1 : -1) : 0;
|
||||||
if(r.nextDouble() < 0.5 * difficulty) dz++;
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
// weite Diagonalsprünge nach oben erst gegen Ende
|
// von niedrigen Blöcken (z. B. Endportalrahmen) nie nach oben, und die Oberkante darf
|
||||||
|
// höchstens einen Block höher liegen
|
||||||
|
if(dy == 1 && (height(from) < 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;
|
if(dy == 1 && dz == 4 && dx != 0 && difficulty < 0.7) dx = 0;
|
||||||
|
|
||||||
return new Pos(dx, dy, dz);
|
return new Vec(dx, dy, dz);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pos getNextLanding() {
|
/** Rechtsklick mit einem Joker; true, wenn er verbraucht wurde. Das Totem wirkt nur automatisch. */
|
||||||
return this.blocks.get(this.blocks.size()-(preview+1));
|
public boolean useJoker(Jokers joker) {
|
||||||
|
boolean used = switch(joker) {
|
||||||
|
case BRIDGE -> this.useBridge();
|
||||||
|
case FEATHER_FALL -> {
|
||||||
|
this.player.addEffect(new Potion(PotionEffect.SLOW_FALLING, 0, FEATHER_FALL_TICKS));
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case SPRING -> {
|
||||||
|
this.springJumpsLeft = SPRING_JUMPS;
|
||||||
|
this.player.addEffect(new Potion(PotionEffect.JUMP_BOOST, 0, Potion.INFINITE_DURATION));
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case TOTEM -> false;
|
||||||
|
};
|
||||||
|
if(used) this.player.playSound(JOKER_SOUND);
|
||||||
|
return used;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joker „Brücke“: verbindet die nächsten {@link #BRIDGE_LENGTH} Blöcke ab der aktuellen Position
|
||||||
|
* mit einem begehbaren Weg, auch über früher schon geschaffte Stücke (z. B. nach einem Reset).
|
||||||
|
* Diese Blöcke zählen nicht zur Combo. Ist der Bereich schon überbrückt, passiert nichts.
|
||||||
|
*/
|
||||||
|
private boolean useBridge() {
|
||||||
|
int target = this.standing + BRIDGE_LENGTH;
|
||||||
|
if(this.rescueSafe != null || (this.standing >= this.bridgeStart && target <= this.bridgeEnd)) return false;
|
||||||
|
this.bridgeStart = this.standing;
|
||||||
|
this.bridgeEnd = target;
|
||||||
|
|
||||||
public void generateStart(Pos start) {
|
// bereits stehende Abschnitte sofort, neue Blöcke bekommen ihren Weg bei Ankunft
|
||||||
this.starting = start;
|
for(int i = this.standing + 1; i < this.generated() && i <= target; i++) {
|
||||||
this.checkpoint = start;
|
this.buildPath(this.step(i - 1).pos(), this.step(i).pos());
|
||||||
this.blocks.add(this.starting);
|
|
||||||
this.world.setBlock(this.starting, Block.DIAMOND_BLOCK);
|
|
||||||
for(int i = 0; i <= preview; i++) generateNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset() {
|
|
||||||
/*
|
|
||||||
for(Pos p : this.blocks) {
|
|
||||||
this.world.setBlock(p, Block.AIR);
|
|
||||||
}
|
}
|
||||||
this.blocks.clear();
|
int delay = 0;
|
||||||
*/
|
while(this.generated() <= target) {
|
||||||
|
Pos from = this.course.getLast().pos();
|
||||||
|
int index = this.generated();
|
||||||
|
this.generateStep(delay, () -> this.buildPath(from, this.step(index).pos()));
|
||||||
|
delay += BRIDGE_STEP_TICKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player.playSound(BRIDGE_SOUND);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkFall() {
|
||||||
|
double y = this.player.getPosition().y();
|
||||||
|
|
||||||
|
if(this.rescueSafe != null) {
|
||||||
|
if(y >= this.rescueSafe.y() + 0.9) this.rescueSafe = null;
|
||||||
|
else if(y < this.rescueSafe.blockY() - RESCUE_DEPTH - 3) {
|
||||||
|
this.rescueSafe = null;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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. Die Strecke geht immer
|
||||||
|
* mindestens 3 Blöcke nach vorne, die Rettung baut nur dahinter und darunter.
|
||||||
|
*/
|
||||||
|
private void rescue() {
|
||||||
|
Pos safe = this.step(this.standing).pos();
|
||||||
|
int x = safe.blockX();
|
||||||
|
int z = safe.blockZ();
|
||||||
|
int floorY = safe.blockY() - RESCUE_DEPTH;
|
||||||
|
this.rescueSafe = safe;
|
||||||
|
|
||||||
|
for(int dx = -1; dx <= 1; dx++) {
|
||||||
|
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 = 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, floorY + 1, z - 1.5, 0, 0));
|
||||||
|
this.player.triggerStatus((byte) 35);
|
||||||
|
this.player.playSound(RESCUE_SOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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)) this.world.setBlock(pos, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Beendet alles, was an den Spieler gebunden weiterläuft (Partikel, Musik, Joker-Effekte). */
|
||||||
|
public void close() {
|
||||||
|
this.atmosphere.stop();
|
||||||
|
this.player.removeEffect(PotionEffect.SLOW_FALLING);
|
||||||
|
this.player.removeEffect(PotionEffect.JUMP_BOOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||||
|
|
||||||
|
import net.kyori.adventure.sound.Sound;
|
||||||
|
import net.kyori.adventure.sound.SoundStop;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
|
import net.minestom.server.entity.Player;
|
||||||
|
import net.minestom.server.instance.Instance;
|
||||||
|
import net.minestom.server.network.packet.server.play.ParticlePacket;
|
||||||
|
import net.minestom.server.timer.Task;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stimmung des aktuellen Abschnitts: Tageszeit (fließender Übergang), Musik, Partikel auf der
|
||||||
|
* Strecke voraus und der Abschnittsname dauerhaft in der Actionbar.
|
||||||
|
*/
|
||||||
|
class StageAtmosphere {
|
||||||
|
private static final int TIME_TRANSITION_TICKS = 100;
|
||||||
|
// die Strecke läuft immer in +z: Partikel vom Spieler bis etwa 10 Sprünge voraus
|
||||||
|
private static final Vec AMBIENT_AHEAD = new Vec(0, 1.5, 14);
|
||||||
|
private static final Vec AMBIENT_SPREAD = new Vec(7, 4, 16);
|
||||||
|
private static final int AMBIENT_EVERY_TICKS = 4;
|
||||||
|
// die Actionbar blendet nach ~3 s aus: jede Sekunde erneuern, nach einem Wechsel 5 s golden
|
||||||
|
private static final int ACTION_BAR_TICKS = 20;
|
||||||
|
private static final int HIGHLIGHT_TICKS = 100;
|
||||||
|
private static final Component ENDLESS_HINT = Component.text(" · /reset für einen Neustart", NamedTextColor.DARK_GRAY);
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final Instance world;
|
||||||
|
private BlockPalette.Stage stage;
|
||||||
|
private Task timeTransition;
|
||||||
|
private final Task ambient;
|
||||||
|
private int ticks = 0;
|
||||||
|
private int highlightUntil = 0;
|
||||||
|
|
||||||
|
StageAtmosphere(Player player, Instance world, BlockPalette.Stage initial) {
|
||||||
|
this.player = player;
|
||||||
|
this.world = world;
|
||||||
|
this.stage = initial;
|
||||||
|
this.world.setTime(initial.time());
|
||||||
|
this.world.refreshTime();
|
||||||
|
this.playMusic();
|
||||||
|
// an den Spieler gebunden, endet also mit ihm
|
||||||
|
this.ambient = player.scheduler().buildTask(this::tick).repeat(TaskSchedule.tick(AMBIENT_EVERY_TICKS)).schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
this.ambient.cancel();
|
||||||
|
if(this.timeTransition != null) this.timeTransition.cancel();
|
||||||
|
this.player.stopSound(SoundStop.source(Sound.Source.MUSIC));
|
||||||
|
}
|
||||||
|
|
||||||
|
void enter(BlockPalette.Stage next) {
|
||||||
|
if(next == this.stage) return;
|
||||||
|
this.stage = next;
|
||||||
|
this.highlightUntil = this.ticks + HIGHLIGHT_TICKS;
|
||||||
|
this.playMusic();
|
||||||
|
this.fadeTimeTo(next.time());
|
||||||
|
this.showStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playMusic() {
|
||||||
|
this.player.stopSound(SoundStop.source(Sound.Source.MUSIC));
|
||||||
|
this.player.playSound(Sound.sound(this.stage.music(), Sound.Source.MUSIC, 1f, 1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Läuft immer vorwärts zur Zielzeit, Tick für Tick, damit Himmel und Licht weich wechseln. */
|
||||||
|
private void fadeTimeTo(long target) {
|
||||||
|
if(this.timeTransition != null) this.timeTransition.cancel();
|
||||||
|
long start = this.world.getTime();
|
||||||
|
long delta = Math.floorMod(target - start, 24000L);
|
||||||
|
if(delta == 0) return;
|
||||||
|
|
||||||
|
this.timeTransition = this.world.scheduler().submitTask(new Supplier<>() {
|
||||||
|
private int tick = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TaskSchedule get() {
|
||||||
|
this.tick++;
|
||||||
|
StageAtmosphere.this.world.setTime(start + delta * this.tick / TIME_TRANSITION_TICKS);
|
||||||
|
StageAtmosphere.this.world.refreshTime();
|
||||||
|
return this.tick < TIME_TRANSITION_TICKS ? TaskSchedule.tick(1) : TaskSchedule.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tick() {
|
||||||
|
this.ticks += AMBIENT_EVERY_TICKS;
|
||||||
|
if(this.ticks % ACTION_BAR_TICKS == 0) this.showStage();
|
||||||
|
if(this.stage.ambient() != null) {
|
||||||
|
this.player.sendPacket(new ParticlePacket(this.stage.ambient(), this.player.getPosition().add(AMBIENT_AHEAD), AMBIENT_SPREAD, 0.02f, 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showStage() {
|
||||||
|
Component text = Component.text(this.stage.name(), this.ticks < this.highlightUntil ? NamedTextColor.GOLD : NamedTextColor.GRAY);
|
||||||
|
if(BlockPalette.isEndless(this.stage)) text = text.append(ENDLESS_HINT);
|
||||||
|
this.player.sendActionBar(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user