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:
parent
5707cf2d24
commit
3b5d3d97d5
19 changed files with 804 additions and 28 deletions
|
|
@ -16,6 +16,7 @@ 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.ObstacleMode;
|
||||
import dev.loki.loparkour.elytra.mode.SpeedDemonMode;
|
||||
import dev.loki.loparkour.elytra.mode.TimeTrialMode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
|
@ -87,6 +88,7 @@ public final class PluginBootstrap {
|
|||
Registry.register(new MinSpeedMode());
|
||||
Registry.register(new TimeTrialMode());
|
||||
Registry.register(new CloseMode());
|
||||
Registry.register(new ObstacleMode());
|
||||
Registry.register(new DuelsMode());
|
||||
Modes.init();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
|||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.command.util.CommandUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -33,7 +34,7 @@ class CommandRouter {
|
|||
void dispatch(String[] args, CommandSender sender, @Nullable Player p) {
|
||||
switch (args.length) {
|
||||
case 0 -> player.handleNoArgs(sender, p);
|
||||
case 1 -> player.handle(args[0], sender, p);
|
||||
case 1 -> handleSingle(args[0], sender, p);
|
||||
case 2 -> handle2(args[0], args[1], sender, p);
|
||||
case THREE_ARGS -> handle3(args[0], args[1], args[2], sender, p);
|
||||
case FOUR_ARGS -> handle4(args[0], args[1], args[2], args[FOURTH_ARG_INDEX], sender, p);
|
||||
|
|
@ -41,6 +42,14 @@ class CommandRouter {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleSingle(String arg, CommandSender sender, @Nullable Player p) {
|
||||
if (p != null && "duels".equalsIgnoreCase(arg)) {
|
||||
Menus.DUELS.open(p);
|
||||
return;
|
||||
}
|
||||
player.handle(arg, sender, p);
|
||||
}
|
||||
|
||||
private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) {
|
||||
if (isAdminCommand(a1)) {
|
||||
admin.handle(a1, a2, sender, p);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class CommandTabCompleter {
|
|||
}
|
||||
if (ParkourOption.PLAY.mayPerform(sender)) {
|
||||
completions.add("play");
|
||||
completions.add("duels");
|
||||
}
|
||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
completions.add("leaderboard");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package dev.loki.loparkour.elytra.generator;
|
||||
|
||||
import dev.loki.loparkour.elytra.section.ElytraSection;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
final class ElytraBoost {
|
||||
private static final double SUSTAINED_BOOST = 0.08;
|
||||
private static final double INITIAL_KEEP = 0.7;
|
||||
private static final double INITIAL_PUSH = 0.6;
|
||||
private static final double EPSILON = 1e-6;
|
||||
private static final int FIREWORK_COUNT = 60;
|
||||
private static final int FIREWORK_PARTICLE_COUNT = 6;
|
||||
private static final double NEAR_KNOT_DISTANCE = 6.0;
|
||||
private static final int PARTICLE_EVERY_TICKS = 4;
|
||||
private static final double BURST_OFFSET = 0.6;
|
||||
private static final double BURST_SPEED = 0.15;
|
||||
private static final float BLAST_VOLUME = 0.8f;
|
||||
private static final float BLAST_PITCH = 1.4f;
|
||||
private static final double TRAIL_OFFSET = 0.2;
|
||||
private static final double TRAIL_SPEED = 0.02;
|
||||
private int boostedSectionIdx = Integer.MIN_VALUE;
|
||||
|
||||
void tick(Player player, ElytraSection section, int idx) {
|
||||
if (!section.isAscending()) {
|
||||
return;
|
||||
}
|
||||
final Vector pos = player.getLocation().toVector();
|
||||
final Vector direction = section.end().clone().subtract(section.beginning()).normalize();
|
||||
if (direction.lengthSquared() < EPSILON) {
|
||||
return;
|
||||
}
|
||||
if (idx != boostedSectionIdx && section.isNearKnot(pos, 0, NEAR_KNOT_DISTANCE)) {
|
||||
applyInitialBoost(player, idx, direction);
|
||||
}
|
||||
if (pos.getX() >= section.beginning().getX() && pos.getX() < section.end().getX()) {
|
||||
sustain(player, direction);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
boostedSectionIdx = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
private void applyInitialBoost(Player player, int idx, Vector direction) {
|
||||
boostedSectionIdx = idx;
|
||||
player.setGliding(true);
|
||||
final Vector velocity = player.getVelocity().clone().multiply(INITIAL_KEEP)
|
||||
.add(direction.clone().multiply(INITIAL_PUSH));
|
||||
player.setVelocity(velocity);
|
||||
final Location loc = player.getLocation();
|
||||
loc.getWorld().spawnParticle(Particle.FIREWORKS_SPARK, loc, FIREWORK_COUNT, BURST_OFFSET, BURST_OFFSET, BURST_OFFSET, BURST_SPEED);
|
||||
loc.getWorld().playSound(loc, Sound.ENTITY_FIREWORK_ROCKET_LAUNCH, 1.0f, 1.0f);
|
||||
loc.getWorld().playSound(loc, Sound.ENTITY_FIREWORK_ROCKET_BLAST, BLAST_VOLUME, BLAST_PITCH);
|
||||
}
|
||||
|
||||
private void sustain(Player player, Vector direction) {
|
||||
if (!player.isGliding() && player.getInventory().getChestplate() != null
|
||||
&& player.getInventory().getChestplate().getType() == Material.ELYTRA) {
|
||||
player.setGliding(true);
|
||||
}
|
||||
player.setVelocity(player.getVelocity().add(direction.clone().multiply(SUSTAINED_BOOST)));
|
||||
if (player.getTicksLived() % PARTICLE_EVERY_TICKS == 0) {
|
||||
final Location loc = player.getLocation();
|
||||
loc.getWorld().spawnParticle(Particle.FIREWORKS_SPARK, loc, FIREWORK_PARTICLE_COUNT,
|
||||
TRAIL_OFFSET, TRAIL_OFFSET, TRAIL_OFFSET, TRAIL_SPEED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,10 +21,8 @@ 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;
|
||||
private static final double GENERATE_DISTANCE = 6.0;
|
||||
private static final double EPSILON = 1e-6;
|
||||
public final Map<Integer, ElytraSection> sections = new LinkedHashMap<>();
|
||||
public final ClientBlockChanger blockChanger = new ClientBlockChanger();
|
||||
protected double movementScore = 0.0;
|
||||
|
|
@ -35,7 +33,7 @@ public class ElytraGenerator extends ParkourGenerator {
|
|||
protected final ElytraPointType pointType;
|
||||
private final int radius;
|
||||
private final Material material;
|
||||
private int boostedSectionIdx = Integer.MIN_VALUE;
|
||||
private final ElytraBoost boost = new ElytraBoost();
|
||||
|
||||
public ElytraGenerator(Session session) {
|
||||
this(session, ElytraPointType.CIRCLE);
|
||||
|
|
@ -105,9 +103,7 @@ public class ElytraGenerator extends ParkourGenerator {
|
|||
reset(true);
|
||||
return;
|
||||
}
|
||||
if (section.isAscending()) {
|
||||
sustainAscendingPush(player, section, idx);
|
||||
}
|
||||
boost.tick(player, section, idx);
|
||||
if (section.isNearKnot(pos, 2, GENERATE_DISTANCE)) {
|
||||
generate();
|
||||
}
|
||||
|
|
@ -125,7 +121,7 @@ public class ElytraGenerator extends ParkourGenerator {
|
|||
blockChanger.clear();
|
||||
movementScore = 0;
|
||||
startTime = null;
|
||||
boostedSectionIdx = Integer.MIN_VALUE;
|
||||
boost.reset();
|
||||
if (regenerate) {
|
||||
if (player != null && playerSpawn != null) {
|
||||
player.teleport(playerSpawn.toLocation(player.getWorld()));
|
||||
|
|
@ -180,20 +176,4 @@ public class ElytraGenerator extends ParkourGenerator {
|
|||
}
|
||||
return progress > 0 && (!player.isGliding() || !target.getValue().isNearPoint(pos, (int) progress, radius));
|
||||
}
|
||||
|
||||
private void sustainAscendingPush(Player player, ElytraSection section, int idx) {
|
||||
final Vector pos = player.getLocation().toVector();
|
||||
if (pos.getX() < section.beginning().getX() || pos.getX() >= section.end().getX()) {
|
||||
return;
|
||||
}
|
||||
if (boostedSectionIdx != idx) {
|
||||
boostedSectionIdx = idx;
|
||||
player.setGliding(true);
|
||||
}
|
||||
final Vector direction = section.end().clone().subtract(section.beginning()).normalize();
|
||||
if (direction.lengthSquared() < EPSILON) {
|
||||
return;
|
||||
}
|
||||
player.setVelocity(player.getVelocity().add(direction.multiply(SUSTAINED_BOOST)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package dev.loki.loparkour.elytra.generator;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.elytra.section.ElytraPointType;
|
||||
import dev.loki.loparkour.elytra.section.ElytraSection;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* Elytra mode that places client-side obstacles on the course which the
|
||||
* player has to fly around. Obstacles are spoofed via
|
||||
* {@link dev.loki.loparkour.elytra.section.ClientBlockChanger#sendNow} and
|
||||
* undone with AIR the same way, so no server chunk is dirtied or pinned.
|
||||
*/
|
||||
public class ObstacleGenerator extends ElytraGenerator {
|
||||
|
||||
private static final int DEFAULT_RADIUS = 3;
|
||||
private static final int OBSTACLE_STEP = 3;
|
||||
private static final int OBSTACLE_KNOT = 3;
|
||||
|
||||
private final Map<Integer, List<Vector>> obstacles = new HashMap<>();
|
||||
private final int radius;
|
||||
|
||||
public ObstacleGenerator(Session session) {
|
||||
super(session);
|
||||
radius = Config.GENERATION.getInt("elytra.radius", DEFAULT_RADIUS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate() {
|
||||
super.generate();
|
||||
final int lastIdx = sections.keySet().stream().max(Integer::compareTo).orElse(-1);
|
||||
final ElytraSection section = sections.get(lastIdx);
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
generateObstacle(lastIdx, section, 1);
|
||||
generateObstacle(lastIdx, section, OBSTACLE_KNOT);
|
||||
}
|
||||
|
||||
private void generateObstacle(int idx, ElytraSection section, int knotIdx) {
|
||||
final Obstacle obstacle = Obstacle.values()[ThreadLocalRandom.current().nextInt(Obstacle.values().length)];
|
||||
final List<Vector> points = obstacle.getPoints(section.getKnot(knotIdx), radius);
|
||||
if (points.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Player player = getPlayer();
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
blockChanger.sendNow(player, points, Material.YELLOW_CONCRETE);
|
||||
obstacles.computeIfAbsent(idx, key -> new ArrayList<>()).addAll(points);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
final Player player = getPlayer();
|
||||
if (player == null || obstacles.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final List<Integer> stale = new ArrayList<>();
|
||||
for (final Integer key : obstacles.keySet()) {
|
||||
if (!sections.containsKey(key)) {
|
||||
stale.add(key);
|
||||
}
|
||||
}
|
||||
for (final Integer key : stale) {
|
||||
final List<Vector> points = obstacles.remove(key);
|
||||
if (points == null) {
|
||||
continue;
|
||||
}
|
||||
blockChanger.sendNow(player, points, Material.AIR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
super.reset(regenerate);
|
||||
final Player player = getPlayer();
|
||||
if (player != null) {
|
||||
for (final List<Vector> points : obstacles.values()) {
|
||||
blockChanger.sendNow(player, points, Material.AIR);
|
||||
}
|
||||
}
|
||||
obstacles.clear();
|
||||
}
|
||||
|
||||
private enum Obstacle {
|
||||
|
||||
PILLARS {
|
||||
@Override
|
||||
List<Vector> getPoints(Vector center, int radius) {
|
||||
final List<Vector> points = new ArrayList<>();
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
if (dz % OBSTACLE_STEP != 0) {
|
||||
continue;
|
||||
}
|
||||
for (int dy = -radius; dy <= radius; dy++) {
|
||||
final Vector point = center.clone().add(new Vector(0, dy, dz));
|
||||
if (point.distance(center) <= radius) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
},
|
||||
|
||||
LINES {
|
||||
@Override
|
||||
List<Vector> getPoints(Vector center, int radius) {
|
||||
final List<Vector> points = new ArrayList<>();
|
||||
for (int dy = -radius; dy <= radius; dy++) {
|
||||
if (dy % OBSTACLE_STEP != 0) {
|
||||
continue;
|
||||
}
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
final Vector point = center.clone().add(new Vector(0, dy, dz));
|
||||
if (point.distance(center) <= radius) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
},
|
||||
|
||||
HOLE_IN_WALL {
|
||||
@Override
|
||||
List<Vector> getPoints(Vector center, int radius) {
|
||||
final int offsetY = ThreadLocalRandom.current().nextInt(-2, 3);
|
||||
final int offsetZ = ThreadLocalRandom.current().nextInt(-2, 3);
|
||||
final Vector offset = center.clone().add(new Vector(0, offsetY, offsetZ));
|
||||
final int radiusSquared = radius * radius;
|
||||
final List<Vector> points = new ArrayList<>();
|
||||
for (int dy = -radius + 1; dy <= radius; dy++) {
|
||||
for (int dz = -radius + 1; dz <= radius; dz++) {
|
||||
final Vector point = center.clone().add(new Vector(0, dy, dz));
|
||||
if (point.distanceSquared(center) <= radiusSquared && point.distanceSquared(offset) >= 2) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
},
|
||||
|
||||
CIRCLE {
|
||||
@Override
|
||||
List<Vector> getPoints(Vector center, int radius) {
|
||||
final List<Vector> points = new ArrayList<>();
|
||||
for (int r = 2; r <= radius; r += 2) {
|
||||
points.addAll(ElytraPointType.CIRCLE.getPoints(center, r));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
},
|
||||
|
||||
DIAMOND {
|
||||
@Override
|
||||
List<Vector> getPoints(Vector center, int radius) {
|
||||
final List<Vector> points = new ArrayList<>();
|
||||
for (int r = 2; r <= radius; r += OBSTACLE_STEP) {
|
||||
points.addAll(diamond(center, r));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private List<Vector> diamond(Vector center, int radius) {
|
||||
final List<Vector> points = new ArrayList<>();
|
||||
final Vector newCenter = center.clone().add(new Vector(0, -radius + 2, 0));
|
||||
final int r = radius - 2;
|
||||
final int range = 2 * r;
|
||||
for (int dy = 0; dy <= range; dy++) {
|
||||
if (dy < r) {
|
||||
points.add(newCenter.clone().add(new Vector(0, dy, -dy)));
|
||||
points.add(newCenter.clone().add(new Vector(0, dy, dy)));
|
||||
} else {
|
||||
final int newDz = range - dy;
|
||||
points.add(newCenter.clone().add(new Vector(0, dy, -newDz)));
|
||||
points.add(newCenter.clone().add(new Vector(0, dy, newDz)));
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
};
|
||||
|
||||
abstract List<Vector> getPoints(Vector center, int radius);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.loparkour.elytra.mode;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.elytra.generator.ObstacleGenerator;
|
||||
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 client-side obstacles to fly around.
|
||||
*/
|
||||
public class ObstacleMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "obstacle";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.elytra.obstacle");
|
||||
}
|
||||
|
||||
@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 ObstacleMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(ObstacleGenerator::new, null, null, player);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.loparkour.elytra.section;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
|
|
@ -12,6 +13,7 @@ import org.bukkit.World;
|
|||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Pipes pending client-side block updates to the player without leaking server
|
||||
|
|
@ -90,6 +92,28 @@ public class ClientBlockChanger {
|
|||
renderBatches(player, toSend, material);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately sends all given positions to the player as the given material,
|
||||
* bypassing the chunk queue.
|
||||
*
|
||||
* @param player the player whose client should be updated.
|
||||
* @param vectors the positions to update.
|
||||
* @param material material to display the blocks as.
|
||||
*/
|
||||
public void sendNow(@NotNull Player player, @NotNull Collection<Vector> vectors, @NotNull Material material) {
|
||||
if (vectors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final World world = player.getWorld();
|
||||
final List<BlockState> states = new ArrayList<>(vectors.size());
|
||||
for (final Vector v : vectors) {
|
||||
final BlockState state = v.toLocation(world).getBlock().getState();
|
||||
state.setType(material);
|
||||
states.add(state);
|
||||
}
|
||||
player.sendBlockChanges(states);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the queue without sending anything.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
123
src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java
Normal file
123
src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ 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.ObstacleMode;
|
||||
import dev.loki.loparkour.elytra.mode.SpeedDemonMode;
|
||||
import dev.loki.loparkour.elytra.mode.TimeTrialMode;
|
||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||
|
|
@ -32,6 +33,7 @@ public final class Modes {
|
|||
public static MinSpeedMode MIN_SPEED;
|
||||
public static TimeTrialMode TIME_TRIAL;
|
||||
public static CloseMode CLOSE;
|
||||
public static ObstacleMode OBSTACLE;
|
||||
public static DuelsMode DUELS;
|
||||
|
||||
public static void init() {
|
||||
|
|
@ -47,6 +49,7 @@ public final class Modes {
|
|||
MIN_SPEED = (MinSpeedMode) Registry.getMode("minspeed");
|
||||
TIME_TRIAL = (TimeTrialMode) Registry.getMode("timetrial");
|
||||
CLOSE = (CloseMode) Registry.getMode("close");
|
||||
OBSTACLE = (ObstacleMode) Registry.getMode("obstacle");
|
||||
DUELS = (DuelsMode) Registry.getMode("duels");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue