Compress the stages so the endless part starts at 1000, keep the stage in the action bar permanently and let the bridge cover the next 10 blocks from the current position

This commit is contained in:
MineTec committed 2026-10-10 13:03:11 +02:00
1 parent 40bedc3c43
commit 7e85a6d229
4 files changed
+56 -37

No files matched your search

@@ -12,11 +12,11 @@ import java.util.Random;
/**
* Joker-Items in der Hotbar, die Anzahl ist die Stapelgröße.
* Brücke: Rechtsklick baut einen Weg bis zum nächsten Checkpoint.
* Brücke: Rechtsklick baut einen Weg über die nächsten 10 Blöcke.
* Totem: rettet automatisch beim Runterfallen (Plattform mit Leiter).
*/
public enum Jokers {
BRIDGE(0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg bis zum nächsten Checkpoint"),
BRIDGE(0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg über die nächsten 10 Blöcke"),
TOTEM(8, Material.TOTEM_OF_UNDYING, "Rettungstotem", NamedTextColor.GOLD, "Rettet dich automatisch beim Runterfallen");
private final int slot;
@@ -9,7 +9,7 @@ import java.util.Random;
/**
* Abschnitte entlang des Fortschritts: einmal durch das komplette Spiel, von der Wiese über
* Untergrund, Nether und Stronghold bis ins End (ab Sprung 1000). Jeder Abschnitt hat eigene
* Untergrund, Nether und Stronghold bis ins End; ab Sprung 1000 geht es endlos weiter. Jeder Abschnitt hat eigene
* Blöcke und eine eigene Stimmung (Tageszeit, Partikel, Musik).
*/
public class BlockPalette {
@@ -23,47 +23,47 @@ public class BlockPalette {
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
)),
new Stage(75, "Wald", NOON, Particle.CHERRY_LEAVES, SoundEvent.MUSIC_OVERWORLD_FOREST, 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_LEAVES, Block.BIRCH_LEAVES, Block.SPRUCE_LEAVES, Block.CHERRY_LEAVES
)),
new Stage(150, "Untergrund", NIGHT, null, SoundEvent.MUSIC_OVERWORLD_DRIPSTONE_CAVES, 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.GRANITE, Block.DIORITE, Block.COBBLESTONE
)),
new Stage(250, "Höhlen", NIGHT, Particle.DRIPPING_WATER, SoundEvent.MUSIC_OVERWORLD_LUSH_CAVES, 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.GOLD_ORE, Block.REDSTONE_ORE, Block.LAPIS_ORE
)),
new Stage(350, "Tiefenschiefer", NIGHT, Particle.SCULK_SOUL, SoundEvent.MUSIC_OVERWORLD_DEEP_DARK, 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_IRON_ORE, Block.AMETHYST_BLOCK
)),
new Stage(450, "Netherportal", NIGHT, Particle.PORTAL, SoundEvent.MUSIC_NETHER_NETHER_WASTES, List.of(
new Stage(390, "Netherportal", NIGHT, Particle.PORTAL, SoundEvent.MUSIC_NETHER_NETHER_WASTES, List.of(
Block.OBSIDIAN, Block.OBSIDIAN, Block.CRYING_OBSIDIAN
)),
new Stage(475, "Nether", NIGHT, Particle.CRIMSON_SPORE, SoundEvent.MUSIC_NETHER_CRIMSON_FOREST, 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.SOUL_SOIL, Block.GLOWSTONE, Block.NETHER_QUARTZ_ORE, Block.NETHER_GOLD_ORE
)),
new Stage(625, "Festung & Bastion", NIGHT, Particle.WHITE_ASH, SoundEvent.MUSIC_NETHER_BASALT_DELTAS, 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.POLISHED_BLACKSTONE_BRICKS, Block.GILDED_BLACKSTONE, Block.BASALT
)),
new Stage(750, "Stronghold", NIGHT, Particle.ENCHANT, SoundEvent.MUSIC_DISC_MELLOHI, 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.CHISELED_STONE_BRICKS, Block.BOOKSHELF
)),
new Stage(925, "Portalraum", NIGHT, Particle.REVERSE_PORTAL, SoundEvent.MUSIC_DRAGON, 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.STONE_BRICKS, Block.MOSSY_STONE_BRICKS
)),
new Stage(1000, "End", NIGHT, Particle.END_ROD, SoundEvent.MUSIC_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.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(
new Stage(1000, "Unendlich", NIGHT, null, SoundEvent.MUSIC_MENU, List.of(
Block.SMOOTH_QUARTZ
))
);
@@ -48,13 +48,13 @@ public class JumpGenerator {
private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS;
private static final int BRIDGE_STEP_TICKS = 4;
private static final int BRIDGE_LENGTH = 10;
private static final Block RESCUE_BLOCK = Block.OAK_PLANKS;
private static final int RESCUE_DEPTH = 6;
private static final TextColor COMBO_COLOR = TextColor.color(255, 69, 0);
private static final Title CHECKPOINT_TITLE = Title.title(Component.empty(), Component.text("Checkpoint", NamedTextColor.GREEN));
private static final Component ENDLESS_HINT = Component.text(" · /reset für einen Neustart", NamedTextColor.DARK_GRAY);
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 Title FINALE_TITLE = Title.title(Component.text("Geschafft!", NamedTextColor.GOLD), Component.text("Durchgespielt – ab jetzt geht es endlos weiter", 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);
@@ -73,7 +73,8 @@ public class JumpGenerator {
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
// zuletzt per Brücke überbrückter Bereich: diese Blöcke zählen nicht zur Combo
private int bridgeStart = -1;
private int bridgeEnd = -1;
// Rettung durch das Totem: Plattform mit Leiter unter dem Block, von dem man gefallen ist
@@ -139,7 +140,7 @@ public class JumpGenerator {
this.player.setLevel(index);
ScoreTab.update(this.player, index);
if(index > this.bridgeEnd) {
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)));
@@ -155,23 +156,19 @@ public class JumpGenerator {
this.player.sendPacket(new ParticlePacket(Particle.CRIT, this.checkpoint, this.checkpoint.add(0.5), 1.5f, 50));
}
// Abschnitt dauerhaft in der Actionbar (Titel werden von der Combo-Anzeige überschrieben)
// neuer Abschnitt: Stimmung wechseln (die Actionbar zeigt der Abschnitt selbst dauerhaft)
BlockPalette.Stage stage = BlockPalette.stageAt(index);
boolean newStage = stage != BlockPalette.stageAt(previous);
if(newStage) {
if(stage != BlockPalette.stageAt(previous)) {
this.atmosphere.enter(stage);
this.player.playSound(STAGE_SOUND);
}
Component stageText = Component.text(stage.name(), newStage ? NamedTextColor.GOLD : NamedTextColor.GRAY);
if(BlockPalette.isEndless(stage)) stageText = stageText.append(ENDLESS_HINT);
this.player.sendActionBar(stageText);
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. */
/** Durchgespielt (Beginn von „Unendlich“): Feuerwerk rund um den Spieler. */
private void celebrate() {
this.player.showTitle(FINALE_TITLE);
for(int i = 0; i < 6; i++) {
@@ -249,20 +246,22 @@ public class JumpGenerator {
}
/**
* Joker „Brücke“: erzeugt die Strecke bis zum nächsten Checkpoint und verbindet sie mit einem
* begehbaren Weg. Diese Blöcke zählen nicht zur Combo. Mehrfacher Aufruf baut nichts doppelt.
* 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.
*/
public boolean useBridge() {
int nextCheckpoint = (this.standing / 10 + 1) * 10;
if(this.rescueSafe != null || this.bridgeEnd >= nextCheckpoint) return false;
this.bridgeEnd = nextCheckpoint;
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;
// bereits stehende Abschnitte sofort, neue Blöcke bekommen ihren Weg bei Ankunft
for(int i = this.standing + 1; i < this.generated() && i <= nextCheckpoint; i++) {
for(int i = this.standing + 1; i < this.generated() && i <= target; i++) {
this.buildPath(this.step(i - 1).pos(), this.step(i).pos());
}
int delay = 0;
while(this.generated() <= nextCheckpoint) {
while(this.generated() <= target) {
Pos from = this.course.getLast().pos();
int index = this.generated();
this.generateStep(delay, () -> this.buildPath(from, this.step(index).pos()));
@@ -2,6 +2,8 @@ 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;
@@ -12,20 +14,27 @@ import net.minestom.server.timer.TaskSchedule;
import java.util.function.Supplier;
/**
* Stimmung des aktuellen Abschnitts: Tageszeit (fließender Übergang), Musik und Partikel,
* die dauerhaft um den Spieler schweben.
* 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;
@@ -35,7 +44,7 @@ class StageAtmosphere {
this.world.refreshTime();
this.playMusic();
// an den Spieler gebunden, endet also mit ihm
this.ambient = player.scheduler().buildTask(this::spawnAmbient).repeat(TaskSchedule.tick(4)).schedule();
this.ambient = player.scheduler().buildTask(this::tick).repeat(TaskSchedule.tick(AMBIENT_EVERY_TICKS)).schedule();
}
void stop() {
@@ -47,8 +56,10 @@ class StageAtmosphere {
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() {
@@ -76,8 +87,17 @@ class StageAtmosphere {
});
}
private void spawnAmbient() {
if(this.stage.ambient() == null) return;
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);
}
}