feat: duels player colors + elytra sub-modes (M-level)

- Add ParkourGenerator.selectBlockData() hook (forced course material);
  BlockSelector checks it first; SingleDuelsGenerator overrides to use the
  player's color (16 concrete colors) and forces only DEFAULT block type
- Elytra sub-modes: SpeedDemonGenerator (max-speed score), MinSpeedGenerator
  (min-speed check, reset on 6 slow ticks), TimeTrialGenerator (score cap),
  CloseGenerator (FLAT rings); + SpeedDemon/MinSpeed/TimeTrial/Close modes,
  registration, locales play.elytra.*, config mode-settings.*
- Config.getDouble(path, default) + ConfigAccessor overload
- checkstyle 0/0, PMD 0, build green
This commit is contained in:
loki5512344 2026-08-05 21:32:54 +02:00
parent acb884b70b
commit 5707cf2d24
Signed by: boba
GPG key ID: 253067914055423B
21 changed files with 496 additions and 27 deletions

View file

@ -13,7 +13,11 @@ import dev.loki.loparkour.hook.papi.PAPIHook;
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
import dev.loki.loparkour.elytra.mode.CloseMode;
import dev.loki.loparkour.elytra.mode.ElytraMode;
import dev.loki.loparkour.elytra.mode.MinSpeedMode;
import dev.loki.loparkour.elytra.mode.SpeedDemonMode;
import dev.loki.loparkour.elytra.mode.TimeTrialMode;
import dev.loki.loparkour.mode.base.Modes;
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
import dev.loki.loparkour.mode.impl.multi.CoopMode;
@ -79,6 +83,10 @@ public final class PluginBootstrap {
Registry.register(new CoopMode());
Registry.register(new GravityShiftMode());
Registry.register(new ElytraMode());
Registry.register(new SpeedDemonMode());
Registry.register(new MinSpeedMode());
Registry.register(new TimeTrialMode());
Registry.register(new CloseMode());
Registry.register(new DuelsMode());
Modes.init();
}

View file

