feat: elytra firework boost, obstacle mode, duels menu, tests

- ElytraBoost: one-shot firework boost (v*0.7+dir*0.6, FIREWORKS_SPARK,
  rocket sounds) on entering ascending section + sustained thrust; Particle.
  FIREWORK doesn't exist in spigot-api 1.20.4 -> FIREWORKS_SPARK
- ObstacleGenerator (client-side obstacles: PILLARS/LINES/HOLE_IN_WALL/
  CIRCLE/DIAMOND via ClientBlockChanger.sendNow) + ObstacleMode + locales
- DuelsMenu (lists open duels sessions, click to join) + /parkour duels
  command + tab completion + locales play.multi.duels.{name,menu_empty}
- Tests: ElytraSection/PointType/KnotDirector/SingleDuelsGenerator (15 new)
- checkstyle 0/0 (main+test), PMD 0, 41 tests pass
This commit is contained in:
loki5512344 2026-08-05 21:49:11 +02:00
parent 5707cf2d24
commit 3b5d3d97d5
Signed by: boba
GPG key ID: 253067914055423B
19 changed files with 804 additions and 28 deletions

View file

@ -3,6 +3,7 @@ package dev.loki.loparkour.menu.core;
import dev.loki.loparkour.menu.community.CommunityMenu;
import dev.loki.loparkour.menu.community.LeaderboardsMenu;
import dev.loki.loparkour.menu.community.SingleLeaderboardMenu;
import dev.loki.loparkour.menu.lobby.DuelsMenu;
import dev.loki.loparkour.menu.lobby.LobbyMenu;
import dev.loki.loparkour.menu.lobby.PlayerManagementMenu;
import dev.loki.loparkour.menu.play.PlayMenu;
@ -37,6 +38,7 @@ public final class Menus {
public static ParkourSettingsMenu PARKOUR_SETTINGS = new ParkourSettingsMenu();
// lobby
public static DuelsMenu DUELS = new DuelsMenu();
public static LobbyMenu LOBBY = new LobbyMenu();
public static PlayerManagementMenu PLAYER_MANAGEMENT = new PlayerManagementMenu();

View file

@ -0,0 +1,123 @@
package dev.loki.loparkour.menu.lobby;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.duels.DuelsGenerator;
import dev.loki.loparkour.menu.core.LPMenu;
import dev.loki.loparkour.mode.base.Modes;
import dev.loki.loparkour.player.core.ParkourUser;
import dev.loki.loparkour.session.core.Session;
import dev.loki.loparkour.util.text.ColorUtil;
import dev.loki.loparkour.world.core.Divider;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* Lists open duels sessions and lets the player join one.
*/
public class DuelsMenu extends LPMenu {
private static final int ROWS = 3;
private static final int ITEMS_PER_PAGE = 7;
private static final int FIRST_SLOT = 10;
private static final int PREV_SLOT = 0;
private static final int NEXT_SLOT = 8;
private static final int SLOT_EMPTY = 13;
private static final int SLOT_CLOSE = 22;
private int currentPage = 0;
@Override
public void open(@NotNull Player player) {
open(player, 0);
}
public void open(@NotNull Player player, int page) {
this.currentPage = page;
final String locale = locale(player);
final String title = Locales.getString(locale, "play.multi.duels.name");
final List<Session> sessions = collectOpenSessions();
final int totalPages = (int) Math.ceil((double) sessions.size() / ITEMS_PER_PAGE);
final int startIndex = currentPage * ITEMS_PER_PAGE;
final int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, sessions.size());
InventoryGUI gui = baseGui(title, ROWS);
if (sessions.isEmpty()) {
gui = gui.setItem(SLOT_EMPTY, localeItem(player, "play.multi.duels.menu_empty"));
} else {
for (int i = startIndex; i < endIndex; i++) {
final Session session = sessions.get(i);
final int slot = FIRST_SLOT + (i - startIndex);
gui = gui.setItem(slot, sessionItem(player, session), e -> {
Modes.DUELS.join(player, session);
player.closeInventory();
});
}
}
if (currentPage > 0) {
gui = gui.setItem(PREV_SLOT, backItem(player), e -> open(player, currentPage - 1));
}
if (currentPage < totalPages - 1) {
gui = gui.setItem(NEXT_SLOT, nextItem(player), e -> open(player, currentPage + 1));
}
gui.setItem(SLOT_CLOSE, closeButton(player), e -> player.closeInventory())
.open(player);
}
private List<Session> collectOpenSessions() {
final int maxPlayers = Modes.DUELS.getMaxPlayers();
final List<Session> sessions = new ArrayList<>();
for (final Session session : Divider.SECTIONS.keySet()) {
if (isOpenForJoining(session, maxPlayers)) {
sessions.add(session);
}
}
return sessions;
}
private boolean isOpenForJoining(Session session, int maxPlayers) {
return session.generator instanceof DuelsGenerator generator
&& generator.allowJoining
&& session.getPlayers().size() < maxPlayers;
}
private ItemStack sessionItem(@NotNull Player player, @NotNull Session session) {
final DuelsGenerator generator = (DuelsGenerator) session.generator;
final ItemStack item = new ItemStack(Material.PLAYER_HEAD);
applySkullOwner(player, item, generator.owner);
final ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color(generator.owner.getName()));
item.setItemMeta(meta);
}
return item;
}
private void applySkullOwner(@NotNull Player player, @NotNull ItemStack skull, @NotNull ParkourUser owner) {
if (ParkourUser.isBedrockPlayer(player)) {
return;
}
if (owner.getName() == null || owner.getName().startsWith(".")) {
return;
}
final SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) {
meta.setOwningPlayer(owner.player);
skull.setItemMeta(meta);
}
}
}