v1.2.4: Remove roguelike perks, update bStats to 3.2.1, add new game modes and lpschem system
- Removed roguelike perk system (double jump, magnet, shield, second chance) - Updated bStats dependency to 3.2.1 with proper relocation - Changed bStats plugin ID to 29754 - Added new game modes: Speedrun, Gravity Shift, Hardcore - Implemented .lpschem format with GZIP + JSON compression - Added JumpValidator for physics-based jump validation - Added JumpType system (neo-jump, head-hitter, fence, trapdoor, ladder) - Implemented Ghost system for recording/playback top players - Added loparkour-vilib as local dependency - Updated config with new mode settings and removed roguelike section - Cleaned up TODO.md roadmap
This commit is contained in:
parent
aecb03ccf8
commit
8aeb547897
68 changed files with 6186 additions and 163 deletions
71
src/main/java/dev/loki/loparkour/ghost/GhostData.java
Normal file
71
src/main/java/dev/loki/loparkour/ghost/GhostData.java
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GhostData {
|
||||
|
||||
private final String playerName;
|
||||
private final int score;
|
||||
private final List<GhostFrame> frames;
|
||||
|
||||
public GhostData(@NotNull String playerName, int score, @NotNull List<GhostFrame> frames) {
|
||||
this.playerName = playerName;
|
||||
this.score = score;
|
||||
this.frames = new ArrayList<>(frames);
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public List<GhostFrame> getFrames() {
|
||||
return frames;
|
||||
}
|
||||
|
||||
public void saveToFile(@NotNull File file) throws IOException {
|
||||
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
|
||||
out.writeUTF(playerName);
|
||||
out.writeInt(score);
|
||||
out.writeInt(frames.size());
|
||||
|
||||
for (GhostFrame frame : frames) {
|
||||
out.writeLong(frame.getTimestamp());
|
||||
out.writeDouble(frame.getX());
|
||||
out.writeDouble(frame.getY());
|
||||
out.writeDouble(frame.getZ());
|
||||
out.writeFloat(frame.getYaw());
|
||||
out.writeFloat(frame.getPitch());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static GhostData loadFromFile(@NotNull File file) throws IOException {
|
||||
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
|
||||
String playerName = in.readUTF();
|
||||
int score = in.readInt();
|
||||
int frameCount = in.readInt();
|
||||
|
||||
List<GhostFrame> frames = new ArrayList<>(frameCount);
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
long timestamp = in.readLong();
|
||||
double x = in.readDouble();
|
||||
double y = in.readDouble();
|
||||
double z = in.readDouble();
|
||||
float yaw = in.readFloat();
|
||||
float pitch = in.readFloat();
|
||||
|
||||
frames.add(new GhostFrame(timestamp, x, y, z, yaw, pitch));
|
||||
}
|
||||
|
||||
return new GhostData(playerName, score, frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/main/java/dev/loki/loparkour/ghost/GhostFrame.java
Normal file
60
src/main/java/dev/loki/loparkour/ghost/GhostFrame.java
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class GhostFrame {
|
||||
|
||||
private final long timestamp;
|
||||
private final double x;
|
||||
private final double y;
|
||||
private final double z;
|
||||
private final float yaw;
|
||||
private final float pitch;
|
||||
|
||||
public GhostFrame(long timestamp, @NotNull Location location) {
|
||||
this.timestamp = timestamp;
|
||||
this.x = location.getX();
|
||||
this.y = location.getY();
|
||||
this.z = location.getZ();
|
||||
this.yaw = location.getYaw();
|
||||
this.pitch = location.getPitch();
|
||||
}
|
||||
|
||||
public GhostFrame(long timestamp, double x, double y, double z, float yaw, float pitch) {
|
||||
this.timestamp = timestamp;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public double getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public float getYaw() {
|
||||
return yaw;
|
||||
}
|
||||
|
||||
public float getPitch() {
|
||||
return pitch;
|
||||
}
|
||||
|
||||
public Location toLocation(org.bukkit.World world) {
|
||||
return new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
}
|
||||
131
src/main/java/dev/loki/loparkour/ghost/GhostManager.java
Normal file
131
src/main/java/dev/loki/loparkour/ghost/GhostManager.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class GhostManager {
|
||||
|
||||
private static final File GHOST_FOLDER = LoParkour.getInFolder("ghosts");
|
||||
private static final int MAX_GHOSTS_PER_MODE = 3;
|
||||
|
||||
private final Map<String, List<GhostData>> ghostsByMode = new HashMap<>();
|
||||
private final List<GhostPlayer> activeGhosts = new ArrayList<>();
|
||||
|
||||
public GhostManager() {
|
||||
if (!GHOST_FOLDER.exists()) {
|
||||
GHOST_FOLDER.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadGhosts(@NotNull String mode) {
|
||||
File modeFolder = new File(GHOST_FOLDER, mode);
|
||||
if (!modeFolder.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<GhostData> ghosts = new ArrayList<>();
|
||||
File[] files = modeFolder.listFiles((dir, name) -> name.endsWith(".ghost"));
|
||||
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
try {
|
||||
GhostData data = GhostData.loadFromFile(file);
|
||||
ghosts.add(data);
|
||||
} catch (IOException e) {
|
||||
LoParkour.logging().error("Failed to load ghost: " + file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
|
||||
ghostsByMode.put(mode, ghosts);
|
||||
}
|
||||
|
||||
public void saveGhost(@NotNull String mode, @NotNull GhostData data) {
|
||||
File modeFolder = new File(GHOST_FOLDER, mode);
|
||||
if (!modeFolder.exists()) {
|
||||
modeFolder.mkdirs();
|
||||
}
|
||||
|
||||
List<GhostData> ghosts = ghostsByMode.getOrDefault(mode, new ArrayList<>());
|
||||
ghosts.add(data);
|
||||
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
|
||||
|
||||
while (ghosts.size() > MAX_GHOSTS_PER_MODE) {
|
||||
GhostData removed = ghosts.remove(ghosts.size() - 1);
|
||||
File file = new File(modeFolder, removed.getPlayerName() + ".ghost");
|
||||
file.delete();
|
||||
}
|
||||
|
||||
ghostsByMode.put(mode, ghosts);
|
||||
|
||||
File file = new File(modeFolder, data.getPlayerName() + ".ghost");
|
||||
try {
|
||||
data.saveToFile(file);
|
||||
} catch (IOException e) {
|
||||
LoParkour.logging().error("Failed to save ghost: " + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
public void spawnGhosts(@NotNull String mode, @NotNull Location startLocation, @NotNull World world) {
|
||||
if (!Config.CONFIG.getBoolean("ghost-mode.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopAllGhosts();
|
||||
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int showTop = Math.min(Config.CONFIG.getInt("ghost-mode.show-top"), ghosts.size());
|
||||
for (int i = 0; i < showTop; i++) {
|
||||
GhostPlayer ghost = new GhostPlayer(ghosts.get(i), world);
|
||||
ghost.spawn(startLocation);
|
||||
activeGhosts.add(ghost);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopAllGhosts() {
|
||||
for (GhostPlayer ghost : activeGhosts) {
|
||||
ghost.stop();
|
||||
}
|
||||
activeGhosts.clear();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public GhostData getTopGhost(@NotNull String mode) {
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return ghosts.get(0);
|
||||
}
|
||||
|
||||
public List<GhostData> getTopGhosts(@NotNull String mode, int count) {
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return ghosts.subList(0, Math.min(count, ghosts.size()));
|
||||
}
|
||||
|
||||
public boolean shouldRecordGhost(@NotNull String mode, int score) {
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.size() < MAX_GHOSTS_PER_MODE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GhostData lowestGhost = ghosts.get(ghosts.size() - 1);
|
||||
return score > lowestGhost.getScore();
|
||||
}
|
||||
}
|
||||
95
src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java
Normal file
95
src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.ArmorStand;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class GhostPlayer {
|
||||
|
||||
private final GhostData data;
|
||||
private final World world;
|
||||
private ArmorStand entity;
|
||||
private BukkitTask task;
|
||||
private int currentFrame = 0;
|
||||
private long startTime;
|
||||
|
||||
public GhostPlayer(@NotNull GhostData data, @NotNull World world) {
|
||||
this.data = data;
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public void spawn(@NotNull Location startLocation) {
|
||||
if (data.getFrames().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity = (ArmorStand) world.spawnEntity(startLocation, EntityType.ARMOR_STAND);
|
||||
entity.setVisible(false);
|
||||
entity.setGravity(false);
|
||||
entity.setInvulnerable(true);
|
||||
entity.setCustomName("§7" + data.getPlayerName() + " §8(Ghost)");
|
||||
entity.setCustomNameVisible(true);
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
currentFrame = 0;
|
||||
|
||||
task = Task.create(LoParkour.getPlugin())
|
||||
.repeat(1)
|
||||
.execute(this::update)
|
||||
.run();
|
||||
}
|
||||
|
||||
private void update() {
|
||||
if (entity == null || !entity.isValid()) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
|
||||
while (currentFrame < data.getFrames().size()) {
|
||||
GhostFrame frame = data.getFrames().get(currentFrame);
|
||||
|
||||
if (frame.getTimestamp() > elapsed) {
|
||||
break;
|
||||
}
|
||||
|
||||
Location loc = frame.toLocation(world);
|
||||
entity.teleport(loc);
|
||||
currentFrame++;
|
||||
}
|
||||
|
||||
if (currentFrame >= data.getFrames().size()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
|
||||
if (entity != null) {
|
||||
entity.remove();
|
||||
entity = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return entity != null && entity.isValid();
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return data.getPlayerName();
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return data.getScore();
|
||||
}
|
||||
}
|
||||
53
src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java
Normal file
53
src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GhostRecorder {
|
||||
|
||||
private static final double MIN_MOVEMENT = 0.1;
|
||||
|
||||
private final List<GhostFrame> frames = new ArrayList<>();
|
||||
private Location lastLocation;
|
||||
private long startTime;
|
||||
private boolean recording = false;
|
||||
|
||||
public void startRecording(@NotNull Location startLocation) {
|
||||
frames.clear();
|
||||
lastLocation = startLocation.clone();
|
||||
startTime = System.currentTimeMillis();
|
||||
recording = true;
|
||||
|
||||
frames.add(new GhostFrame(0, startLocation.clone()));
|
||||
}
|
||||
|
||||
public void recordFrame(@NotNull Location currentLocation) {
|
||||
if (!recording) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastLocation.distance(currentLocation) < MIN_MOVEMENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
long timestamp = System.currentTimeMillis() - startTime;
|
||||
frames.add(new GhostFrame(timestamp, currentLocation.clone()));
|
||||
lastLocation = currentLocation.clone();
|
||||
}
|
||||
|
||||
public GhostData stopRecording(String playerName, int score) {
|
||||
recording = false;
|
||||
return new GhostData(playerName, score, frames);
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return recording;
|
||||
}
|
||||
|
||||
public int getFrameCount() {
|
||||
return frames.size();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue