feat: add Duels mode (MultiMode, generator-per-player)
- duels/: DuelsMode (MultiMode), DuelsGenerator (owner: per-player
SingleDuelsGenerator map, allowJoining, goal), SingleDuelsGenerator
(getPlayers()=List.of(own player) -> independent course/score; score
reaches goal -> win), DuelsCountdown (10s countdown + race start)
- win(): sorted scoreboard, victory/loss titles, return to default after 10s;
leave() only removePlayer (no Coop recursion)
- config modes.duels.{enabled,max-players,goal,island-distance}, locales
play.multi.duels.* (ru/en), registered in Modes/PluginBootstrap
- checkstyle 0/0, PMD 0, build green
This commit is contained in:
parent
8fde1b579b
commit
acb884b70b
11 changed files with 494 additions and 3 deletions
|
|
@ -7,6 +7,7 @@ import dev.loki.loparkour.command.core.LoParkourCommand;
|
|||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.duels.DuelsMode;
|
||||
import dev.loki.loparkour.hook.holo.HoloHook;
|
||||
import dev.loki.loparkour.hook.papi.PAPIHook;
|
||||
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
||||
|
|
@ -78,6 +79,7 @@ public final class PluginBootstrap {
|
|||
Registry.register(new CoopMode());
|
||||
Registry.register(new GravityShiftMode());
|
||||
Registry.register(new ElytraMode());
|
||||
Registry.register(new DuelsMode());
|
||||
Modes.init();
|
||||
}
|
||||
|
||||
|
|
|
|||
110
src/main/java/dev/loki/loparkour/duels/DuelsCountdown.java
Normal file
110
src/main/java/dev/loki/loparkour/duels/DuelsCountdown.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package dev.loki.loparkour.duels;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Start countdown for a duels session: 10s timer with titles, then starts the
|
||||
* race by generating the first parkour block of every player's course.
|
||||
*/
|
||||
final class DuelsCountdown {
|
||||
|
||||
private static final int COUNTDOWN_SECONDS = 10;
|
||||
private static final int COUNTDOWN_PERIOD = 20;
|
||||
private static final int TITLE_STAY = 21;
|
||||
private static final int COUNTDOWN_COLOR_START = 3;
|
||||
private static final long BANNER_DELAY_TICKS = 5L;
|
||||
private static final int FIRST_BLOCK_FORWARD = 6;
|
||||
private static final double SPAWN_CENTER_OFFSET = 0.5;
|
||||
private static final int COLOR_RED = 1;
|
||||
private static final int COLOR_YELLOW = 2;
|
||||
private static final int COLOR_GREEN = 3;
|
||||
private static final String RED = "&#DA2626";
|
||||
private static final String YELLOW = "&#DCD31D";
|
||||
private static final String GREEN = "*D929";
|
||||
private static final String DEFAULT_COLOR = "E120";
|
||||
|
||||
private final DuelsGenerator generator;
|
||||
private BukkitTask countdownTask;
|
||||
|
||||
DuelsCountdown(@NotNull DuelsGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
void giveStartItem(@NotNull ParkourPlayer owner) {
|
||||
Bukkit.getScheduler().runTaskLater(LoParkour.getPlugin(), () -> {
|
||||
if (owner.player.isOnline()) {
|
||||
owner.player.getInventory().addItem(Locales.getItem(owner.locale, "play.multi.duels.start").build());
|
||||
}
|
||||
}, BANNER_DELAY_TICKS);
|
||||
}
|
||||
|
||||
void start() {
|
||||
cancel();
|
||||
final int[] counter = {COUNTDOWN_SECONDS};
|
||||
countdownTask = Bukkit.getScheduler().runTaskTimer(LoParkour.getPlugin(),
|
||||
() -> tick(counter), 0L, COUNTDOWN_PERIOD);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (countdownTask != null) {
|
||||
countdownTask.cancel();
|
||||
countdownTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
void tick(@NotNull int[] counter) {
|
||||
if (generator.state.stopped) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
final int current = counter[0]--;
|
||||
if (current == 0) {
|
||||
startRace();
|
||||
return;
|
||||
}
|
||||
if (current <= COUNTDOWN_COLOR_START) {
|
||||
sendCountdownTitle(current);
|
||||
}
|
||||
}
|
||||
|
||||
void startRace() {
|
||||
generator.allowJoining = false;
|
||||
for (final Map.Entry<ParkourPlayer, SingleDuelsGenerator> entry : generator.playerGenerators.entrySet()) {
|
||||
final Location ps = generator.playerSpawns.get(entry.getKey());
|
||||
if (ps == null) {
|
||||
continue;
|
||||
}
|
||||
final Vector heading = entry.getValue().state.heading;
|
||||
final Location bs = ps.clone().add(FIRST_BLOCK_FORWARD * heading.getX() - SPAWN_CENTER_OFFSET, 0,
|
||||
FIRST_BLOCK_FORWARD * heading.getZ() - SPAWN_CENTER_OFFSET);
|
||||
entry.getValue().generateFirst(ps, bs);
|
||||
}
|
||||
for (final ParkourPlayer pp : new ArrayList<>(generator.playerGenerators.keySet())) {
|
||||
pp.player.sendTitle(ColorUtil.color(Locales.getString(pp.locale, "play.multi.duels.go")), "", 0, TITLE_STAY, 0);
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
void sendCountdownTitle(int current) {
|
||||
final String color = switch (current) {
|
||||
case COLOR_RED -> RED;
|
||||
case COLOR_YELLOW -> YELLOW;
|
||||
case COLOR_GREEN -> GREEN;
|
||||
default -> DEFAULT_COLOR;
|
||||
};
|
||||
for (final ParkourPlayer pp : new ArrayList<>(generator.playerGenerators.keySet())) {
|
||||
pp.player.sendTitle(ColorUtil.color(color + current), "", 0, TITLE_STAY, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/main/java/dev/loki/loparkour/duels/DuelsGenerator.java
Normal file
167
src/main/java/dev/loki/loparkour/duels/DuelsGenerator.java
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package dev.loki.loparkour.duels;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Coordinator of a duels session: owns the session generator, builds one
|
||||
* {@link SingleDuelsGenerator} per player and drives them every tick.
|
||||
*/
|
||||
public class DuelsGenerator extends ParkourGenerator {
|
||||
|
||||
private static final long WIN_RETURN_DELAY_TICKS = 200L;
|
||||
private static final int PLATFORM_RADIUS = 1;
|
||||
private static final double SPAWN_CENTER_OFFSET = 0.5;
|
||||
private static final int DEFAULT_GOAL = 50;
|
||||
private static final int DEFAULT_ISLAND_DISTANCE = 20;
|
||||
private static final int VICTORY_FADE_IN = 1;
|
||||
private static final int VICTORY_STAY = 100;
|
||||
private static final int VICTORY_FADE_OUT = 10;
|
||||
|
||||
public boolean allowJoining = true;
|
||||
public final ParkourPlayer owner;
|
||||
public final Map<ParkourPlayer, SingleDuelsGenerator> playerGenerators = new LinkedHashMap<>();
|
||||
public final int goal;
|
||||
public final int islandDistance;
|
||||
final Map<ParkourPlayer, Location> playerSpawns = new LinkedHashMap<>();
|
||||
private final DuelsCountdown countdown;
|
||||
|
||||
public DuelsGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
this.goal = Config.CONFIG.getInt("modes.duels.goal", DEFAULT_GOAL);
|
||||
this.islandDistance = Config.CONFIG.getInt("modes.duels.island-distance", DEFAULT_ISLAND_DISTANCE);
|
||||
this.owner = session.getPlayers().get(0);
|
||||
this.countdown = new DuelsCountdown(this);
|
||||
addPlayer(owner);
|
||||
countdown.giveStartItem(owner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateFirst(Location spawn, Location block) {
|
||||
playerSpawns.put(owner, spawn);
|
||||
countdown.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startTick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (!state.stopped) {
|
||||
playerGenerators.values().forEach(SingleDuelsGenerator::tick);
|
||||
}
|
||||
}
|
||||
|
||||
public void addPlayer(@NotNull ParkourPlayer pp) {
|
||||
if (!allowJoining || playerGenerators.containsKey(pp)) {
|
||||
return;
|
||||
}
|
||||
final SingleDuelsGenerator gen = new SingleDuelsGenerator(session);
|
||||
gen.owningGenerator = this;
|
||||
gen.setOwnedPlayer(pp);
|
||||
gen.setPlayerIndex(playerGenerators.size());
|
||||
gen.state.heading = Option.HEADING.getDirection();
|
||||
final Location spawnBase = session.getSpawnLocation();
|
||||
if (spawnBase == null || spawnBase.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
Location platformCenter = spawnBase;
|
||||
if (pp != owner) {
|
||||
platformCenter = spawnBase.clone().add(0, 0, islandDistance * (double) playerGenerators.size());
|
||||
for (int dx = -PLATFORM_RADIUS; dx <= PLATFORM_RADIUS; dx++) {
|
||||
for (int dz = -PLATFORM_RADIUS; dz <= PLATFORM_RADIUS; dz++) {
|
||||
platformCenter.getBlock().getRelative(dx, 0, dz).setType(Material.STONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
final Location playerSpawn = platformCenter.clone().add(SPAWN_CENTER_OFFSET, 1, SPAWN_CENTER_OFFSET);
|
||||
playerSpawns.put(pp, playerSpawn);
|
||||
playerGenerators.put(pp, gen);
|
||||
pp.player.teleport(playerSpawn);
|
||||
}
|
||||
|
||||
public void removePlayer(@NotNull ParkourPlayer pp) {
|
||||
final SingleDuelsGenerator gen = playerGenerators.remove(pp);
|
||||
if (gen == null) {
|
||||
return;
|
||||
}
|
||||
gen.reset(false);
|
||||
if (pp == owner) {
|
||||
countdown.cancel();
|
||||
returnToLobby();
|
||||
return;
|
||||
}
|
||||
if (playerGenerators.size() == 1) {
|
||||
win(new ArrayList<>(playerGenerators.keySet()).get(0));
|
||||
}
|
||||
}
|
||||
|
||||
public void win(@NotNull ParkourPlayer winner) {
|
||||
if (state.stopped) {
|
||||
return;
|
||||
}
|
||||
state.stopped = true;
|
||||
countdown.cancel();
|
||||
final String winningName = winner.getName();
|
||||
final String winningTime = playerGenerators.get(winner).getFormattedTime();
|
||||
final List<Map.Entry<ParkourPlayer, SingleDuelsGenerator>> leaderboard =
|
||||
new ArrayList<>(playerGenerators.entrySet());
|
||||
leaderboard.sort((one, two) -> two.getValue().state.score - one.getValue().state.score);
|
||||
for (final ParkourPlayer pp : new ArrayList<>(playerGenerators.keySet())) {
|
||||
pp.send(Locales.getString(pp.locale, "play.multi.duels.overview").formatted(winningName, winningTime));
|
||||
for (int i = 0; i < leaderboard.size(); i++) {
|
||||
pp.send(Locales.getString(pp.locale, "play.multi.duels.place")
|
||||
.formatted(i + 1, leaderboard.get(i).getKey().getName(), leaderboard.get(i).getValue().state.score));
|
||||
}
|
||||
pp.player.sendTitle(ColorUtil.color(Locales.getString(pp.locale,
|
||||
pp == winner ? "play.multi.duels.victory" : "play.multi.duels.loss")), "", VICTORY_FADE_IN, VICTORY_STAY, VICTORY_FADE_OUT);
|
||||
}
|
||||
Bukkit.getScheduler().runTaskLater(LoParkour.getPlugin(), () -> returnToLobby(), WIN_RETURN_DELAY_TICKS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
if (regenerate) {
|
||||
return;
|
||||
}
|
||||
countdown.cancel();
|
||||
for (final ParkourPlayer pp : new ArrayList<>(playerGenerators.keySet())) {
|
||||
removePlayer(pp);
|
||||
}
|
||||
}
|
||||
|
||||
public int getGoal() {
|
||||
return goal;
|
||||
}
|
||||
|
||||
private void returnToLobby() {
|
||||
for (final ParkourPlayer pp : new ArrayList<>(playerGenerators.keySet())) {
|
||||
if (pp.player.isOnline() && playerGenerators.containsKey(pp)) {
|
||||
Modes.DEFAULT.create(pp.player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.DUELS;
|
||||
}
|
||||
}
|
||||
81
src/main/java/dev/loki/loparkour/duels/DuelsMode.java
Normal file
81
src/main/java/dev/loki/loparkour/duels/DuelsMode.java
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package dev.loki.loparkour.duels;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.mode.base.MultiMode;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Duels mode: every player gets their own parkour course, first to the goal wins.
|
||||
*/
|
||||
public class DuelsMode implements MultiMode {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "duels";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.multi.duels.item");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Leaderboard getLeaderboard() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPlayers() {
|
||||
return Config.CONFIG.getInt("modes.duels.max-players", 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator instanceof DuelsGenerator) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
final int maxPlayers = getMaxPlayers();
|
||||
Session.create(DuelsGenerator::new,
|
||||
session -> session.getPlayers().size() < maxPlayers && ((DuelsGenerator) session.generator).allowJoining,
|
||||
null,
|
||||
player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void join(Player player, Session session) {
|
||||
if (!session.isAcceptingPlayers()) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
final ParkourPlayer pp = ParkourUser.register(player, session);
|
||||
session.addPlayers(pp);
|
||||
((DuelsGenerator) session.generator).addPlayer(pp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void leave(Player player, Session session) {
|
||||
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp == null || session.getPlayers().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
((DuelsGenerator) session.generator).removePlayer(pp);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package dev.loki.loparkour.duels;
|
||||
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* One parkour course per duels player, isolated from everyone else.
|
||||
*
|
||||
* <p>Scoring only looks at the owned player: {@link #getPlayers()} returns just
|
||||
* the owned player, so {@code state.history}/{@code state.score} belong to that
|
||||
* single course. The owning {@link DuelsGenerator} ticks every instance.</p>
|
||||
*/
|
||||
public class SingleDuelsGenerator extends ParkourGenerator {
|
||||
|
||||
private static final List<Material> COLORS = List.of(
|
||||
Material.LIGHT_BLUE_CONCRETE, Material.RED_CONCRETE, Material.LIME_CONCRETE, Material.YELLOW_CONCRETE,
|
||||
Material.ORANGE_CONCRETE, Material.BLUE_CONCRETE, Material.MAGENTA_CONCRETE, Material.WHITE_CONCRETE,
|
||||
Material.LIGHT_GRAY_CONCRETE, Material.BLACK_CONCRETE, Material.GREEN_CONCRETE, Material.BROWN_CONCRETE,
|
||||
Material.CYAN_CONCRETE, Material.PURPLE_CONCRETE, Material.GRAY_CONCRETE, Material.PINK_CONCRETE);
|
||||
|
||||
public DuelsGenerator owningGenerator;
|
||||
private ParkourPlayer ownedPlayer;
|
||||
private BlockData blockData;
|
||||
private boolean won;
|
||||
|
||||
public SingleDuelsGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
public void setOwnedPlayer(@NotNull ParkourPlayer pp) {
|
||||
this.ownedPlayer = pp;
|
||||
}
|
||||
|
||||
public void setPlayerIndex(int index) {
|
||||
this.blockData = COLORS.get(index % COLORS.size()).createBlockData();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<ParkourPlayer> getPlayers() {
|
||||
return ownedPlayer == null ? List.of() : List.of(ownedPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startTick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
checkWin();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void score() {
|
||||
super.score();
|
||||
checkWin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duels has no persistent leaderboard; falling must not register anything.
|
||||
*/
|
||||
@Override
|
||||
public void registerScore(String time, String difficulty, int score) {
|
||||
}
|
||||
|
||||
private void checkWin() {
|
||||
if (!won && owningGenerator != null && state.score >= owningGenerator.goal) {
|
||||
won = true;
|
||||
owningGenerator.win(ownedPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.DUELS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
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.ElytraMode;
|
||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||
|
|
@ -23,6 +24,7 @@ public final class Modes {
|
|||
public static CoopMode COOP;
|
||||
public static GravityShiftMode GRAVITY_SHIFT;
|
||||
public static ElytraMode ELYTRA;
|
||||
public static DuelsMode DUELS;
|
||||
|
||||
public static void init() {
|
||||
DEFAULT = (DefaultMode) Registry.getMode("default");
|
||||
|
|
@ -33,5 +35,6 @@ public final class Modes {
|
|||
COOP = (CoopMode) Registry.getMode("coop");
|
||||
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
||||
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
||||
DUELS = (DuelsMode) Registry.getMode("duels");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue