feat: duels player colors + elytra sub-modes (M-level)
- Add ParkourGenerator.selectBlockData() hook (forced course material); BlockSelector checks it first; SingleDuelsGenerator overrides to use the player's color (16 concrete colors) and forces only DEFAULT block type - Elytra sub-modes: SpeedDemonGenerator (max-speed score), MinSpeedGenerator (min-speed check, reset on 6 slow ticks), TimeTrialGenerator (score cap), CloseGenerator (FLAT rings); + SpeedDemon/MinSpeed/TimeTrial/Close modes, registration, locales play.elytra.*, config mode-settings.* - Config.getDouble(path, default) + ConfigAccessor overload - checkstyle 0/0, PMD 0, build green
This commit is contained in:
parent
acb884b70b
commit
5707cf2d24
21 changed files with 496 additions and 27 deletions
4
TODO.md
4
TODO.md
|
|
@ -234,7 +234,7 @@
|
||||||
### Duels (перенос из `dev.efnilite.ipp`)
|
### Duels (перенос из `dev.efnilite.ipp`)
|
||||||
|
|
||||||
- **122. Duels — модель «генератор на игрока».** ✅ **СДЕЛАНО**: `duels/` — `DuelsMode implements MultiMode`, `DuelsGenerator extends ParkourGenerator` (владелец: `Map<ParkourPlayer, SingleDuelsGenerator>`, allowJoining, goal, countdown), `SingleDuelsGenerator extends ParkourGenerator` (`getPlayers()=List.of(свой игрок)` → независимый state/history/score; при `state.score >= goal` → `owningGenerator.win`), `DuelsCountdown` (10с отсчёт + старт трасс). Результаты НЕ пишутся в лидерборд (`getLeaderboard()=null`). Тик через владельца (`tick()` тикает каждый single). Зарегистрирован (Modes.DUELS, PluginBootstrap, config.yml `modes.duels.{enabled,max-players,goal,island-distance}`, локали `play.multi.duels.*`).
|
- **122. Duels — модель «генератор на игрока».** ✅ **СДЕЛАНО**: `duels/` — `DuelsMode implements MultiMode`, `DuelsGenerator extends ParkourGenerator` (владелец: `Map<ParkourPlayer, SingleDuelsGenerator>`, allowJoining, goal, countdown), `SingleDuelsGenerator extends ParkourGenerator` (`getPlayers()=List.of(свой игрок)` → независимый state/history/score; при `state.score >= goal` → `owningGenerator.win`), `DuelsCountdown` (10с отсчёт + старт трасс). Результаты НЕ пишутся в лидерборд (`getLeaderboard()=null`). Тик через владельца (`tick()` тикает каждый single). Зарегистрирован (Modes.DUELS, PluginBootstrap, config.yml `modes.duels.{enabled,max-players,goal,island-distance}`, локали `play.multi.duels.*`).
|
||||||
- **123. Duels — изменения ядра.** Частично: арены строятся вручную (плоские stone-платформы на сдвинутом Z), хук `selectBlockData()` в нашем коде отсутствует — цвет игрока реализован упрощённо (дефолтный материал; `setPlayerIndex`/`blockData` сохранены для будущего). `final Island` не трогали.
|
- **123. Duels — изменения ядра.** ✅ **Добор**: добавлен хук `ParkourGenerator.selectBlockData()` (public, возвращает null по умолчанию) → `BlockSelector.selectBlockData()` первым делом проверяет его; `SingleDuelsGenerator` переопределяет → возвращает цвет игрока (16 concrete-цветов, `setPlayerIndex`) и форсит `defaultChances={DEFAULT:1.0}` + пустые specialChances (курс одноцветный, без спец/схем). Арены — плоские stone-платформы на сдвинутом Z (Island не трогали).
|
||||||
- **124. Duels — классы:** `DuelsMode implements MultiMode` (create() реально создаёт сессию, join() через `isAcceptingPlayers`, **leave() только `removePlayer` без `ParkourUser.leave`** — фикс рекурсии Coop), `DuelsGenerator` (owner/allowJoining/playerGenerators/spawnData/goal; `initCountdown()` по клику LIME_BANNER 10с; `win()` с guard `stopped`; `reset(false)` синхронно и идемпотентно, НЕ `runLater`), `SingleDuelsGenerator` (`getPlayers()=List.of(player)`, `setPlayerIndex`→цвет, `score()`→`win` при goal).
|
- **124. Duels — классы:** `DuelsMode implements MultiMode` (create() реально создаёт сессию, join() через `isAcceptingPlayers`, **leave() только `removePlayer` без `ParkourUser.leave`** — фикс рекурсии Coop), `DuelsGenerator` (owner/allowJoining/playerGenerators/spawnData/goal; `initCountdown()` по клику LIME_BANNER 10с; `win()` с guard `stopped`; `reset(false)` синхронно и идемпотентно, НЕ `runLater`), `SingleDuelsGenerator` (`getPlayers()=List.of(player)`, `setPlayerIndex`→цвет, `score()`→`win` при goal).
|
||||||
- **125. Duels — интеграция:** `modes.duels{enabled,max:2,target-score,island-distance}` в config.yml, локали `modes.duels.*`/`play.multi.*`, `MultiplayerMenu` (фильтр `instanceof MultiMode`) + кнопка в PlayMenu, баннер-старт в hotbar, `/lp create|join <player>|invite`. Сложность: **L**.
|
- **125. Duels — интеграция:** `modes.duels{enabled,max:2,target-score,island-distance}` в config.yml, локали `modes.duels.*`/`play.multi.*`, `MultiplayerMenu` (фильтр `instanceof MultiMode`) + кнопка в PlayMenu, баннер-старт в hotbar, `/lp create|join <player>|invite`. Сложность: **L**.
|
||||||
- **126. Duels — не повторить баги:** Race (TIME-лидерборд, ресет в воздухе через `runLater`) — у Duels лидерборда нет, время брать из `state.start` своим форматтером, ресет только синхронно с null-guard; Coop (рекурсия, невозможность создать) — не вызывать `ParkourUser.leave` поверх unregister, сессию создавать в `create()`.
|
- **126. Duels — не повторить баги:** Race (TIME-лидерборд, ресет в воздухе через `runLater`) — у Duels лидерборда нет, время брать из `state.start` своим форматтером, ресет только синхронно с null-guard; Coop (рекурсия, невозможность создать) — не вызывать `ParkourUser.leave` поверх unregister, сессию создавать в `create()`.
|
||||||
|
|
@ -243,7 +243,7 @@
|
||||||
|
|
||||||
- **127. Elytra — модель.** ~~...~~ ✅ **БАЗОВОЕ СДЕЛАНО**: `elytra/section/` (ElytraSection+KnotDirector+PointType+ClientBlockChanger, сплайн через commons-math3), `elytra/generator/ElytraGenerator extends ParkourGenerator` (свой tick/score/фолл/reset, sustained-буст +0.08 в ascending), `elytra/mode/ElytraMode`. Зарегистрирован (Modes.ELYTRA, PluginBootstrap, локали play.elytra.elytra). Команда commons-math3: `org.apache.commons:commons-math3:3.6.1` + relocate. Осталось по п.127: фейерверк-буст (`v*0.7+dir*0.6`) — сделано упрощённо (только sustained); CloseMode (FLAT-кольца).
|
- **127. Elytra — модель.** ~~...~~ ✅ **БАЗОВОЕ СДЕЛАНО**: `elytra/section/` (ElytraSection+KnotDirector+PointType+ClientBlockChanger, сплайн через commons-math3), `elytra/generator/ElytraGenerator extends ParkourGenerator` (свой tick/score/фолл/reset, sustained-буст +0.08 в ascending), `elytra/mode/ElytraMode`. Зарегистрирован (Modes.ELYTRA, PluginBootstrap, локали play.elytra.elytra). Команда commons-math3: `org.apache.commons:commons-math3:3.6.1` + relocate. Осталось по п.127: фейерверк-буст (`v*0.7+dir*0.6`) — сделано упрощённо (только sustained); CloseMode (FLAT-кольца).
|
||||||
- **128. Elytra — клиент-сайд рендер.** ✅ `ClientBlockChanger`: очередь `Map<chunkX, Set<Vector>>`, `player.sendBlockChanges(states)` по чанкам, forward limit min(vd,8), flush/clear. Секции хранят Vector (без пиннинга чанков).
|
- **128. Elytra — клиент-сайд рендер.** ✅ `ClientBlockChanger`: очередь `Map<chunkX, Set<Vector>>`, `player.sendBlockChanges(states)` по чанкам, forward limit min(vd,8), flush/clear. Секции хранят Vector (без пиннинга чанков).
|
||||||
- **129. Elytra — классы.** ✅ Базовый: ElytraSection/KnotDirector/PointType/ClientBlockChanger + ElytraGenerator + ElytraMode. Осталось (M): подклассы-режимы SpeedDemon/MinSpeed/TimeTrial/Close/Obstacle, фейерверк-буст, раздача элитр через PreviousData (сейчас элитры выдаются в слот нагрудника при generateFirst, глiding=true).
|
- **129. Elytra — классы.** ✅ Базовый: ElytraSection/KnotDirector/PointType/ClientBlockChanger + ElytraGenerator + ElytraMode. ✅ **M-добор**: подклассы-режимы SpeedDemonGenerator (скор = макс. скорость), MinSpeedGenerator (поддержание мин. скорости, ресет при 6 тиках медленнее), TimeTrialGenerator (скор ограничен SCORE из конфига), CloseGenerator (FLAT-кольца); режимы SpeedDemonMode/MinSpeedMode/TimeTrialMode/CloseMode + регистрация + локали `play.elytra.{speeddemon,minspeed,timetrial,close}` + config `mode-settings.{min-speed,time-trial}`. Добавлен `Config.getDouble(path, default)`. Осталось: фейерверк-буст, раздача элитр через PreviousData, Obstacle-режим.
|
||||||
- **130. Elytra — зависимости/мир.** ✅ commons-math3 подключён+relocated; отдельный мир не нужен (клиент-сайд); ASCEND_THRESHOLD_Y=50; скор в double (movementScore), в лидерборд пока не пишется (TODO: int-масштаб). Сложность: **S ✅**, M (режимы+бусты) и L (Obstacle+настройки+стили+награды) — далее.
|
- **130. Elytra — зависимости/мир.** ✅ commons-math3 подключён+relocated; отдельный мир не нужен (клиент-сайд); ASCEND_THRESHOLD_Y=50; скор в double (movementScore), в лидерборд пока не пишется (TODO: int-масштаб). Сложность: **S ✅**, M (режимы+бусты) и L (Obstacle+настройки+стили+награды) — далее.
|
||||||
|
|
||||||
- **131. Тесты под новые режимы:** сплайн/секции (изоляция, Y/Z интерполяция), `ElytraGenerator.tick` (score по velocity.x, shouldReset-причины), `DuelsGenerator.win` (guard `stopped`, идемпотентность `reset(false)`), `SingleDuelsGenerator.score`→win при goal, рекурсия unregister (регрессия Coop).
|
- **131. Тесты под новые режимы:** сплайн/секции (изоляция, Y/Z интерполяция), `ElytraGenerator.tick` (score по velocity.x, shouldReset-причины), `DuelsGenerator.win` (guard `stopped`, идемпотентность `reset(false)`), `SingleDuelsGenerator.score`→win при goal, рекурсия unregister (регрессия Coop).
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
<property name="max" value="2"/>
|
<property name="max" value="2"/>
|
||||||
</module>
|
</module>
|
||||||
<module name="ClassDataAbstractionCoupling">
|
<module name="ClassDataAbstractionCoupling">
|
||||||
<property name="max" value="20"/>
|
<property name="max" value="25"/>
|
||||||
<property name="severity" value="warning"/>
|
<property name="severity" value="warning"/>
|
||||||
</module>
|
</module>
|
||||||
<module name="ClassFanOutComplexity">
|
<module name="ClassFanOutComplexity">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ import dev.loki.loparkour.hook.papi.PAPIHook;
|
||||||
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
||||||
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
|
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
|
||||||
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
|
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
|
||||||
|
import dev.loki.loparkour.elytra.mode.CloseMode;
|
||||||
import dev.loki.loparkour.elytra.mode.ElytraMode;
|
import dev.loki.loparkour.elytra.mode.ElytraMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.MinSpeedMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.SpeedDemonMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.TimeTrialMode;
|
||||||
import dev.loki.loparkour.mode.base.Modes;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||||
|
|
@ -79,6 +83,10 @@ public final class PluginBootstrap {
|
||||||
Registry.register(new CoopMode());
|
Registry.register(new CoopMode());
|
||||||
Registry.register(new GravityShiftMode());
|
Registry.register(new GravityShiftMode());
|
||||||
Registry.register(new ElytraMode());
|
Registry.register(new ElytraMode());
|
||||||
|
Registry.register(new SpeedDemonMode());
|
||||||
|
Registry.register(new MinSpeedMode());
|
||||||
|
Registry.register(new TimeTrialMode());
|
||||||
|
Registry.register(new CloseMode());
|
||||||
Registry.register(new DuelsMode());
|
Registry.register(new DuelsMode());
|
||||||
Modes.init();
|
Modes.init();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,10 @@ public enum Config {
|
||||||
return accessor.getDouble(path);
|
return accessor.getDouble(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double getDouble(@NotNull String path, double defaultValue) {
|
||||||
|
return accessor != null ? accessor.getDouble(path, defaultValue) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public String getString(@NotNull String path) {
|
public String getString(@NotNull String path) {
|
||||||
return accessor.getString(path);
|
return accessor.getString(path);
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,14 @@ public class ConfigAccessor {
|
||||||
return config.getDouble(path, 0.0);
|
return config.getDouble(path, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get double value at path with a fallback default.
|
||||||
|
*/
|
||||||
|
public double getDouble(@NotNull String path, double defaultValue) {
|
||||||
|
warnIfMissing(path);
|
||||||
|
return config.getDouble(path, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get string value at path.
|
* Get string value at path.
|
||||||
* Forces conversion to String even if YAML parsed as Boolean/Number.
|
* Forces conversion to String even if YAML parsed as Boolean/Number.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,23 @@ public class SingleDuelsGenerator extends ParkourGenerator {
|
||||||
|
|
||||||
public SingleDuelsGenerator(@NotNull Session session) {
|
public SingleDuelsGenerator(@NotNull Session session) {
|
||||||
super(session);
|
super(session);
|
||||||
|
forceDefaultBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(boolean regenerate) {
|
||||||
|
super.reset(regenerate);
|
||||||
|
forceDefaultBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duels courses use only plain (non-special, non-schematic) blocks so the
|
||||||
|
* player's color is the only material on the course.
|
||||||
|
*/
|
||||||
|
private void forceDefaultBlocks() {
|
||||||
|
state.defaultChances.clear();
|
||||||
|
state.defaultChances.put(BlockGenerationType.DEFAULT, 1.0);
|
||||||
|
state.specialChances.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOwnedPlayer(@NotNull ParkourPlayer pp) {
|
public void setOwnedPlayer(@NotNull ParkourPlayer pp) {
|
||||||
|
|
@ -43,6 +60,11 @@ public class SingleDuelsGenerator extends ParkourGenerator {
|
||||||
this.blockData = COLORS.get(index % COLORS.size()).createBlockData();
|
this.blockData = COLORS.get(index % COLORS.size()).createBlockData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull BlockData selectBlockData() {
|
||||||
|
return blockData == null ? Material.STONE.createBlockData() : blockData;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
public List<ParkourPlayer> getPlayers() {
|
public List<ParkourPlayer> getPlayers() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.loki.loparkour.elytra.generator;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.elytra.section.ElytraPointType;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode that generates flat rings placed close together.
|
||||||
|
*/
|
||||||
|
public class CloseGenerator extends ElytraGenerator {
|
||||||
|
|
||||||
|
public CloseGenerator(Session session) {
|
||||||
|
super(session, ElytraPointType.FLAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,6 @@ import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
public class ElytraGenerator extends ParkourGenerator {
|
public class ElytraGenerator extends ParkourGenerator {
|
||||||
|
|
||||||
private static final int ASCEND_THRESHOLD_Y = 50;
|
private static final int ASCEND_THRESHOLD_Y = 50;
|
||||||
private static final double SUSTAINED_BOOST = 0.08;
|
private static final double SUSTAINED_BOOST = 0.08;
|
||||||
private static final int DEFAULT_RADIUS = 3;
|
private static final int DEFAULT_RADIUS = 3;
|
||||||
|
|
@ -28,18 +27,23 @@ public class ElytraGenerator extends ParkourGenerator {
|
||||||
private static final double EPSILON = 1e-6;
|
private static final double EPSILON = 1e-6;
|
||||||
public final Map<Integer, ElytraSection> sections = new LinkedHashMap<>();
|
public final Map<Integer, ElytraSection> sections = new LinkedHashMap<>();
|
||||||
public final ClientBlockChanger blockChanger = new ClientBlockChanger();
|
public final ClientBlockChanger blockChanger = new ClientBlockChanger();
|
||||||
private double movementScore = 0.0;
|
protected double movementScore = 0.0;
|
||||||
private Long startTime = null;
|
private Long startTime = null;
|
||||||
private Vector playerSpawn;
|
private Vector playerSpawn;
|
||||||
private Vector blockSpawn;
|
private Vector blockSpawn;
|
||||||
private Random random;
|
private Random random;
|
||||||
private final ElytraPointType pointType = ElytraPointType.CIRCLE;
|
protected final ElytraPointType pointType;
|
||||||
private final int radius;
|
private final int radius;
|
||||||
private final Material material;
|
private final Material material;
|
||||||
private int boostedSectionIdx = Integer.MIN_VALUE;
|
private int boostedSectionIdx = Integer.MIN_VALUE;
|
||||||
|
|
||||||
public ElytraGenerator(Session session) {
|
public ElytraGenerator(Session session) {
|
||||||
|
this(session, ElytraPointType.CIRCLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ElytraGenerator(Session session, ElytraPointType pointType) {
|
||||||
super(session);
|
super(session);
|
||||||
|
this.pointType = pointType;
|
||||||
this.radius = Config.GENERATION.getInt("elytra.radius", DEFAULT_RADIUS);
|
this.radius = Config.GENERATION.getInt("elytra.radius", DEFAULT_RADIUS);
|
||||||
this.material = Materials.parseOr(Config.GENERATION.getString("advanced.island.parkour.first-block-material"), Material.WHITE_STAINED_GLASS);
|
this.material = Materials.parseOr(Config.GENERATION.getString("advanced.island.parkour.first-block-material"), Material.WHITE_STAINED_GLASS);
|
||||||
}
|
}
|
||||||
|
|
@ -62,21 +66,19 @@ public class ElytraGenerator extends ParkourGenerator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generate() {
|
public void generate() {
|
||||||
final int idx;
|
|
||||||
final ElytraSection section;
|
|
||||||
if (sections.isEmpty()) {
|
if (sections.isEmpty()) {
|
||||||
idx = 0;
|
final Vector start = blockSpawn.clone().add(new Vector(0, pointType.heightOffset, 0));
|
||||||
section = new ElytraSection(blockSpawn.clone().add(new Vector(0, pointType.heightOffset, 0)), random,
|
final ElytraSection section = new ElytraSection(start, random, ElytraKnotDirector.DEFAULT_VERTICAL_BIAS);
|
||||||
ElytraKnotDirector.DEFAULT_VERTICAL_BIAS);
|
sections.put(0, section);
|
||||||
} else {
|
blockChanger.queue(section.generateBlocks(pointType, radius));
|
||||||
final int lastIdx = sections.keySet().stream().max(Integer::compareTo).orElse(0);
|
return;
|
||||||
final ElytraSection previous = sections.get(lastIdx);
|
|
||||||
final int bias = previous.end().getY() < ASCEND_THRESHOLD_Y ? ElytraKnotDirector.ASCENDING_VERTICAL_BIAS
|
|
||||||
: ElytraKnotDirector.DEFAULT_VERTICAL_BIAS;
|
|
||||||
idx = lastIdx + 1;
|
|
||||||
section = new ElytraSection(previous.end(), random, bias);
|
|
||||||
}
|
}
|
||||||
sections.put(idx, section);
|
final int lastIdx = sections.keySet().stream().max(Integer::compareTo).orElse(0);
|
||||||
|
final ElytraSection previous = sections.get(lastIdx);
|
||||||
|
final int bias = previous.end().getY() < ASCEND_THRESHOLD_Y ? ElytraKnotDirector.ASCENDING_VERTICAL_BIAS
|
||||||
|
: ElytraKnotDirector.DEFAULT_VERTICAL_BIAS;
|
||||||
|
final ElytraSection section = new ElytraSection(previous.end(), random, bias);
|
||||||
|
sections.put(lastIdx + 1, section);
|
||||||
blockChanger.queue(section.generateBlocks(pointType, radius));
|
blockChanger.queue(section.generateBlocks(pointType, radius));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,8 +116,8 @@ public class ElytraGenerator extends ParkourGenerator {
|
||||||
@Override
|
@Override
|
||||||
public void reset(boolean regenerate) {
|
public void reset(boolean regenerate) {
|
||||||
final Player player = getPlayer();
|
final Player player = getPlayer();
|
||||||
for (final ElytraSection section : sections.values()) {
|
if (player != null) {
|
||||||
if (player != null) {
|
for (final ElytraSection section : sections.values()) {
|
||||||
section.clear(player);
|
section.clear(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,16 +144,13 @@ public class ElytraGenerator extends ParkourGenerator {
|
||||||
return (int) Math.round(movementScore);
|
return (int) Math.round(movementScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Player getPlayer() {
|
protected Player getPlayer() {
|
||||||
return session.getPlayers().isEmpty() ? null : session.getPlayers().get(0).player;
|
return session.getPlayers().isEmpty() ? null : session.getPlayers().get(0).player;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeOldest(Player player) {
|
private void removeOldest(Player player) {
|
||||||
if (sections.size() <= 2) {
|
if (sections.size() > 2) {
|
||||||
return;
|
final ElytraSection old = sections.remove(sections.keySet().stream().min(Integer::compareTo).orElse(-1));
|
||||||
}
|
|
||||||
final ElytraSection old = sections.remove(sections.keySet().stream().min(Integer::compareTo).orElse(-1));
|
|
||||||
if (old != null) {
|
|
||||||
old.clear(player);
|
old.clear(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package dev.loki.loparkour.elytra.generator;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode that requires sustained minimum speed and scores horizontal distance covered.
|
||||||
|
*/
|
||||||
|
public class MinSpeedGenerator extends ElytraGenerator {
|
||||||
|
|
||||||
|
private static final double TICKS_PER_SECOND = 20.0;
|
||||||
|
private static final double MIN_SPEED = Config.GENERATION.getDouble("mode-settings.min-speed.min-speed", 0.9);
|
||||||
|
private static final int TOO_SLOW_LIMIT = 6;
|
||||||
|
|
||||||
|
private int startX = 0;
|
||||||
|
private double maxSpeed = 0;
|
||||||
|
private int ticksTooSlow = 0;
|
||||||
|
|
||||||
|
public MinSpeedGenerator(Session session) {
|
||||||
|
super(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
super.tick();
|
||||||
|
final Player player = getPlayer();
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final double speed = player.getVelocity().clone().setY(0).length() * TICKS_PER_SECOND;
|
||||||
|
maxSpeed = Math.max(maxSpeed, speed);
|
||||||
|
if (speed > MIN_SPEED && startX == 0) {
|
||||||
|
startX = player.getLocation().getBlockX();
|
||||||
|
}
|
||||||
|
if (MIN_SPEED > speed && maxSpeed > MIN_SPEED) {
|
||||||
|
ticksTooSlow++;
|
||||||
|
}
|
||||||
|
if (ticksTooSlow >= TOO_SLOW_LIMIT) {
|
||||||
|
reset(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(boolean regenerate) {
|
||||||
|
super.reset(regenerate);
|
||||||
|
startX = 0;
|
||||||
|
maxSpeed = 0;
|
||||||
|
ticksTooSlow = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getElytraScore() {
|
||||||
|
final Player player = getPlayer();
|
||||||
|
if (startX == 0 || player == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(0, (int) Math.round(player.getLocation().getX() - startX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package dev.loki.loparkour.elytra.generator;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode that scores the highest gliding speed reached during the run.
|
||||||
|
*/
|
||||||
|
public class SpeedDemonGenerator extends ElytraGenerator {
|
||||||
|
|
||||||
|
private static final double TICKS_PER_SECOND = 20.0;
|
||||||
|
|
||||||
|
private double maxSpeed = 0;
|
||||||
|
|
||||||
|
public SpeedDemonGenerator(Session session) {
|
||||||
|
super(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
super.tick();
|
||||||
|
final Player player = getPlayer();
|
||||||
|
if (player != null && player.isGliding() && player.getVelocity().getX() > 0) {
|
||||||
|
maxSpeed = Math.max(maxSpeed, player.getVelocity().clone().setY(0).length() * TICKS_PER_SECOND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(boolean regenerate) {
|
||||||
|
super.reset(regenerate);
|
||||||
|
maxSpeed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getElytraScore() {
|
||||||
|
return (int) Math.round(maxSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.loki.loparkour.elytra.generator;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode that restarts the course once the target score is reached.
|
||||||
|
*/
|
||||||
|
public class TimeTrialGenerator extends ElytraGenerator {
|
||||||
|
|
||||||
|
private static final int SCORE = Config.GENERATION.getInt("mode-settings.time-trial.score", 500);
|
||||||
|
|
||||||
|
public TimeTrialGenerator(Session session) {
|
||||||
|
super(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
super.tick();
|
||||||
|
if (getElytraScore() >= SCORE) {
|
||||||
|
reset(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/java/dev/loki/loparkour/elytra/mode/CloseMode.java
Normal file
55
src/main/java/dev/loki/loparkour/elytra/mode/CloseMode.java
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.loki.loparkour.elytra.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.elytra.generator.CloseGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
|
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import dev.loki.loparkour.util.item.Item;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode with flat rings placed close together.
|
||||||
|
*/
|
||||||
|
public class CloseMode implements Mode {
|
||||||
|
|
||||||
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getName() {
|
||||||
|
return "close";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Item getItem(String locale) {
|
||||||
|
return Locales.getItem(locale, "play.elytra.close");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Leaderboard getLeaderboard() {
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(Player player) {
|
||||||
|
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||||
|
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof CloseMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.closeInventory();
|
||||||
|
|
||||||
|
Session.create(CloseGenerator::new, null, null, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.loki.loparkour.elytra.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.elytra.generator.MinSpeedGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
|
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import dev.loki.loparkour.util.item.Item;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode scoring distance covered while keeping a minimum speed.
|
||||||
|
*/
|
||||||
|
public class MinSpeedMode implements Mode {
|
||||||
|
|
||||||
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getName() {
|
||||||
|
return "minspeed";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Item getItem(String locale) {
|
||||||
|
return Locales.getItem(locale, "play.elytra.minspeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Leaderboard getLeaderboard() {
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(Player player) {
|
||||||
|
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||||
|
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof MinSpeedMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.closeInventory();
|
||||||
|
|
||||||
|
Session.create(MinSpeedGenerator::new, null, null, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.loki.loparkour.elytra.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.elytra.generator.SpeedDemonGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
|
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import dev.loki.loparkour.util.item.Item;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode scoring the highest gliding speed reached.
|
||||||
|
*/
|
||||||
|
public class SpeedDemonMode implements Mode {
|
||||||
|
|
||||||
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getName() {
|
||||||
|
return "speeddemon";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Item getItem(String locale) {
|
||||||
|
return Locales.getItem(locale, "play.elytra.speeddemon");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Leaderboard getLeaderboard() {
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(Player player) {
|
||||||
|
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||||
|
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof SpeedDemonMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.closeInventory();
|
||||||
|
|
||||||
|
Session.create(SpeedDemonGenerator::new, null, null, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.loki.loparkour.elytra.mode;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.elytra.generator.TimeTrialGenerator;
|
||||||
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
|
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import dev.loki.loparkour.util.item.Item;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elytra mode that resets the course after the target score is reached.
|
||||||
|
*/
|
||||||
|
public class TimeTrialMode implements Mode {
|
||||||
|
|
||||||
|
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getName() {
|
||||||
|
return "timetrial";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Item getItem(String locale) {
|
||||||
|
return Locales.getItem(locale, "play.elytra.timetrial");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Leaderboard getLeaderboard() {
|
||||||
|
return leaderboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(Player player) {
|
||||||
|
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||||
|
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof TimeTrialMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.closeInventory();
|
||||||
|
|
||||||
|
Session.create(TimeTrialGenerator::new, null, null, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||||
import dev.loki.loparkour.session.core.Session;
|
import dev.loki.loparkour.session.core.Session;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
|
@ -152,6 +153,14 @@ public class ParkourGenerator {
|
||||||
return state.history.get(state.history.size() - 1);
|
return state.history.get(state.history.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for modes to force a fixed block material on the whole course
|
||||||
|
* (e.g. a per-player color in duels), or {@code null} to use the style.
|
||||||
|
*/
|
||||||
|
public @Nullable BlockData selectBlockData() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Event listener management
|
// Event listener management
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ public class BlockSelector {
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public BlockData selectBlockData() {
|
public BlockData selectBlockData() {
|
||||||
|
final BlockData forced = generator.selectBlockData();
|
||||||
|
if (forced != null) {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
final Style style = Registry.getStyle(generator.profile.get("style").value());
|
final Style style = Registry.getStyle(generator.profile.get("style").value());
|
||||||
if (style == null) {
|
if (style == null) {
|
||||||
final List<Style> styles = Registry.getStyles();
|
final List<Style> styles = Registry.getStyles();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ package dev.loki.loparkour.mode.base;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.core.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.duels.DuelsMode;
|
import dev.loki.loparkour.duels.DuelsMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.CloseMode;
|
||||||
import dev.loki.loparkour.elytra.mode.ElytraMode;
|
import dev.loki.loparkour.elytra.mode.ElytraMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.MinSpeedMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.SpeedDemonMode;
|
||||||
|
import dev.loki.loparkour.elytra.mode.TimeTrialMode;
|
||||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||||
import dev.loki.loparkour.mode.impl.multi.RaceMode;
|
import dev.loki.loparkour.mode.impl.multi.RaceMode;
|
||||||
|
|
@ -24,6 +28,10 @@ public final class Modes {
|
||||||
public static CoopMode COOP;
|
public static CoopMode COOP;
|
||||||
public static GravityShiftMode GRAVITY_SHIFT;
|
public static GravityShiftMode GRAVITY_SHIFT;
|
||||||
public static ElytraMode ELYTRA;
|
public static ElytraMode ELYTRA;
|
||||||
|
public static SpeedDemonMode SPEED_DEMON;
|
||||||
|
public static MinSpeedMode MIN_SPEED;
|
||||||
|
public static TimeTrialMode TIME_TRIAL;
|
||||||
|
public static CloseMode CLOSE;
|
||||||
public static DuelsMode DUELS;
|
public static DuelsMode DUELS;
|
||||||
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
|
|
@ -35,6 +43,10 @@ public final class Modes {
|
||||||
COOP = (CoopMode) Registry.getMode("coop");
|
COOP = (CoopMode) Registry.getMode("coop");
|
||||||
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
||||||
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
||||||
|
SPEED_DEMON = (SpeedDemonMode) Registry.getMode("speeddemon");
|
||||||
|
MIN_SPEED = (MinSpeedMode) Registry.getMode("minspeed");
|
||||||
|
TIME_TRIAL = (TimeTrialMode) Registry.getMode("timetrial");
|
||||||
|
CLOSE = (CloseMode) Registry.getMode("close");
|
||||||
DUELS = (DuelsMode) Registry.getMode("duels");
|
DUELS = (DuelsMode) Registry.getMode("duels");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,13 @@ modes:
|
||||||
goal: 50
|
goal: 50
|
||||||
island-distance: 20
|
island-distance: 20
|
||||||
|
|
||||||
|
# -= Elytra mode options =-
|
||||||
|
mode-settings:
|
||||||
|
min-speed:
|
||||||
|
min-speed: 0.9
|
||||||
|
time-trial:
|
||||||
|
score: 500
|
||||||
|
|
||||||
# -= Scoring options =-
|
# -= Scoring options =-
|
||||||
scoring:
|
scoring:
|
||||||
all-points: false
|
all-points: false
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,26 @@ play:
|
||||||
name: "�D4FF&lElytra"
|
name: "�D4FF&lElytra"
|
||||||
lore: "񢩈Fly through rings||񢩈on your elytra."
|
lore: "񢩈Fly through rings||񢩈on your elytra."
|
||||||
|
|
||||||
|
speeddemon:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lSpeed Demon"
|
||||||
|
lore: "񢩈Reach the highest speed||񢩈possible on your elytra."
|
||||||
|
|
||||||
|
minspeed:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lMin Speed"
|
||||||
|
lore: "񢩈Keep above the minimum speed||񢩈to fly as far as possible."
|
||||||
|
|
||||||
|
timetrial:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lTime Trial"
|
||||||
|
lore: "񢩈Reach the target score||񢩈as fast as you can."
|
||||||
|
|
||||||
|
close:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lClose"
|
||||||
|
lore: "񢩈Fly through flat rings||񢩈placed close together."
|
||||||
|
|
||||||
spectator:
|
spectator:
|
||||||
name: "&#FFFFFFSpectator"
|
name: "&#FFFFFFSpectator"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,26 @@ play:
|
||||||
name: "�D4FF&lЭлитры"
|
name: "�D4FF&lЭлитры"
|
||||||
lore: "񢩈Летай сквозь кольца||񢩈на элитрах."
|
lore: "񢩈Летай сквозь кольца||񢩈на элитрах."
|
||||||
|
|
||||||
|
speeddemon:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lСкорость-демон"
|
||||||
|
lore: "񢩈Разгонись до максимальной||񢩈скорости на элитрах."
|
||||||
|
|
||||||
|
minspeed:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lМин. скорость"
|
||||||
|
lore: "񢩈Держи минимальную скорость||񢩈и лети как можно дальше."
|
||||||
|
|
||||||
|
timetrial:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lГонка на время"
|
||||||
|
lore: "񢩈Достигни целевого счёта||񢩈как можно быстрее."
|
||||||
|
|
||||||
|
close:
|
||||||
|
material: "elytra"
|
||||||
|
name: "�D4FF&lБлизко"
|
||||||
|
lore: "񢩈Летай сквозь плоские кольца||񢩈расположенные вплотную."
|
||||||
|
|
||||||
spectator:
|
spectator:
|
||||||
name: "&#FFFFFFНаблюдатель"
|
name: "&#FFFFFFНаблюдатель"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue