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:
1 parent
137e162ea4
commit
806d76e560
6 files changed
+171
-25
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user