Resume from the last checkpoint, celebrate reaching the end with fireworks, continue endlessly afterwards and start the run right on spawn

This commit is contained in:
MineTec committed 2026-10-08 00:35:08 +02:00
1 parent 137e162ea4
commit 806d76e560
6 files changed
+171 -25

No files matched your search

+2
View File
@@ -4,3 +4,5 @@ target/
*.iml
.idea/workspace.xml
forwarding.secret
progress.properties
progress.properties.tmp
@@ -23,7 +23,6 @@ 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;
@@ -37,6 +36,7 @@ public class EndlessJumper {
public static void main(String[] args) throws IOException {
MinecraftServer server = MinecraftServer.init(new Auth.Velocity(readForwardingSecret()));
Progress.load();
InstanceManager instanceManager = MinecraftServer.getInstanceManager();
GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler();
PlayerSkin skin = PlayerSkin.fromUsername("Notch");
@@ -52,18 +52,16 @@ public class EndlessJumper {
p.setSkin(skin);
event.setSpawningInstance(world);
// 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());
Player p = event.getPlayer();
p.setCanPickupItem(true);
ScoreTab.join(p);
Jokers.giveStart(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 -> {
@@ -0,0 +1,54 @@
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);
}
/** 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);
}
}
}
@@ -61,6 +61,10 @@ public class BlockPalette {
new Stage(1000, "End", NIGHT, Particle.END_ROD, SoundEvent.MUSIC_END, List.of(
Block.END_STONE, Block.END_STONE, Block.END_STONE_BRICKS,
Block.PURPUR_BLOCK, Block.PURPUR_PILLAR, Block.OBSIDIAN
)),
// nach dem End endlos weiter mit einem neutralen Block
new Stage(1100, "Unendlich", NIGHT, null, SoundEvent.MUSIC_MENU, List.of(
Block.SMOOTH_QUARTZ
))
);
@@ -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,6 +1,7 @@
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.text.Component;
@@ -43,6 +44,7 @@ public class JumpGenerator {
private static final int PREVIEW = 3;
private static final int FULL_DIFFICULTY_AT = 1000;
private static final int FINALE_AT = 1000;
private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS;
private static final int BRIDGE_STEP_TICKS = 4;
@@ -51,6 +53,7 @@ public class JumpGenerator {
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 Title FINALE_TITLE = Title.title(Component.text("Geschafft!", NamedTextColor.GOLD), Component.text("Du hast das End erreicht", NamedTextColor.YELLOW));
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);
@@ -62,9 +65,11 @@ public class JumpGenerator {
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 = 0;
private int standing;
private int combo = 0;
private Pos checkpoint;
// bis zu diesem Index wurde per Brücke gebaut: diese Blöcke zählen nicht zur Combo
@@ -74,14 +79,33 @@ public class JumpGenerator {
private final Map<Pos, Block> rescueBlocks = new HashMap<>();
private Pos rescueSafe;
public JumpGenerator(Pos start, Player player, Instance world) {
/** @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(0));
this.addStep(new Step(start, Block.DIAMOND_BLOCK));
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);
if(startIndex > 0) this.player.sendMessage(Component.text("Weiter ab Sprung " + startIndex, NamedTextColor.GOLD));
// nacheinander animieren, damit jeder Block aus seinem bereits stehenden Vorgänger wächst
this.ensureGenerated(PREVIEW + 1, BlockTransition.TOTAL_TICKS);
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) {
@@ -97,7 +121,7 @@ public class JumpGenerator {
}
private Pos landing() {
return this.course.get(this.standing + 1).pos();
return this.step(this.standing + 1).pos();
}
/** Wird bei jeder Bewegung aufgerufen: erkennt Landungen und behandelt das Runterfallen. */
@@ -121,7 +145,8 @@ public class JumpGenerator {
}
if(isCheckpoint(index)) {
this.checkpoint = this.course.get(index).pos();
this.checkpoint = this.step(index).pos();
Progress.save(this.player.getUuid(), index);
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));
@@ -137,25 +162,38 @@ public class JumpGenerator {
this.player.sendActionBar(Component.text(stage.name(), NamedTextColor.GRAY));
}
if(previous < FINALE_AT && index >= FINALE_AT) this.celebrate();
this.ensureGenerated(index + PREVIEW + 1, 0);
}
/** Ankunft im End: Feuerwerk rund um den Spieler. Danach geht es endlos weiter. */
private void celebrate() {
this.player.showTitle(FINALE_TITLE);
for(int i = 0; i < 6; i++) {
Pos at = this.player.getPosition().add(this.random.nextInt(-6, 7), 0, this.random.nextInt(2, 10));
this.world.scheduler().buildTask(() -> Fireworks.launch(this.world, at, this.random))
.delay(TaskSchedule.tick(1 + i * 8))
.schedule();
}
}
private void ensureGenerated(int upTo, int stepTicks) {
int delay = 0;
while(this.course.size() <= upTo) {
while(this.generated() <= upTo) {
this.generateStep(delay, null);
delay += stepTicks;
}
}
private void addStep(Step step) {
this.courseIndex.put(BlockKey.of(step.pos()), this.course.size());
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.course.size();
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));
@@ -216,14 +254,14 @@ public class JumpGenerator {
this.bridgeEnd = nextCheckpoint;
// 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());
for(int i = this.standing + 1; i < this.generated() && i <= nextCheckpoint; i++) {
this.buildPath(this.step(i - 1).pos(), this.step(i).pos());
}
int delay = 0;
while(this.course.size() <= nextCheckpoint) {
while(this.generated() <= nextCheckpoint) {
Pos from = this.course.getLast().pos();
int index = this.course.size();
this.generateStep(delay, () -> this.buildPath(from, this.course.get(index).pos()));
int index = this.generated();
this.generateStep(delay, () -> this.buildPath(from, this.step(index).pos()));
delay += BRIDGE_STEP_TICKS;
}
@@ -267,7 +305,7 @@ public class JumpGenerator {
* mindestens 3 Blöcke nach vorne, die Rettung baut nur dahinter und darunter.
*/
private void rescue() {
Pos safe = this.course.get(this.standing).pos();
Pos safe = this.step(this.standing).pos();
int x = safe.blockX();
int z = safe.blockZ();
int floorY = safe.blockY() - RESCUE_DEPTH;