@ -99,6 +99,10 @@ public enum Config {
return accessor.getDouble(path);
}
public double getDouble(@NotNull String path, double defaultValue) {
return accessor != null ? accessor.getDouble(path, defaultValue) : defaultValue;
}
@NotNull
public String getString(@NotNull String path) {
return accessor.getString(path);

View file

@ -82,6 +82,14 @@ public class ConfigAccessor {
warnIfMissing(path);
return config.getDouble(path, 0.0);
}
/**
* Get double value at path with a fallback default.
*/
public double getDouble(@NotNull String path, double defaultValue) {
warnIfMissing(path);
return config.getDouble(path, defaultValue);
}
/**
* Get string value at path.

View file

@ -33,6 +33,23 @@ public class SingleDuelsGenerator extends ParkourGenerator {
public SingleDuelsGenerator(@NotNull Session session) {
super(session);
forceDefaultBlocks();
}
@Override
public void reset(boolean regenerate) {
super.reset(regenerate);
forceDefaultBlocks();
}
/**
* Duels courses use only plain (non-special, non-schematic) blocks so the
* player's color is the only material on the course.
*/
private void forceDefaultBlocks() {
state.defaultChances.clear();
state.defaultChances.put(BlockGenerationType.DEFAULT, 1.0);
state.specialChances.clear();
}
public void setOwnedPlayer(@NotNull ParkourPlayer pp) {
@ -43,6 +60,11 @@ public class SingleDuelsGenerator extends ParkourGenerator {
this.blockData = COLORS.get(index % COLORS.size()).createBlockData();
}
@Override
public @NotNull BlockData selectBlockData() {
return blockData == null ? Material.STONE.createBlockData() : blockData;
}
@Override
@NotNull
public List<ParkourPlayer> getPlayers() {

View file

@ -0,0 +1,14 @@
package dev.loki.loparkour.elytra.generator;
import dev.loki.loparkour.elytra.section.ElytraPointType;
import dev.loki.loparkour.session.core.Session;
/**
* Elytra mode that generates flat rings placed close together.
*/
public class CloseGenerator extends ElytraGenerator {
public CloseGenerator(Session session) {
super(session, ElytraPointType.FLAT);
}
}

View file

@ -20,7 +20,6 @@ import java.util.Map;
import java.util.Random;
public class ElytraGenerator extends ParkourGenerator {
private static final int ASCEND_THRESHOLD_Y = 50;
private static final double SUSTAINED_BOOST = 0.08;
private static final int DEFAULT_RADIUS = 3;
@ -28,18 +27,23 @@ public class ElytraGenerator extends ParkourGenerator {
private static final double EPSILON = 1e-6;
public final Map<Integer, ElytraSection> sections = new LinkedHashMap<>();
public final ClientBlockChanger blockChanger = new ClientBlockChanger();
private double movementScore = 0.0;
protected double movementScore = 0.0;
private Long startTime = null;
private Vector playerSpawn;
private Vector blockSpawn;
private Random random;
private final ElytraPointType pointType = ElytraPointType.CIRCLE;
protected final ElytraPointType pointType;
private final int radius;
private final Material material;
private int boostedSectionIdx = Integer.MIN_VALUE;
public ElytraGenerator(Session session) {
this(session, ElytraPointType.CIRCLE);
}
protected ElytraGenerator(Session session, ElytraPointType pointType) {
super(session);
this.pointType = pointType;
this.radius = Config.GENERATION.getInt("elytra.radius", DEFAULT_RADIUS);
this.material = Materials.parseOr(Config.GENERATION.getString("advanced.island.parkour.first-block-material"), Material.WHITE_STAINED_GLASS);
}
@ -62,21 +66,19 @@ public class ElytraGenerator extends ParkourGenerator {
@Override
public void generate() {
final int idx;
final ElytraSection section;
if (sections.isEmpty()) {
idx = 0;
section = new ElytraSection(blockSpawn.clone().add(new Vector(0, pointType.heightOffset, 0)), random,
ElytraKnotDirector.DEFAULT_VERTICAL_BIAS);
} else {
final int lastIdx = sections.keySet().stream().max(Integer::compareTo).orElse(0);
final ElytraSection previous = sections.get(lastIdx);
final int bias = previous.end().getY() < ASCEND_THRESHOLD_Y ? ElytraKnotDirector.ASCENDING_VERTICAL_BIAS
: ElytraKnotDirector.DEFAULT_VERTICAL_BIAS;
idx = lastIdx + 1;
section = new ElytraSection(previous.end(), random, bias);
final Vector start = blockSpawn.clone().add(new Vector(0, pointType.heightOffset, 0));
final ElytraSection section = new ElytraSection(start, random, ElytraKnotDirector.DEFAULT_VERTICAL_BIAS);
sections.put(0, section);
blockChanger.queue(section.generateBlocks(pointType, radius));
return;
}
sections.put(idx, section);
final int lastIdx = sections.keySet().stream().max(Integer::compareTo).orElse(0);
final ElytraSection previous = sections.get(lastIdx);
final int bias = previous.end().getY() < ASCEND_THRESHOLD_Y ? ElytraKnotDirector.ASCENDING_VERTICAL_BIAS
: ElytraKnotDirector.DEFAULT_VERTICAL_BIAS;
final ElytraSection section = new ElytraSection(previous.end(), random, bias);
sections.put(lastIdx + 1, section);
blockChanger.queue(section.generateBlocks(pointType, radius));
}
@ -114,8 +116,8 @@ public class ElytraGenerator extends ParkourGenerator {
@Override
public void reset(boolean regenerate) {
final Player player = getPlayer();
for (final ElytraSection section : sections.values()) {
if (player != null) {
if (player != null) {
for (final ElytraSection section : sections.values()) {
section.clear(player);
}
}
@ -142,16 +144,13 @@ public class ElytraGenerator extends ParkourGenerator {
return (int) Math.round(movementScore);
}
private Player getPlayer() {
protected Player getPlayer() {
return session.getPlayers().isEmpty() ? null : session.getPlayers().get(0).player;
}
private void removeOldest(Player player) {
if (sections.size() <= 2) {
return;
}
final ElytraSection old = sections.remove(sections.keySet().stream().min(Integer::compareTo).orElse(-1));
if (old != null) {
if (sections.size() > 2) {
final ElytraSection old = sections.remove(sections.keySet().stream().min(Integer::compareTo).orElse(-1));
old.clear(player);
}
}

View file

@ -0,0 +1,60 @@
package dev.loki.loparkour.elytra.generator;
import dev.loki.loparkour.config.core.Config;
import dev.loki.loparkour.session.core.Session;
import org.bukkit.entity.Player;
/**
* Elytra mode that requires sustained minimum speed and scores horizontal distance covered.
*/
public class MinSpeedGenerator extends ElytraGenerator {
private static final double TICKS_PER_SECOND = 20.0;
private static final double MIN_SPEED = Config.GENERATION.getDouble("mode-settings.min-speed.min-speed", 0.9);
private static final int TOO_SLOW_LIMIT = 6;
private int startX = 0;
private double maxSpeed = 0;
private int ticksTooSlow = 0;
public MinSpeedGenerator(Session session) {
super(session);
}
@Override
public void tick() {
super.tick();
final Player player = getPlayer();
if (player == null) {
return;
}
final double speed = player.getVelocity().clone().setY(0).length() * TICKS_PER_SECOND;
maxSpeed = Math.max(maxSpeed, speed);
if (speed > MIN_SPEED && startX == 0) {
startX = player.getLocation().getBlockX();
}
if (MIN_SPEED > speed && maxSpeed > MIN_SPEED) {
ticksTooSlow++;
}
if (ticksTooSlow >= TOO_SLOW_LIMIT) {
reset(true);
}
}
@Override
public void reset(boolean regenerate) {
super.reset(regenerate);
startX = 0;
maxSpeed = 0;
ticksTooSlow = 0;
}
@Override
public int getElytraScore() {
final Player player = getPlayer();
if (startX == 0 || player == null) {
return 0;
}
return Math.max(0, (int) Math.round(player.getLocation().getX() - startX));
}
}

View file

@ -0,0 +1,38 @@
package dev.loki.loparkour.elytra.generator;
import dev.loki.loparkour.session.core.Session;
import org.bukkit.entity.Player;
/**
* Elytra mode that scores the highest gliding speed reached during the run.
*/
public class SpeedDemonGenerator extends ElytraGenerator {
private static final double TICKS_PER_SECOND = 20.0;
private double maxSpeed = 0;
public SpeedDemonGenerator(Session session) {
super(session);
}
@Override
public void tick() {
super.tick();
final Player player = getPlayer();
if (player != null && player.isGliding() && player.getVelocity().getX() > 0) {
maxSpeed = Math.max(maxSpeed, player.getVelocity().clone().setY(0).length() * TICKS_PER_SECOND);
}
}
@Override
public void reset(boolean regenerate) {
super.reset(regenerate);
maxSpeed = 0;
}
@Override
public int getElytraScore() {
return (int) Math.round(maxSpeed);
}
}

View file

@ -0,0 +1,24 @@
package dev.loki.loparkour.elytra.generator;
import dev.loki.loparkour.config.core.Config;
import dev.loki.loparkour.session.core.Session;
/**
* Elytra mode that restarts the course once the target score is reached.
*/
public class TimeTrialGenerator extends ElytraGenerator {
private static final int SCORE = Config.GENERATION.getInt("mode-settings.time-trial.score", 500);
public TimeTrialGenerator(Session session) {
super(session);
}
@Override
public void tick() {
super.tick();
if (getElytraScore() >= SCORE) {
reset(true);
}
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.loparkour.elytra.mode;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.elytra.generator.CloseGenerator;
import dev.loki.loparkour.leaderboard.core.Leaderboard;
import dev.loki.loparkour.mode.base.Mode;
import dev.loki.loparkour.mode.base.ModeMessages;
import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.session.core.Session;
import dev.loki.loparkour.util.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Elytra mode with flat rings placed close together.
*/
public class CloseMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "close";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.elytra.close");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!ModeMessages.checkJoiningEnabled(player)) {
return;
}
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof CloseMode) {
return;
}
player.closeInventory();
Session.create(CloseGenerator::new, null, null, player);
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.loparkour.elytra.mode;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.elytra.generator.MinSpeedGenerator;
import dev.loki.loparkour.leaderboard.core.Leaderboard;
import dev.loki.loparkour.mode.base.Mode;
import dev.loki.loparkour.mode.base.ModeMessages;
import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.session.core.Session;
import dev.loki.loparkour.util.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Elytra mode scoring distance covered while keeping a minimum speed.
*/
public class MinSpeedMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "minspeed";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.elytra.minspeed");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!ModeMessages.checkJoiningEnabled(player)) {
return;
}
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof MinSpeedMode) {
return;
}
player.closeInventory();
Session.create(MinSpeedGenerator::new, null, null, player);
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.loparkour.elytra.mode;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.elytra.generator.SpeedDemonGenerator;
import dev.loki.loparkour.leaderboard.core.Leaderboard;
import dev.loki.loparkour.mode.base.Mode;
import dev.loki.loparkour.mode.base.ModeMessages;
import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.session.core.Session;
import dev.loki.loparkour.util.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Elytra mode scoring the highest gliding speed reached.
*/
public class SpeedDemonMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "speeddemon";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.elytra.speeddemon");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!ModeMessages.checkJoiningEnabled(player)) {
return;
}
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof SpeedDemonMode) {
return;
}
player.closeInventory();
Session.create(SpeedDemonGenerator::new, null, null, player);
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.loparkour.elytra.mode;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.elytra.generator.TimeTrialGenerator;
import dev.loki.loparkour.leaderboard.core.Leaderboard;
import dev.loki.loparkour.mode.base.Mode;
import dev.loki.loparkour.mode.base.ModeMessages;
import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.session.core.Session;
import dev.loki.loparkour.util.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Elytra mode that resets the course after the target score is reached.
*/
public class TimeTrialMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "timetrial";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.elytra.timetrial");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!ModeMessages.checkJoiningEnabled(player)) {
return;
}
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof TimeTrialMode) {
return;
}
player.closeInventory();
Session.create(TimeTrialGenerator::new, null, null, player);
}
}

View file

@ -14,6 +14,7 @@ import dev.loki.loparkour.player.spectator.ParkourSpectator;
import dev.loki.loparkour.session.core.Session;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -152,6 +153,14 @@ public class ParkourGenerator {
return state.history.get(state.history.size() - 1);
}
/**
* Hook for modes to force a fixed block material on the whole course
* (e.g. a per-player color in duels), or {@code null} to use the style.
*/
public @Nullable BlockData selectBlockData() {
return null;
}
// Event listener management
/**

View file

@ -31,6 +31,10 @@ public class BlockSelector {
*/
@NotNull
public BlockData selectBlockData() {
final BlockData forced = generator.selectBlockData();
if (forced != null) {
return forced;
}
final Style style = Registry.getStyle(generator.profile.get("style").value());
if (style == null) {
final List<Style> styles = Registry.getStyles();

View file

@ -2,7 +2,11 @@ package dev.loki.loparkour.mode.base;
import dev.loki.loparkour.api.core.Registry;
import dev.loki.loparkour.duels.DuelsMode;
import dev.loki.loparkour.elytra.mode.CloseMode;
import dev.loki.loparkour.elytra.mode.ElytraMode;
import dev.loki.loparkour.elytra.mode.MinSpeedMode;
import dev.loki.loparkour.elytra.mode.SpeedDemonMode;
import dev.loki.loparkour.elytra.mode.TimeTrialMode;
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
import dev.loki.loparkour.mode.impl.multi.CoopMode;
import dev.loki.loparkour.mode.impl.multi.RaceMode;
@ -24,6 +28,10 @@ public final class Modes {
public static CoopMode COOP;
public static GravityShiftMode GRAVITY_SHIFT;
public static ElytraMode ELYTRA;
public static SpeedDemonMode SPEED_DEMON;
public static MinSpeedMode MIN_SPEED;
public static TimeTrialMode TIME_TRIAL;
public static CloseMode CLOSE;
public static DuelsMode DUELS;
public static void init() {
@ -35,6 +43,10 @@ public final class Modes {
COOP = (CoopMode) Registry.getMode("coop");
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
ELYTRA = (ElytraMode) Registry.getMode("elytra");
SPEED_DEMON = (SpeedDemonMode) Registry.getMode("speeddemon");
MIN_SPEED = (MinSpeedMode) Registry.getMode("minspeed");
TIME_TRIAL = (TimeTrialMode) Registry.getMode("timetrial");
CLOSE = (CloseMode) Registry.getMode("close");
DUELS = (DuelsMode) Registry.getMode("duels");
}
}