feat: personal run replays (ghost system)
- GhostData: worldName field, format v4; GhostManager stores last N runs per player (ghosts/<uuid>/<ts>.ghost)
- GhostService: per-player recording (start on run start, every tick, finalize on fall), gated by ghost-mode.enabled + per-player toggle
- PlayerGhost: packet-based fake-player NPC with viewer skin (NMS via remapNms gradle task, reflection send, graceful degradation)
- GhostReplay: teleport viewer to run start, replay frames via PlayerGhost, restore on finish
- /lp replay [index] command + tab completion; settings menu ghost_recording toggle (persisted disk/SQL)
- config: ghost-mode.{enabled,max-runs}, remove dead show-top/transparency + GHOST_* statics
- tests: GhostData v4 round-trip, GhostManager eviction/ordering, yawToFixed (79 total)
This commit is contained in:
parent
4c255111f0
commit
8c8046a4f5
30 changed files with 1027 additions and 227 deletions
|
|
@ -33,6 +33,7 @@ public enum ParkourOption {
|
|||
FALL_MESSAGE("settings.parkour_settings.items.fall_message", "LoParkour.settings.fall_message"),
|
||||
PARTICLES("settings.parkour_settings.items.particles", "LoParkour.settings.particles"),
|
||||
SOUND("settings.parkour_settings.items.sound", "LoParkour.settings.sound"),
|
||||
GHOST_RECORDING("settings.parkour_settings.items.ghost_recording", "LoParkour.settings.ghost_recording"),
|
||||
SPECIAL_BLOCKS("settings.parkour_settings.items.special_blocks", "LoParkour.settings.special_blocks"),
|
||||
|
||||
LANG("settings.lang", "LoParkour.settings.lang"),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
private static final int SLOT_SPECIAL_BLOCKS = 12;
|
||||
private static final int SLOT_FALL_MESSAGE = 13;
|
||||
private static final int SLOT_SCOREBOARD = 14;
|
||||
private static final int SLOT_GHOST_RECORDING = 15;
|
||||
private static final int SLOT_STYLE = 19;
|
||||
private static final int SLOT_SCHEM_DIFF = 20;
|
||||
private static final int SLOT_CLOSE = 31;
|
||||
|
|
@ -55,47 +56,54 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
final String title = Locales.getString(locale, "settings.name");
|
||||
|
||||
baseGui(title, ROWS_MAIN)
|
||||
.setItem(SLOT_PARTICLES, toggleItem(locale, "particles", pp.particles), e -> {
|
||||
pp.particles = !pp.particles;
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
open(pp);
|
||||
})
|
||||
.setItem(SLOT_SOUND, toggleItem(locale, "sound", pp.sound), e -> {
|
||||
pp.sound = !pp.sound;
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
open(pp);
|
||||
})
|
||||
.setItem(SLOT_SPECIAL_BLOCKS, toggleItem(locale, "special_blocks", pp.useSpecialBlocks), e -> {
|
||||
if (pp.session.generator.state.score == 0) {
|
||||
pp.useSpecialBlocks = !pp.useSpecialBlocks;
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
}
|
||||
open(pp);
|
||||
})
|
||||
.setItem(SLOT_FALL_MESSAGE, toggleItem(locale, "fall_message", pp.showFallMessage), e -> {
|
||||
pp.showFallMessage = !pp.showFallMessage;
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
open(pp);
|
||||
})
|
||||
.setItem(SLOT_SCOREBOARD, toggleItem(locale, "scoreboard", pp.showScoreboard), e -> {
|
||||
pp.showScoreboard = !pp.showScoreboard;
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
open(pp);
|
||||
})
|
||||
.setItem(SLOT_PARTICLES, toggleItem(locale, "particles", pp.particles),
|
||||
e -> toggleSetting(pp, () -> pp.particles = !pp.particles))
|
||||
.setItem(SLOT_SOUND, toggleItem(locale, "sound", pp.sound),
|
||||
e -> toggleSetting(pp, () -> pp.sound = !pp.sound))
|
||||
.setItem(SLOT_SPECIAL_BLOCKS, toggleItem(locale, "special_blocks", pp.useSpecialBlocks),
|
||||
e -> toggleSpecialBlocks(pp))
|
||||
.setItem(SLOT_FALL_MESSAGE, toggleItem(locale, "fall_message", pp.showFallMessage),
|
||||
e -> toggleSetting(pp, () -> pp.showFallMessage = !pp.showFallMessage))
|
||||
.setItem(SLOT_SCOREBOARD, toggleItem(locale, "scoreboard", pp.showScoreboard),
|
||||
e -> toggleSetting(pp, () -> pp.showScoreboard = !pp.showScoreboard))
|
||||
.setItem(SLOT_GHOST_RECORDING, toggleItem(locale, "ghost_recording",
|
||||
Boolean.TRUE.equals(pp.ghostRecording)), e -> toggleGhostRecording(pp))
|
||||
.setItem(SLOT_STYLE, styleItem(pp), e -> openStyleMenu(pp))
|
||||
.setItem(SLOT_SCHEM_DIFF, schemDiffItem(pp), e -> {
|
||||
if (pp.session.generator.state.score == 0) {
|
||||
final List<Double> diffs = List.of(0.0, DIFF_QUARTER, DIFF_HALF, DIFF_THREE_QUARTER, 1.0);
|
||||
final int idx = diffs.indexOf(pp.schematicDifficulty);
|
||||
pp.schematicDifficulty = diffs.get((idx + 1) % diffs.size());
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
}
|
||||
open(pp);
|
||||
})
|
||||
.setItem(SLOT_SCHEM_DIFF, schemDiffItem(pp), e -> cycleSchematicDifficulty(pp))
|
||||
.setItem(SLOT_CLOSE, closeButton(player), e -> Menus.SETTINGS.open(player))
|
||||
.open(player);
|
||||
}
|
||||
|
||||
private void toggleSetting(ParkourPlayer pp, Runnable update) {
|
||||
update.run();
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
open(pp);
|
||||
}
|
||||
|
||||
private void toggleSpecialBlocks(ParkourPlayer pp) {
|
||||
if (pp.session.generator.state.score == 0) {
|
||||
pp.useSpecialBlocks = !pp.useSpecialBlocks;
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
}
|
||||
open(pp);
|
||||
}
|
||||
|
||||
private void cycleSchematicDifficulty(ParkourPlayer pp) {
|
||||
if (pp.session.generator.state.score == 0) {
|
||||
final List<Double> diffs = List.of(0.0, DIFF_QUARTER, DIFF_HALF, DIFF_THREE_QUARTER, 1.0);
|
||||
final int idx = diffs.indexOf(pp.schematicDifficulty);
|
||||
pp.schematicDifficulty = diffs.get((idx + 1) % diffs.size());
|
||||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
}
|
||||
open(pp);
|
||||
}
|
||||
|
||||
private void toggleGhostRecording(ParkourPlayer pp) {
|
||||
pp.ghostRecording = !Boolean.TRUE.equals(pp.ghostRecording);
|
||||
pp.save(false);
|
||||
open(pp);
|
||||
}
|
||||
|
||||
private ItemStack toggleItem(String locale, String key, boolean value) {
|
||||
final Material mat = value ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE;
|
||||
final String symbol = Locales.getString(locale, "settings.parkour_settings." + (value ? "enabled" : "disabled"));
|
||||
|
|
@ -106,13 +114,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
final ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color(name + " §7[" + symbol + "§7]"));
|
||||
if (lore != null && !lore.isEmpty()) {
|
||||
final List<String> loreLines = new ArrayList<>();
|
||||
for (final String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", symbol)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
applyLore(meta, lore, symbol);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
@ -131,13 +133,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
final String name = Locales.getString(pp.locale, "settings.parkour_settings.items.schematics.name");
|
||||
final String lore = Locales.getString(pp.locale, "settings.parkour_settings.items.schematics.lore");
|
||||
meta.setDisplayName(ColorUtil.color(name.replace("%s", label)));
|
||||
if (lore != null && !lore.isEmpty()) {
|
||||
final List<String> loreLines = new ArrayList<>();
|
||||
for (final String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", label)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
applyLore(meta, lore, label);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
@ -150,18 +146,23 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
final ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color(name.replace("%s", pp.style)));
|
||||
if (lore != null && !lore.isEmpty()) {
|
||||
final List<String> loreLines = new ArrayList<>();
|
||||
for (final String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", pp.style)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
applyLore(meta, lore, pp.style);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private void applyLore(ItemMeta meta, String lore, String replacement) {
|
||||
if (lore == null || lore.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final List<String> loreLines = new ArrayList<>();
|
||||
for (final String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", replacement)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
|
||||
private void openStyleMenu(ParkourPlayer pp) {
|
||||
final Player player = pp.player;
|
||||
final String locale = pp.locale;
|
||||
|
|
@ -179,7 +180,6 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
if (Config.CONFIG.getBoolean("permissions.per-style") && !player.hasPermission(perm)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final ItemStack item = new ItemStack(style.getNext());
|
||||
final ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue