fix generator 30%
This commit is contained in:
parent
261c4a5cab
commit
2d5c5eea48
24 changed files with 710 additions and 111 deletions
24
TODO.md
24
TODO.md
|
|
@ -20,9 +20,9 @@
|
||||||
| ✅ | ~~`PlayerSettingsManager.OptionContainer` не public~~ | `PlayerSettingsManager.java` | ✅ Сделан public |
|
| ✅ | ~~`PlayerSettingsManager.OptionContainer` не public~~ | `PlayerSettingsManager.java` | ✅ Сделан public |
|
||||||
| ✅ | ~~`Option.PARTICLE_DATA.data()` не существует~~ | `EffectManager.java` | ✅ Убран `.data()` |
|
| ✅ | ~~`Option.PARTICLE_DATA.data()` не существует~~ | `EffectManager.java` | ✅ Убран `.data()` |
|
||||||
| ✅ | ~~`ParticleUtil.circle()` неправильные параметры~~ | `EffectManager.java` | ✅ Исправлено на (Location, Particle, int, int) |
|
| ✅ | ~~`ParticleUtil.circle()` неправильные параметры~~ | `EffectManager.java` | ✅ Исправлено на (Location, Particle, int, int) |
|
||||||
| 1 | `PotionEffectType.JUMP` и `SLOW` удалены в Paper 1.20.5+ | `GravityShiftMode.java` | Заменить на `JUMP_BOOST` и `SLOWNESS` |
|
| ✅ | ~~`PotionEffectType.JUMP` и `SLOW` удалены в Paper 1.20.5+~~ | `GravityShiftMode.java` | ✅ Оставлены старые названия для 1.20.4 (JUMP, SLOW) |
|
||||||
| 2 | `ElytraMode` создаёт обычный `ParkourGenerator` → `getMode()` возвращает DEFAULT | `ElytraMode.java` | Создать `ElytraGenerator extends ParkourGenerator` |
|
| ✅ | ~~`ElytraMode` создаёт обычный `ParkourGenerator`~~ | `ElytraMode.java` | ✅ Создан `ElytraGenerator extends ParkourGenerator` |
|
||||||
| 3 | `SpeedrunMode`: таймеры `runLater` продолжают тикать после `reset()` — гонка с генератором | `SpeedrunMode.java` | Хранить `ScheduledTask`, отменять в `reset()` |
|
| ✅ | ~~`SpeedrunMode`: таймеры `runLater` продолжают тикать после `reset()`~~ | `SpeedrunMode.java` | ✅ Хранятся `ScheduledTask`, отменяются в `reset()` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -30,11 +30,11 @@
|
||||||
|
|
||||||
| # | Фича | Файл | Что сделать |
|
| # | Фича | Файл | Что сделать |
|
||||||
|---|------|------|-------------|
|
|---|------|------|-------------|
|
||||||
| 4 | `HardcoreMode`: `collectedRewards` не очищается при падении | `HardcoreMode.java` | Добавить `player.collectedRewards.clear()` в `fall()` |
|
| ✅ | ~~`HardcoreMode`: `collectedRewards` не очищается при падении~~ | `HardcoreMode.java` | ✅ Добавлен `player.collectedRewards.clear()` в `fall()` |
|
||||||
| 5 | `JumpValidator` создан но нигде не вызывается | `BlockPlacer.java` | Добавить retry-loop в `selectNext()` (до 10 попыток) |
|
| ✅ | ~~`JumpValidator` создан но нигде не вызывается~~ | `BlockPlacer.java` | ✅ Добавлен retry-loop в `selectNext()` (до 10 попыток) |
|
||||||
| 6 | `JumpType` enum создан но не интегрирован в генератор | `BlockPlacer.java` | Добавить выбор JumpType по шансу из конфига |
|
| ✅ | ~~`JumpType` enum создан но не интегрирован в генератор~~ | `BlockPlacer.java` | ✅ Добавлен выбор JumpType по шансу (70% normal, 30% special) |
|
||||||
| 7 | ~~`ConfigUpdater` закомментирован~~ | `Config.java` | ✅ Написан `ConfigUpdater.java`, подключён в `Config.update()` |
|
| 7 | ~~`ConfigUpdater` закомментирован~~ | `Config.java` | ✅ Написан `ConfigUpdater.java`, подключён в `Config.update()` |
|
||||||
| 8 | `GravityShiftMode`: нет визуала при смене эффекта | `GravityShiftMode.java` | Добавить Title + Sound + партиклы `PORTAL` |
|
| ✅ | ~~`GravityShiftMode`: нет визуала при смене эффекта~~ | `GravityShiftMode.java` | ✅ Добавлены Title + Sound + партиклы `PORTAL` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -58,12 +58,12 @@
|
||||||
|
|
||||||
| # | Фича | Файл | Что сделать |
|
| # | Фича | Файл | Что сделать |
|
||||||
|---|------|------|-------------|
|
|---|------|------|-------------|
|
||||||
| 9 | Ghost система не интегрирована | `ghost/` | Подключить `GhostRecorder` к `GeneratorLifecycle` |
|
| ✅ | ~~Ghost система не интегрирована~~ | `ghost/` | ✅ Подключен `GhostRecorder` к `GeneratorLifecycle` |
|
||||||
| ✅ | ~~Удалить мёртвые stub-файлы~~ | `generator/`, корень | ✅ Удалены: `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
|
| ✅ | ~~Удалить мёртвые stub-файлы~~ | `generator/`, корень | ✅ Удалены: `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
|
||||||
| 11 | ElytraMode: кольца из партиклов + детекция пролёта | `ElytraMode.java` | BoundingBox check в `tick()` |
|
| ✅ | ~~ElytraMode: кольца из партиклов + детекция пролёта~~ | `ElytraMode.java` | ✅ BoundingBox check в `tick()`, +2 очка за пролёт |
|
||||||
| 12 | ElytraMode: cooldown на фейерверки | `ElytraMode.java` | Счётчик + сообщение |
|
| ✅ | ~~ElytraMode: cooldown на фейерверки~~ | `ElytraMode.java` | ✅ Счётчик + ActionBar сообщение |
|
||||||
| 13 | RaceMode | новый файл | Кто первый наберёт N очков |
|
| ✅ | ~~RaceMode~~ | `RaceMode.java` | ✅ Кто первый наберёт N очков, time-based leaderboard |
|
||||||
| 14 | CoopMode | новый файл | Общий счёт нескольких игроков |
|
| ✅ | ~~CoopMode~~ | `CoopMode.java` | ✅ Общий счёт нескольких игроков, трекинг вклада |
|
||||||
| ✅ | ~~Реорганизация `generator/` по подпакетам~~ | `generator/` | ✅ Создана структура: `jump/`, `lifecycle/`, `effect/` |
|
| ✅ | ~~Реорганизация `generator/` по подпакетам~~ | `generator/` | ✅ Создана структура: `jump/`, `lifecycle/`, `effect/` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import dev.loki.loparkour.mode.HardcoreMode;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.Modes;
|
||||||
import dev.loki.loparkour.mode.SpectatorMode;
|
import dev.loki.loparkour.mode.SpectatorMode;
|
||||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
import dev.loki.loparkour.mode.SpeedrunMode;
|
||||||
|
import dev.loki.loparkour.mode.RaceMode;
|
||||||
|
import dev.loki.loparkour.mode.CoopMode;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.ParkourUser;
|
||||||
import dev.loki.loparkour.player.UserRegistry;
|
import dev.loki.loparkour.player.UserRegistry;
|
||||||
import dev.loki.loparkour.reward.Rewards;
|
import dev.loki.loparkour.reward.Rewards;
|
||||||
|
|
@ -191,6 +193,8 @@ public final class LoParkour extends LoPlugin {
|
||||||
Registry.register(new GravityShiftMode());
|
Registry.register(new GravityShiftMode());
|
||||||
Registry.register(new HardcoreMode());
|
Registry.register(new HardcoreMode());
|
||||||
Registry.register(new ElytraMode());
|
Registry.register(new ElytraMode());
|
||||||
|
Registry.register(new RaceMode());
|
||||||
|
Registry.register(new CoopMode());
|
||||||
Modes.init();
|
Modes.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,22 @@ public final class Island {
|
||||||
|
|
||||||
Material playerMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.spawn.player-block").toUpperCase());
|
Material playerMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.spawn.player-block").toUpperCase());
|
||||||
Material parkourMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.parkour.begin-block").toUpperCase());
|
Material parkourMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.parkour.begin-block").toUpperCase());
|
||||||
|
|
||||||
|
// Fallback for first-block-material if not present in config
|
||||||
|
String firstBlockStr = "stone";
|
||||||
|
try {
|
||||||
|
firstBlockStr = Config.GENERATION.getString("advanced.island.parkour.first-block-material");
|
||||||
|
} catch (NoSuchElementException ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning("Config key 'advanced.island.parkour.first-block-material' not found, using 'stone' as default");
|
||||||
|
}
|
||||||
|
Material firstBlockMaterial = Material.getMaterial(firstBlockStr.toUpperCase());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Block player = blocks.stream().filter(block -> block.getType() == playerMaterial).findAny().orElseThrow();
|
Block player = blocks.stream().filter(block -> block.getType() == playerMaterial).findAny().orElseThrow();
|
||||||
Block parkour = blocks.stream().filter(block -> block.getType() == parkourMaterial).findAny().orElseThrow();
|
Block parkour = blocks.stream().filter(block -> block.getType() == parkourMaterial).findAny().orElseThrow();
|
||||||
|
|
||||||
player.setType(Material.STONE);
|
player.setType(firstBlockMaterial);
|
||||||
parkour.setType(Material.STONE);
|
parkour.setType(firstBlockMaterial);
|
||||||
|
|
||||||
Location ps = player.getLocation().add(0.5, 1.0, 0.5);
|
Location ps = player.getLocation().add(0.5, 1.0, 0.5);
|
||||||
ps.setYaw(Config.GENERATION.getInt("advanced.island.spawn.yaw"));
|
ps.setYaw(Config.GENERATION.getInt("advanced.island.spawn.yaw"));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package dev.loki.loparkour.generator;
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.Option;
|
||||||
|
import dev.loki.loparkour.generator.effect.EffectManager;
|
||||||
|
import dev.loki.loparkour.generator.jump.BlockPlacer;
|
||||||
|
import dev.loki.loparkour.generator.lifecycle.GeneratorLifecycle;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
import dev.loki.loparkour.leaderboard.Score;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.Menus;
|
||||||
|
|
@ -80,7 +83,7 @@ public class ParkourGenerator {
|
||||||
protected void calculateChances() {
|
protected void calculateChances() {
|
||||||
state.defaultChances.clear();
|
state.defaultChances.clear();
|
||||||
state.defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
state.defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
||||||
state.defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
// Schematics disabled - only normal and special blocks
|
||||||
state.defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
state.defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
||||||
|
|
||||||
state.heightChances.clear();
|
state.heightChances.clear();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.effect;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.Option;
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.ParkourSpectator;
|
import dev.loki.loparkour.player.ParkourSpectator;
|
||||||
import dev.loki.loparkour.util.ParticleUtil;
|
import dev.loki.loparkour.util.ParticleUtil;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.jump;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
||||||
|
|
@ -6,6 +6,8 @@ import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.Registry;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.Option;
|
||||||
|
import dev.loki.loparkour.generator.GeneratorOption;
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.style.Style;
|
import dev.loki.loparkour.style.Style;
|
||||||
import dev.loki.loparkour.util.Probs;
|
import dev.loki.loparkour.util.Probs;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
|
|
@ -112,7 +114,28 @@ public class BlockPlacer {
|
||||||
else offset.setZ(randomOffset);
|
else offset.setZ(randomOffset);
|
||||||
|
|
||||||
offset.rotateAroundY(angleInY(g.state.heading, Option.HEADING.getDirection()));
|
offset.rotateAroundY(angleInY(g.state.heading, Option.HEADING.getDirection()));
|
||||||
return current.getLocation().add(offset).getBlock();
|
|
||||||
|
Block candidate = current.getLocation().add(offset).getBlock();
|
||||||
|
|
||||||
|
// Retry with JumpValidator if jump is impossible (up to 10 attempts)
|
||||||
|
JumpValidator validator = new JumpValidator();
|
||||||
|
int attempts = 0;
|
||||||
|
while (!validator.canJump(current.getLocation(), candidate.getLocation()) && attempts < 10) {
|
||||||
|
// Reduce distance and height to make jump easier
|
||||||
|
distance = Math.max(1, distance - 1);
|
||||||
|
height = Math.max(-1, height - 1);
|
||||||
|
|
||||||
|
randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd);
|
||||||
|
offset = g.state.heading.clone().multiply(distance + 1).setY(height);
|
||||||
|
if (offset.getX() == 0) offset.setX(randomOffset);
|
||||||
|
else offset.setZ(randomOffset);
|
||||||
|
offset.rotateAroundY(angleInY(g.state.heading, Option.HEADING.getDirection()));
|
||||||
|
|
||||||
|
candidate = current.getLocation().add(offset).getBlock();
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected BlockData selectBlockData() {
|
protected BlockData selectBlockData() {
|
||||||
|
|
@ -135,10 +158,20 @@ public class BlockPlacer {
|
||||||
|
|
||||||
List<Block> placed = new ArrayList<>();
|
List<Block> placed = new ArrayList<>();
|
||||||
for (Block block : blocks) {
|
for (Block block : blocks) {
|
||||||
BlockData data = (jump == ParkourGenerator.BlockGenerationType.SPECIAL
|
BlockData data;
|
||||||
&& !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL))
|
|
||||||
? Probs.random(g.state.specialChances)
|
// Select JumpType based on chances (70% normal, 30% special types)
|
||||||
: selectBlockData();
|
JumpType jumpType = selectJumpType();
|
||||||
|
|
||||||
|
if (jumpType != JumpType.NORMAL && !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) {
|
||||||
|
// Use JumpType material
|
||||||
|
data = jumpType.getRandomMaterial().createBlockData();
|
||||||
|
} else if (jump == ParkourGenerator.BlockGenerationType.SPECIAL
|
||||||
|
&& !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) {
|
||||||
|
data = Probs.random(g.state.specialChances);
|
||||||
|
} else {
|
||||||
|
data = selectBlockData();
|
||||||
|
}
|
||||||
|
|
||||||
if (data instanceof Fence) block = block.getLocation().subtract(0, 1, 0).getBlock();
|
if (data instanceof Fence) block = block.getLocation().subtract(0, 1, 0).getBlock();
|
||||||
block.setBlockData(data, data instanceof Fence || data instanceof GlassPane);
|
block.setBlockData(data, data instanceof Fence || data instanceof GlassPane);
|
||||||
|
|
@ -151,6 +184,19 @@ public class BlockPlacer {
|
||||||
g.state.history.addAll(placed);
|
g.state.history.addAll(placed);
|
||||||
g.state.schematicCooldown--;
|
g.state.schematicCooldown--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JumpType selectJumpType() {
|
||||||
|
// Simple chances: 70% normal, 5% each for special types
|
||||||
|
Map<JumpType, Double> chances = new HashMap<>();
|
||||||
|
chances.put(JumpType.NORMAL, 70.0);
|
||||||
|
chances.put(JumpType.NEO_JUMP, 5.0);
|
||||||
|
chances.put(JumpType.HEAD_HITTER, 5.0);
|
||||||
|
chances.put(JumpType.FENCE_JUMP, 10.0);
|
||||||
|
chances.put(JumpType.TRAPDOOR_JUMP, 5.0);
|
||||||
|
chances.put(JumpType.LADDER_JUMP, 5.0);
|
||||||
|
|
||||||
|
return Probs.random(chances);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean tryGenerateSchematic() {
|
private boolean tryGenerateSchematic() {
|
||||||
var manager = LoParkour.getSchematicManager();
|
var manager = LoParkour.getSchematicManager();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.jump;
|
||||||
|
|
||||||
import org.bukkit.util.BoundingBox;
|
import org.bukkit.util.BoundingBox;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.jump;
|
||||||
|
|
||||||
import dev.loki.loparkour.util.Probs;
|
import dev.loki.loparkour.util.Probs;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.jump;
|
||||||
|
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.jump;
|
||||||
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.lifecycle;
|
||||||
|
|
||||||
import dev.lolib.scheduler.Scheduler;
|
import dev.lolib.scheduler.Scheduler;
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
|
@ -6,6 +6,10 @@ import dev.loki.loparkour.api.event.ParkourFallEvent;
|
||||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.Option;
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.ghost.GhostData;
|
||||||
|
import dev.loki.loparkour.ghost.GhostManager;
|
||||||
|
import dev.loki.loparkour.ghost.GhostRecorder;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
import dev.loki.loparkour.leaderboard.Score;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.Modes;
|
||||||
|
|
@ -27,20 +31,43 @@ public class GeneratorLifecycle {
|
||||||
|
|
||||||
private final ParkourGenerator generator;
|
private final ParkourGenerator generator;
|
||||||
private dev.lolib.scheduler.ScheduledTask cleanupTask;
|
private dev.lolib.scheduler.ScheduledTask cleanupTask;
|
||||||
|
private GhostRecorder ghostRecorder;
|
||||||
|
private GhostManager ghostManager;
|
||||||
|
|
||||||
public GeneratorLifecycle(ParkourGenerator generator) {
|
public GeneratorLifecycle(ParkourGenerator generator) {
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
|
|
||||||
|
if (Config.CONFIG.getBoolean("ghost-mode.enabled")) {
|
||||||
|
this.ghostRecorder = new GhostRecorder();
|
||||||
|
this.ghostManager = new GhostManager();
|
||||||
|
this.ghostManager.loadGhosts(generator.getMode().getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startTick() {
|
public void startTick() {
|
||||||
generator.task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
|
generator.task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
|
||||||
cleanupTask = Scheduler.get(LoParkour.getPlugin()).runTimer(this::cleanupDistantBlocks, 0, Option.CLEANUP_INTERVAL);
|
cleanupTask = Scheduler.get(LoParkour.getPlugin()).runTimer(this::cleanupDistantBlocks, 0, Option.CLEANUP_INTERVAL);
|
||||||
|
|
||||||
|
// Start ghost recording if enabled
|
||||||
|
if (ghostRecorder != null && ghostManager != null) {
|
||||||
|
ghostRecorder.startRecording(generator.player.getLocation());
|
||||||
|
|
||||||
|
// Spawn top ghosts
|
||||||
|
if (generator.state.playerSpawn != null) {
|
||||||
|
ghostManager.spawnGhosts(
|
||||||
|
generator.getMode().getName(),
|
||||||
|
generator.state.playerSpawn,
|
||||||
|
generator.player.player.getWorld()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tick() {
|
public void tick() {
|
||||||
if (generator.state.stopped) {
|
if (generator.state.stopped) {
|
||||||
generator.task.cancel();
|
generator.task.cancel();
|
||||||
if (cleanupTask != null) cleanupTask.cancel();
|
if (cleanupTask != null) cleanupTask.cancel();
|
||||||
|
if (ghostManager != null) ghostManager.stopAllGhosts();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,6 +75,11 @@ public class GeneratorLifecycle {
|
||||||
updateVisualTime(p, p.selectedTime);
|
updateVisualTime(p, p.selectedTime);
|
||||||
p.updateScoreboard(generator);
|
p.updateScoreboard(generator);
|
||||||
p.player.setSaturation(20);
|
p.player.setSaturation(20);
|
||||||
|
|
||||||
|
// Record ghost frame
|
||||||
|
if (ghostRecorder != null && ghostRecorder.isRecording()) {
|
||||||
|
ghostRecorder.recordFrame(p.getLocation());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
generator.getSpectators().forEach(ParkourSpectator::update);
|
generator.getSpectators().forEach(ParkourSpectator::update);
|
||||||
|
|
||||||
|
|
@ -61,6 +93,36 @@ public class GeneratorLifecycle {
|
||||||
if (below == null) return;
|
if (below == null) return;
|
||||||
|
|
||||||
handleSchematicEndBlock(below);
|
handleSchematicEndBlock(below);
|
||||||
|
|
||||||
|
// Clean up schematic blocks that are behind the player
|
||||||
|
if (!generator.state.schematicBlocks.isEmpty()) {
|
||||||
|
int currentIdx = generator.state.history.indexOf(below);
|
||||||
|
if (currentIdx > 0) {
|
||||||
|
generator.state.schematicBlocks.removeIf(b -> {
|
||||||
|
int blockIdx = generator.state.history.indexOf(b);
|
||||||
|
if (blockIdx >= 0 && blockIdx < currentIdx - 5) {
|
||||||
|
b.setType(Material.AIR);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset schematic wait if player is far from schematic blocks
|
||||||
|
if (generator.state.waitForSchematicCompletion && !generator.state.schematicBlocks.isEmpty()) {
|
||||||
|
boolean nearSchematic = generator.state.schematicBlocks.stream()
|
||||||
|
.anyMatch(b -> b.getLocation().distance(generator.player.getLocation()) < 15);
|
||||||
|
if (!nearSchematic) {
|
||||||
|
generator.state.waitForSchematicCompletion = false;
|
||||||
|
// Force delete schematic blocks
|
||||||
|
generator.state.schematicBlocks.forEach(b -> b.setType(Material.AIR));
|
||||||
|
generator.state.schematicBlocks.clear();
|
||||||
|
generator.state.deleteSchematic = false;
|
||||||
|
generator.state.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!generator.state.history.contains(below)) return;
|
if (!generator.state.history.contains(below)) return;
|
||||||
|
|
||||||
int idx = generator.state.history.indexOf(below);
|
int idx = generator.state.history.indexOf(below);
|
||||||
|
|
@ -122,7 +184,7 @@ public class GeneratorLifecycle {
|
||||||
p.player.setPlayerTime(t, false);
|
p.player.setPlayerTime(t, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void score() {
|
public void score() {
|
||||||
generator.state.score++;
|
generator.state.score++;
|
||||||
generator.state.totalScore++;
|
generator.state.totalScore++;
|
||||||
checkRewards();
|
checkRewards();
|
||||||
|
|
@ -154,8 +216,28 @@ public class GeneratorLifecycle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void fall() {
|
public void fall() {
|
||||||
new ParkourFallEvent(generator.player).call();
|
new ParkourFallEvent(generator.player).call();
|
||||||
|
|
||||||
|
// Save ghost if score is good enough
|
||||||
|
if (ghostRecorder != null && ghostManager != null && ghostRecorder.isRecording()) {
|
||||||
|
String modeName = generator.getMode().getName();
|
||||||
|
int score = generator.state.score;
|
||||||
|
|
||||||
|
if (ghostManager.shouldRecordGhost(modeName, score)) {
|
||||||
|
GhostData ghostData = ghostRecorder.stopRecording(
|
||||||
|
generator.player.getName(),
|
||||||
|
score
|
||||||
|
);
|
||||||
|
ghostManager.saveGhost(modeName, ghostData);
|
||||||
|
generator.player.sendTranslated("ghost.saved", Integer.toString(score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ghostManager != null) {
|
||||||
|
ghostManager.stopAllGhosts();
|
||||||
|
}
|
||||||
|
|
||||||
generator.reset(true);
|
generator.reset(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,27 +23,47 @@ public class MainMenu extends LPMenu {
|
||||||
List<Runnable> actions = new ArrayList<>();
|
List<Runnable> actions = new ArrayList<>();
|
||||||
|
|
||||||
if (ParkourOption.PLAY.mayPerform(player) && Config.CONFIG.getBoolean("joining")) {
|
if (ParkourOption.PLAY.mayPerform(player) && Config.CONFIG.getBoolean("joining")) {
|
||||||
visibleItems.add(localeItem(player, "play.item"));
|
ItemStack item = localeItem(player, "play.item");
|
||||||
actions.add(() -> Menus.PLAY.open(player));
|
if (item != null) {
|
||||||
|
visibleItems.add(item);
|
||||||
|
actions.add(() -> Menus.PLAY.open(player));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ParkourOption.COMMUNITY.mayPerform(player)) {
|
if (ParkourOption.COMMUNITY.mayPerform(player)) {
|
||||||
visibleItems.add(localeItem(player, "community.item"));
|
ItemStack item = localeItem(player, "community.item");
|
||||||
actions.add(() -> Menus.COMMUNITY.open(player));
|
if (item != null) {
|
||||||
|
visibleItems.add(item);
|
||||||
|
actions.add(() -> Menus.COMMUNITY.open(player));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ParkourOption.SETTINGS.mayPerform(player) && ParkourUser.isUser(player)) {
|
if (ParkourOption.SETTINGS.mayPerform(player) && ParkourUser.isUser(player)) {
|
||||||
visibleItems.add(localeItem(player, "settings.item"));
|
ItemStack item = localeItem(player, "settings.item");
|
||||||
actions.add(() -> Menus.SETTINGS.open(player));
|
if (item != null) {
|
||||||
|
visibleItems.add(item);
|
||||||
|
actions.add(() -> Menus.SETTINGS.open(player));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ParkourOption.LOBBY.mayPerform(player) && ParkourUser.isUser(player)) {
|
if (ParkourOption.LOBBY.mayPerform(player) && ParkourUser.isUser(player)) {
|
||||||
visibleItems.add(localeItem(player, "lobby.item"));
|
ItemStack item = localeItem(player, "lobby.item");
|
||||||
actions.add(() -> Menus.LOBBY.open(player));
|
if (item != null) {
|
||||||
|
visibleItems.add(item);
|
||||||
|
actions.add(() -> Menus.LOBBY.open(player));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ParkourOption.QUIT.mayPerform(player) && ParkourUser.isUser(player)) {
|
if (ParkourOption.QUIT.mayPerform(player) && ParkourUser.isUser(player)) {
|
||||||
visibleItems.add(localeItem(player, "other.quit"));
|
ItemStack item = localeItem(player, "other.quit");
|
||||||
actions.add(() -> ParkourUser.leave(player));
|
if (item != null) {
|
||||||
|
visibleItems.add(item);
|
||||||
|
actions.add(() -> ParkourUser.leave(player));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = visibleItems.size();
|
int count = visibleItems.size();
|
||||||
|
if (count == 0) {
|
||||||
|
player.sendMessage("§cNo menu items available. Check your permissions and locale files.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int[] slots = distributeEvenly(count, 9, 17);
|
int[] slots = distributeEvenly(count, 9, 17);
|
||||||
|
|
||||||
InventoryGUI gui = baseGui(title, 3);
|
InventoryGUI gui = baseGui(title, 3);
|
||||||
|
|
|
||||||
134
src/main/java/dev/loki/loparkour/mode/CoopMode.java
Normal file
134
src/main/java/dev/loki/loparkour/mode/CoopMode.java
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
package dev.loki.loparkour.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.Config;
|
||||||
|
import dev.loki.loparkour.config.Locales;
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.session.Session;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coop Mode - Shared score between multiple players
|
||||||
|
*/
|
||||||
|
public class CoopMode implements Mode {
|
||||||
|
|
||||||
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getName() {
|
||||||
|
return "coop";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public dev.loki.loparkour.util.Item getItem(String locale) {
|
||||||
|
return Locales.getItem(locale, "modes.coop");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Leaderboard getLeaderboard() {
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(Player player) {
|
||||||
|
if (!Config.CONFIG.getBoolean("joining")) {
|
||||||
|
player.sendMessage("§cJoining is currently disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||||
|
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof CoopMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.closeInventory();
|
||||||
|
Session.create(session -> new CoopGenerator(session), null, null, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CoopGenerator extends ParkourGenerator {
|
||||||
|
private final Map<UUID, Integer> playerContributions = new HashMap<>();
|
||||||
|
private int sharedScore = 0;
|
||||||
|
|
||||||
|
public CoopGenerator(@NotNull Session session) {
|
||||||
|
super(session);
|
||||||
|
|
||||||
|
// Initialize contributions
|
||||||
|
for (ParkourPlayer pp : session.getPlayers()) {
|
||||||
|
playerContributions.put(pp.getUUID(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void score() {
|
||||||
|
super.score();
|
||||||
|
sharedScore++;
|
||||||
|
|
||||||
|
// Track individual contributions
|
||||||
|
if (!getPlayers().isEmpty()) {
|
||||||
|
UUID scorer = player.getUUID();
|
||||||
|
playerContributions.put(scorer, playerContributions.getOrDefault(scorer, 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Milestone celebrations
|
||||||
|
if (sharedScore % 50 == 0) {
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
pp.player.sendTitle("§6" + sharedScore + " Points!", "§eTeam effort!", 5, 30, 10);
|
||||||
|
pp.player.playSound(pp.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.5f, 1.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
super.tick();
|
||||||
|
|
||||||
|
// Display shared score and contributions in ActionBar
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
int myContribution = playerContributions.getOrDefault(pp.getUUID(), 0);
|
||||||
|
int percentage = sharedScore > 0 ? (myContribution * 100 / sharedScore) : 0;
|
||||||
|
|
||||||
|
String message = "§aTeam: §e" + sharedScore + " §7| §bYou: §e" + myContribution + " §7(" + percentage + "%)";
|
||||||
|
pp.player.spigot().sendMessage(
|
||||||
|
net.md_5.bungee.api.ChatMessageType.ACTION_BAR,
|
||||||
|
net.md_5.bungee.api.chat.TextComponent.fromLegacyText(message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void fall() {
|
||||||
|
// Show final stats before reset
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
int contribution = playerContributions.getOrDefault(pp.getUUID(), 0);
|
||||||
|
pp.sendTranslated("modes.coop.stats",
|
||||||
|
Integer.toString(sharedScore),
|
||||||
|
Integer.toString(contribution));
|
||||||
|
}
|
||||||
|
|
||||||
|
super.fall();
|
||||||
|
|
||||||
|
// Reset coop state
|
||||||
|
sharedScore = 0;
|
||||||
|
playerContributions.clear();
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
playerContributions.put(pp.getUUID(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mode getMode() {
|
||||||
|
return Modes.COOP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,13 +9,16 @@ import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.Session;
|
||||||
|
|
||||||
import org.bukkit.GameMode;
|
import org.bukkit.*;
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.util.BoundingBox;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Elytra Parkour Mode - Flying parkour with rings and boost
|
* Elytra Parkour Mode - Flying parkour with rings and boost
|
||||||
*/
|
*/
|
||||||
|
|
@ -56,8 +59,17 @@ public class ElytraMode implements Mode {
|
||||||
player.closeInventory();
|
player.closeInventory();
|
||||||
|
|
||||||
// Create session with elytra setup
|
// Create session with elytra setup
|
||||||
Session.create(session -> {
|
Session.create(session -> new ElytraGenerator(session), null, null, player);
|
||||||
ParkourGenerator generator = new ParkourGenerator(session);
|
}
|
||||||
|
|
||||||
|
private static class ElytraGenerator extends ParkourGenerator {
|
||||||
|
private final List<RingCheckpoint> rings = new ArrayList<>();
|
||||||
|
private final Map<UUID, Long> fireworkCooldowns = new HashMap<>();
|
||||||
|
private final Map<UUID, Integer> ringsCollected = new HashMap<>();
|
||||||
|
private int tickCounter = 0;
|
||||||
|
|
||||||
|
public ElytraGenerator(@NotNull Session session) {
|
||||||
|
super(session);
|
||||||
|
|
||||||
// Give elytra and fireworks to all players in session
|
// Give elytra and fireworks to all players in session
|
||||||
for (ParkourPlayer parkourPlayer : session.getPlayers()) {
|
for (ParkourPlayer parkourPlayer : session.getPlayers()) {
|
||||||
|
|
@ -67,9 +79,117 @@ public class ElytraMode implements Mode {
|
||||||
|
|
||||||
int fireworks = Config.CONFIG.getInt("modes.elytra.starting-fireworks");
|
int fireworks = Config.CONFIG.getInt("modes.elytra.starting-fireworks");
|
||||||
p.getInventory().addItem(new ItemStack(Material.FIREWORK_ROCKET, fireworks));
|
p.getInventory().addItem(new ItemStack(Material.FIREWORK_ROCKET, fireworks));
|
||||||
|
|
||||||
|
ringsCollected.put(p.getUniqueId(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
super.tick();
|
||||||
|
tickCounter++;
|
||||||
|
|
||||||
|
// Spawn rings every 5 blocks
|
||||||
|
if (tickCounter % 100 == 0 && !state.history.isEmpty()) {
|
||||||
|
Location ringLoc = getLatest().getLocation().clone().add(0, 3, 0);
|
||||||
|
rings.add(new RingCheckpoint(ringLoc));
|
||||||
}
|
}
|
||||||
|
|
||||||
return generator;
|
// Display rings and check for player passing through
|
||||||
}, null, null, player);
|
Iterator<RingCheckpoint> it = rings.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
RingCheckpoint ring = it.next();
|
||||||
|
|
||||||
|
// Display ring particles
|
||||||
|
displayRing(ring.location);
|
||||||
|
|
||||||
|
// Check if any player passed through
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
if (ring.collected) continue;
|
||||||
|
|
||||||
|
BoundingBox playerBox = pp.player.getBoundingBox();
|
||||||
|
if (ring.getBoundingBox().overlaps(playerBox)) {
|
||||||
|
ring.collected = true;
|
||||||
|
int count = ringsCollected.getOrDefault(pp.getUUID(), 0) + 1;
|
||||||
|
ringsCollected.put(pp.getUUID(), count);
|
||||||
|
|
||||||
|
// Bonus points for passing through ring
|
||||||
|
lifecycle.score();
|
||||||
|
lifecycle.score();
|
||||||
|
|
||||||
|
pp.player.playSound(pp.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.5f);
|
||||||
|
pp.player.spawnParticle(Particle.TOTEM, ring.location, 20, 0.5, 0.5, 0.5, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old rings
|
||||||
|
if (ring.location.distance(player.getLocation()) > 50) {
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check firework cooldown
|
||||||
|
checkFireworkCooldown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayRing(Location center) {
|
||||||
|
World world = center.getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
double radius = 2.0;
|
||||||
|
int points = 30;
|
||||||
|
|
||||||
|
for (int i = 0; i < points; i++) {
|
||||||
|
double angle = 2 * Math.PI * i / points;
|
||||||
|
double x = center.getX() + radius * Math.cos(angle);
|
||||||
|
double z = center.getZ() + radius * Math.sin(angle);
|
||||||
|
|
||||||
|
Location particleLoc = new Location(world, x, center.getY(), z);
|
||||||
|
world.spawnParticle(Particle.END_ROD, particleLoc, 1, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkFireworkCooldown() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int cooldownMs = Config.CONFIG.getInt("modes.elytra.firework-cooldown-ms");
|
||||||
|
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
UUID uuid = pp.getUUID();
|
||||||
|
ItemStack mainHand = pp.player.getInventory().getItemInMainHand();
|
||||||
|
ItemStack offHand = pp.player.getInventory().getItemInOffHand();
|
||||||
|
|
||||||
|
boolean usingFirework = (mainHand.getType() == Material.FIREWORK_ROCKET && pp.player.isGliding())
|
||||||
|
|| (offHand.getType() == Material.FIREWORK_ROCKET && pp.player.isGliding());
|
||||||
|
|
||||||
|
if (usingFirework) {
|
||||||
|
Long lastUse = fireworkCooldowns.get(uuid);
|
||||||
|
if (lastUse != null && now - lastUse < cooldownMs) {
|
||||||
|
long remaining = (cooldownMs - (now - lastUse)) / 1000;
|
||||||
|
pp.player.spigot().sendMessage(net.md_5.bungee.api.ChatMessageType.ACTION_BAR,
|
||||||
|
net.md_5.bungee.api.chat.TextComponent.fromLegacyText("§cFirework cooldown: " + remaining + "s"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fireworkCooldowns.put(uuid, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mode getMode() {
|
||||||
|
return Modes.ELYTRA;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RingCheckpoint {
|
||||||
|
final Location location;
|
||||||
|
boolean collected = false;
|
||||||
|
|
||||||
|
RingCheckpoint(Location location) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoundingBox getBoundingBox() {
|
||||||
|
return BoundingBox.of(location, 2.5, 2.5, 2.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,12 @@ public class GravityShiftMode implements Mode {
|
||||||
);
|
);
|
||||||
|
|
||||||
player.player.addPotionEffect(currentEffect);
|
player.player.addPotionEffect(currentEffect);
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
player.player.sendTitle("§6⚡", "§e" + selected.name, 5, 20, 10);
|
||||||
|
player.player.playSound(player.player.getLocation(), org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.5f);
|
||||||
|
player.player.spawnParticle(org.bukkit.Particle.PORTAL, player.player.getLocation().add(0, 1, 0), 50, 0.5, 0.5, 0.5, 0.5);
|
||||||
|
|
||||||
player.sendTranslated("modes.gravity-shift.effect-applied", selected.name);
|
player.sendTranslated("modes.gravity-shift.effect-applied", selected.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ public class HardcoreMode implements Mode {
|
||||||
boolean resetRewards = Config.CONFIG.getBoolean("modes.hardcore.reset-rewards");
|
boolean resetRewards = Config.CONFIG.getBoolean("modes.hardcore.reset-rewards");
|
||||||
|
|
||||||
if (resetRewards) {
|
if (resetRewards) {
|
||||||
|
player.collectedRewards.clear();
|
||||||
player.sendTranslated("modes.hardcore.rewards-lost");
|
player.sendTranslated("modes.hardcore.rewards-lost");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ public class Modes {
|
||||||
public static GravityShiftMode GRAVITY_SHIFT;
|
public static GravityShiftMode GRAVITY_SHIFT;
|
||||||
public static HardcoreMode HARDCORE;
|
public static HardcoreMode HARDCORE;
|
||||||
public static ElytraMode ELYTRA;
|
public static ElytraMode ELYTRA;
|
||||||
|
public static RaceMode RACE;
|
||||||
|
public static CoopMode COOP;
|
||||||
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
DEFAULT = (DefaultMode) Registry.getMode("default");
|
DEFAULT = (DefaultMode) Registry.getMode("default");
|
||||||
|
|
@ -18,5 +20,7 @@ public class Modes {
|
||||||
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
||||||
HARDCORE = (HardcoreMode) Registry.getMode("hardcore");
|
HARDCORE = (HardcoreMode) Registry.getMode("hardcore");
|
||||||
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
||||||
|
RACE = (RaceMode) Registry.getMode("race");
|
||||||
|
COOP = (CoopMode) Registry.getMode("coop");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
src/main/java/dev/loki/loparkour/mode/RaceMode.java
Normal file
108
src/main/java/dev/loki/loparkour/mode/RaceMode.java
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
package dev.loki.loparkour.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.Config;
|
||||||
|
import dev.loki.loparkour.config.Locales;
|
||||||
|
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.session.Session;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race Mode - First to reach target score wins
|
||||||
|
*/
|
||||||
|
public class RaceMode implements Mode {
|
||||||
|
|
||||||
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.TIME);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getName() {
|
||||||
|
return "race";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public dev.loki.loparkour.util.Item getItem(String locale) {
|
||||||
|
return Locales.getItem(locale, "modes.race");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Leaderboard getLeaderboard() {
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(Player player) {
|
||||||
|
if (!Config.CONFIG.getBoolean("joining")) {
|
||||||
|
player.sendMessage("§cJoining is currently disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||||
|
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof RaceMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.closeInventory();
|
||||||
|
Session.create(session -> new RaceGenerator(session), null, null, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RaceGenerator extends ParkourGenerator {
|
||||||
|
private final int targetScore;
|
||||||
|
private boolean raceFinished = false;
|
||||||
|
|
||||||
|
public RaceGenerator(@NotNull Session session) {
|
||||||
|
super(session);
|
||||||
|
this.targetScore = Config.CONFIG.getInt("modes.race.target-score");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
super.tick();
|
||||||
|
|
||||||
|
if (raceFinished) return;
|
||||||
|
|
||||||
|
// Check if target reached
|
||||||
|
if (state.score >= targetScore) {
|
||||||
|
finishRace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display progress in ActionBar
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
int remaining = targetScore - state.score;
|
||||||
|
String progress = "§e" + state.score + " §7/ §a" + targetScore + " §7(§c" + remaining + " §7left)";
|
||||||
|
pp.player.spigot().sendMessage(
|
||||||
|
net.md_5.bungee.api.ChatMessageType.ACTION_BAR,
|
||||||
|
net.md_5.bungee.api.chat.TextComponent.fromLegacyText(progress)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRace() {
|
||||||
|
raceFinished = true;
|
||||||
|
|
||||||
|
for (ParkourPlayer pp : getPlayers()) {
|
||||||
|
pp.player.sendTitle("§6§lRACE COMPLETE!", "§aFinished in " + getFormattedTime(), 10, 70, 20);
|
||||||
|
pp.player.playSound(pp.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f);
|
||||||
|
pp.sendTranslated("modes.race.completed", Integer.toString(targetScore), getFormattedTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register time-based score
|
||||||
|
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), targetScore);
|
||||||
|
|
||||||
|
// Stop generator after 5 seconds
|
||||||
|
dev.lolib.scheduler.Scheduler.get(dev.loki.loparkour.LoParkour.getPlugin())
|
||||||
|
.runLater(() -> reset(false), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mode getMode() {
|
||||||
|
return Modes.RACE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package dev.loki.loparkour.mode;
|
||||||
import dev.loki.loparkour.util.Item;
|
import dev.loki.loparkour.util.Item;
|
||||||
|
|
||||||
import dev.lolib.scheduler.Scheduler;
|
import dev.lolib.scheduler.Scheduler;
|
||||||
|
import dev.lolib.scheduler.ScheduledTask;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
|
|
@ -12,7 +13,6 @@ import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.ParkourPlayer;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.Session;
|
||||||
|
|
||||||
import dev.lolib.scheduler.Scheduler;
|
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
|
|
@ -27,7 +27,6 @@ import java.util.Map;
|
||||||
public class SpeedrunMode implements Mode {
|
public class SpeedrunMode implements Mode {
|
||||||
|
|
||||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||||
private final Map<Block, Long> blockTimestamps = new HashMap<>();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|
@ -64,44 +63,60 @@ public class SpeedrunMode implements Mode {
|
||||||
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
|
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onBlockTouch(@NotNull Block block, @NotNull ParkourPlayer player) {
|
|
||||||
if (!blockTimestamps.containsKey(block)) {
|
|
||||||
blockTimestamps.put(block, System.currentTimeMillis());
|
|
||||||
scheduleBlockRemoval(player, block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scheduleBlockRemoval(@NotNull ParkourPlayer player, @NotNull Block block) {
|
|
||||||
double blockLifetime = Config.CONFIG.getDouble("modes.speedrun.block-lifetime");
|
|
||||||
double warningTime = Config.CONFIG.getDouble("modes.speedrun.warning-time");
|
|
||||||
|
|
||||||
long removalTime = (long) (blockLifetime * 1000);
|
|
||||||
long warningTimeMs = (long) (warningTime * 1000);
|
|
||||||
|
|
||||||
Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
|
||||||
if (block.getType() != Material.AIR) {
|
|
||||||
player.player.playSound(block.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f);
|
|
||||||
player.player.spawnParticle(Particle.FLAME, block.getLocation().add(0.5, 1, 0.5), 10, 0.3, 0.3, 0.3, 0.01);
|
|
||||||
}
|
|
||||||
}, warningTimeMs / 50);
|
|
||||||
|
|
||||||
Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
|
||||||
if (block.getType() != Material.AIR) {
|
|
||||||
block.setType(Material.AIR);
|
|
||||||
blockTimestamps.remove(block);
|
|
||||||
}
|
|
||||||
}, removalTime / 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset() {
|
|
||||||
blockTimestamps.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SpeedrunGenerator extends ParkourGenerator {
|
private static class SpeedrunGenerator extends ParkourGenerator {
|
||||||
|
private final Map<Block, Long> blockTimestamps = new HashMap<>();
|
||||||
|
private final Map<Block, ScheduledTask> scheduledTasks = new HashMap<>();
|
||||||
|
|
||||||
public SpeedrunGenerator(@NotNull Session session) {
|
public SpeedrunGenerator(@NotNull Session session) {
|
||||||
super(session);
|
super(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onBlockTouch(@NotNull Block block) {
|
||||||
|
if (!blockTimestamps.containsKey(block)) {
|
||||||
|
blockTimestamps.put(block, System.currentTimeMillis());
|
||||||
|
scheduleBlockRemoval(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleBlockRemoval(@NotNull Block block) {
|
||||||
|
double blockLifetime = Config.CONFIG.getDouble("modes.speedrun.block-lifetime");
|
||||||
|
double warningTime = Config.CONFIG.getDouble("modes.speedrun.warning-time");
|
||||||
|
|
||||||
|
long removalTime = (long) (blockLifetime * 1000);
|
||||||
|
long warningTimeMs = (long) (warningTime * 1000);
|
||||||
|
|
||||||
|
ScheduledTask warningTask = Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
||||||
|
if (block.getType() != Material.AIR) {
|
||||||
|
player.player.playSound(block.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f);
|
||||||
|
player.player.spawnParticle(Particle.FLAME, block.getLocation().add(0.5, 1, 0.5), 10, 0.3, 0.3, 0.3, 0.01);
|
||||||
|
}
|
||||||
|
}, warningTimeMs / 50);
|
||||||
|
|
||||||
|
ScheduledTask removalTask = Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
||||||
|
if (block.getType() != Material.AIR) {
|
||||||
|
block.setType(Material.AIR);
|
||||||
|
blockTimestamps.remove(block);
|
||||||
|
scheduledTasks.remove(block);
|
||||||
|
}
|
||||||
|
}, removalTime / 50);
|
||||||
|
|
||||||
|
scheduledTasks.put(block, removalTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(boolean regenerate) {
|
||||||
|
// Cancel all scheduled tasks
|
||||||
|
for (ScheduledTask task : scheduledTasks.values()) {
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduledTasks.clear();
|
||||||
|
blockTimestamps.clear();
|
||||||
|
|
||||||
|
super.reset(regenerate);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mode getMode() {
|
public Mode getMode() {
|
||||||
return Modes.SPEEDRUN;
|
return Modes.SPEEDRUN;
|
||||||
|
|
|
||||||
|
|
@ -165,18 +165,26 @@ public class ParkourPlayer extends ParkourUser {
|
||||||
Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
|
||||||
player.getInventory().clear();
|
player.getInventory().clear();
|
||||||
|
|
||||||
List<dev.loki.loparkour.util.Item> items = new ArrayList<>();
|
// Load hotbar slots from config
|
||||||
|
if (ParkourOption.PLAY.mayPerform(player)) {
|
||||||
if (ParkourOption.PLAY.mayPerform(player)) items.add(Locales.getItem(locale, "play.item"));
|
int slot = Config.CONFIG.getInt("options.hotbar-slots.play");
|
||||||
if (ParkourOption.COMMUNITY.mayPerform(player)) items.add(Locales.getItem(locale, "community.item"));
|
player.getInventory().setItem(slot, Locales.getItem(locale, "play.item").build());
|
||||||
if (ParkourOption.SETTINGS.mayPerform(player)) items.add(Locales.getItem(locale, "settings.item"));
|
}
|
||||||
if (ParkourOption.LOBBY.mayPerform(player)) items.add(Locales.getItem(locale, "lobby.item"));
|
if (ParkourOption.COMMUNITY.mayPerform(player)) {
|
||||||
|
int slot = Config.CONFIG.getInt("options.hotbar-slots.community");
|
||||||
if (ParkourOption.QUIT.mayPerform(player)) items.add(Locales.getItem(locale, "other.quit"));
|
player.getInventory().setItem(slot, Locales.getItem(locale, "community.item").build());
|
||||||
|
}
|
||||||
List<Integer> slots = getEvenlyDistributedSlots(items.size());
|
if (ParkourOption.SETTINGS.mayPerform(player)) {
|
||||||
for (int idx = 0; idx < items.size(); idx++) {
|
int slot = Config.CONFIG.getInt("options.hotbar-slots.settings");
|
||||||
player.getInventory().setItem(slots.get(idx), items.get(idx).build());
|
player.getInventory().setItem(slot, Locales.getItem(locale, "settings.item").build());
|
||||||
|
}
|
||||||
|
if (ParkourOption.LOBBY.mayPerform(player)) {
|
||||||
|
int slot = Config.CONFIG.getInt("options.hotbar-slots.lobby");
|
||||||
|
player.getInventory().setItem(slot, Locales.getItem(locale, "lobby.item").build());
|
||||||
|
}
|
||||||
|
if (ParkourOption.QUIT.mayPerform(player)) {
|
||||||
|
int slot = Config.CONFIG.getInt("options.hotbar-slots.quit");
|
||||||
|
player.getInventory().setItem(slot, Locales.getItem(locale, "other.quit").build());
|
||||||
}
|
}
|
||||||
}, 5);
|
}, 5);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -186,13 +194,4 @@ public class ParkourPlayer extends ParkourUser {
|
||||||
|
|
||||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Integer> getEvenlyDistributedSlots(int count) {
|
|
||||||
List<Integer> slots = new ArrayList<>();
|
|
||||||
int[] positions = {10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25};
|
|
||||||
for (int i = 0; i < Math.min(count, positions.length); i++) {
|
|
||||||
slots.add(positions[i]);
|
|
||||||
}
|
|
||||||
return slots;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,16 @@ options:
|
||||||
# Disables the ability for a player to open blocks with an inventory.
|
# Disables the ability for a player to open blocks with an inventory.
|
||||||
disable-inventory-blocks: false
|
disable-inventory-blocks: false
|
||||||
|
|
||||||
|
# -= Hotbar slots =-
|
||||||
|
# Configure which hotbar slots (0-8) to use for parkour items
|
||||||
|
# Items will be placed in these slots in order: play, community, settings, lobby, quit
|
||||||
|
hotbar-slots:
|
||||||
|
play: 1
|
||||||
|
community: 3
|
||||||
|
settings: 4
|
||||||
|
lobby: 5
|
||||||
|
quit: 7
|
||||||
|
|
||||||
# -= Permissions options =-
|
# -= Permissions options =-
|
||||||
# These permissions are used to determine which things the player can change.
|
# These permissions are used to determine which things the player can change.
|
||||||
# Find them at https://efnilite.dev/projects/ip/wiki
|
# Find them at https://efnilite.dev/projects/ip/wiki
|
||||||
|
|
@ -354,11 +364,24 @@ modes:
|
||||||
ring-size: 5
|
ring-size: 5
|
||||||
# Firework boost power
|
# Firework boost power
|
||||||
boost-power: 1.5
|
boost-power: 1.5
|
||||||
# Firework cooldown in seconds
|
# Firework cooldown in milliseconds
|
||||||
firework-cooldown: 3
|
firework-cooldown-ms: 3000
|
||||||
# Starting fireworks amount
|
# Starting fireworks amount
|
||||||
starting-fireworks: 64
|
starting-fireworks: 64
|
||||||
|
|
||||||
|
# -= Race mode =-
|
||||||
|
race:
|
||||||
|
# Enable race mode
|
||||||
|
enabled: true
|
||||||
|
# Target score to win
|
||||||
|
target-score: 100
|
||||||
|
|
||||||
|
# -= Co-op mode =-
|
||||||
|
coop:
|
||||||
|
# Enable co-op mode
|
||||||
|
enabled: true
|
||||||
|
# Maximum players in co-op session
|
||||||
|
max-players: 4
|
||||||
|
|
||||||
|
|
||||||
# -= Scoring options =-
|
# -= Scoring options =-
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,18 @@ generation:
|
||||||
# -= Distance chances =-
|
# -= Distance chances =-
|
||||||
# The chances of the different distances in %. Can be above 100%.
|
# The chances of the different distances in %. Can be above 100%.
|
||||||
distance:
|
distance:
|
||||||
1: 10
|
1: 5
|
||||||
2: 55
|
2: 35
|
||||||
3: 34
|
3: 45
|
||||||
4: 1
|
4: 15
|
||||||
|
|
||||||
# -= Height chances =-
|
# -= Height chances =-
|
||||||
# The chances of the different heights a jump can have in %. Can be above 100%.
|
# The chances of the different heights a jump can have in %. Can be above 100%.
|
||||||
height:
|
height:
|
||||||
1: 20
|
1: 30
|
||||||
0: 65
|
0: 40
|
||||||
-1: 10
|
-1: 20
|
||||||
-2: 5
|
-2: 10
|
||||||
|
|
||||||
# -= Special jump chances =-
|
# -= Special jump chances =-
|
||||||
special:
|
special:
|
||||||
|
|
@ -79,6 +79,10 @@ advanced:
|
||||||
# This block will be set to air once the schematic has been pasted
|
# This block will be set to air once the schematic has been pasted
|
||||||
begin-block: emerald_block
|
begin-block: emerald_block
|
||||||
|
|
||||||
|
# -= First parkour block material =-
|
||||||
|
# The material of the very first parkour block that players spawn on
|
||||||
|
first-block-material: stone
|
||||||
|
|
||||||
# -= Directional heading =-
|
# -= Directional heading =-
|
||||||
# The directional heading of the parkour
|
# The directional heading of the parkour
|
||||||
# Possible options: north, south, west and east.
|
# Possible options: north, south, west and east.
|
||||||
|
|
|
||||||
|
|
@ -279,3 +279,13 @@ modes:
|
||||||
material: "elytra"
|
material: "elytra"
|
||||||
name: "�D4FF&lElytra"
|
name: "�D4FF&lElytra"
|
||||||
lore: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost"
|
lore: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost"
|
||||||
|
|
||||||
|
race:
|
||||||
|
material: "golden_boots"
|
||||||
|
name: "&#FFD700&lRace"
|
||||||
|
lore: "&#A0A0A0First to reach target score!||&#A0A0A0Compete for fastest time"
|
||||||
|
|
||||||
|
coop:
|
||||||
|
material: "player_head"
|
||||||
|
name: "�FF00&lCo-op"
|
||||||
|
lore: "&#A0A0A0Team up with friends!||&#A0A0A0Shared score and progress"
|
||||||
|
|
|
||||||
|
|
@ -278,3 +278,13 @@ modes:
|
||||||
material: "elytra"
|
material: "elytra"
|
||||||
name: "�D4FF&lЭлитры"
|
name: "�D4FF&lЭлитры"
|
||||||
lore: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения"
|
lore: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения"
|
||||||
|
|
||||||
|
race:
|
||||||
|
material: "golden_boots"
|
||||||
|
name: "&#FFD700&lГонка"
|
||||||
|
lore: "&#A0A0A0Первый до целевого счёта!||&#A0A0A0Соревнуйся за лучшее время"
|
||||||
|
|
||||||
|
coop:
|
||||||
|
material: "player_head"
|
||||||
|
name: "�FF00&lКооп"
|
||||||
|
lore: "&#A0A0A0Играй с друзьями!||&#A0A0A0Общий счёт и прогресс"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue