fix(elytra,chat): make elytra sub-modes report their mode and wire the chat-scope toggle
- ElytraGenerator no longer hardcodes Modes.elytra(): the mode is passed through the constructor (canonical (session, pointType, mode) plus two convenience overloads); CloseGenerator/MinSpeed/Obstacle/SpeedDemon/ TimeTrial now declare their own mode, so getMode() is reachable - Session.ChatType gets next(); ParkourUser gets setChatType(); SettingsMenu gains a chat-scope cycle button (slot 15) so LOBBY_ONLY/PLAYERS_ONLY are no longer dead branches - ChatTypeTest covers the cycle order and wrap-around - TODO.md: PROGRESS-8 records both fixes checkstyle 0 error / 135 warning, PMD 0, tests 96/0 fail (1 skipped)
This commit is contained in:
parent
02e2947f47
commit
8ff02cff18
11 changed files with 122 additions and 17 deletions
38
TODO.md
38
TODO.md
|
|
@ -86,14 +86,22 @@
|
|||
- **47. `UserRegistry.getUsers()` строит полный Set на каждый BlockBreak/Place/Drop/Damage** (UserRegistry.java:94-98) → O(n) аллокаций на горячем пути. → Кэш `Map<UUID, ParkourUser>` + привязка через PersistentDataContainer или entity.
|
||||
- **48. `sendTranslated(...).formatted()` бросает IllegalFormatException при `%` в нике/сообщении** (ParkourUser.java:102, Locales.java:76-82). → Обернуть в try или использовать replace-based формат.
|
||||
- **49. `getJoinCount` — int между main и async (bStats)** (UserRegistry.java:100-104). → AtomicInteger.
|
||||
- **168. `ParkourUser.chatType` — состояние без записи.** Поле инициализировано `PUBLIC`, читается в `ParkourRestrictionListener.onChat` (switch `chatType()`), но сеттера нет нигде → ветки PRIVATE/GUILD недостижимы, режим группового чата не работает. → Либо подключить переключатель (команда/меню), либо убрать switch и мёртвый enum.
|
||||
- **168. ✅ ИСПРАВЛЕНО (2026-10-06, см. ПРОГРЕСС-8): `ParkourUser.chatType` — состояние без записи.** Поле инициализировано
|
||||
`PUBLIC`, читалось в `ParkourRestrictionListener.onChat` (switch `chatType()`), но сеттера не было нигде → ветки
|
||||
`LOBBY_ONLY`/`PLAYERS_ONLY` были недостижимы. Оказалось, фича спроектирована, но не подключена: в `en.yml`/`ru.yml`
|
||||
уже лежал готовый блок `settings.chat` (material/name/lore/values + formats), а `ParkourOption.CHAT("settings.chat", …)`
|
||||
уже был в реестре опций. → Кнопка-цикл в `SettingsMenu` (слот 15) + `ParkourUser.setChatType(...)` + `ChatType.next()`.
|
||||
|
||||
### Modes
|
||||
|
||||
- **50. `GravityShiftMode` не снимает эффекты при выходе игрока** (только в reset для присутствующих, GravityShiftMode.java:149-163). → Снимать в unregister.
|
||||
- **51. `RaceMode.finishRace` — `reset(false)` оставляет игрока в воздухе** (RaceMode.java:142-147; GeneratorCleanup.reset чистит курс без регенерации/телепорта). → Телепорт в лобби + регенерация старта.
|
||||
- **52. `SpeedrunMode` оставляет удалённые блоки в `history`** → ломает координатные поиски. → Удалять координату из history при снятии блока.
|
||||
- **167. Elytra-под-режимы не отличаются от базового.** `ElytraGenerator.getMode()` возвращает `Modes.elytra()`, а 5 под-генераторов (`Close/MinSpeed/TimeTrial/SpeedDemon/ObstacleGenerator`) его не переопределяют → guard `getMode() instanceof CloseMode` в `create()` (строка 47 во всех 5 режимах) **никогда не истинен**: повторная команда в том же режиме не делает early-return, а пересоздаёт сессию. Геттеры `Modes.speedDemon()/minSpeed()/timeTrial()/close()/obstacle()` не вызываются нигде (5 статиков только пишутся). → Переопределить `getMode()` в под-генераторах (или сравнивать класс генератора) и использовать/удалить геттеры.
|
||||
- **167. ✅ ИСПРАВЛЕНО (2026-10-06, см. ПРОГРЕСС-8): Elytra-под-режимы не отличались от базового.**
|
||||
`ElytraGenerator.getMode()` возвращал `Modes.elytra()`, а 5 под-генераторов (`Close/MinSpeed/TimeTrial/SpeedDemon/ObstacleGenerator`)
|
||||
его не переопределяли → guard `getMode() instanceof CloseMode` в `create()` **никогда не был истинен**: повторная команда
|
||||
в том же режиме не делала early-return, а пересоздавала сессию. Геттеры `Modes.speedDemon()/minSpeed()/timeTrial()/close()/obstacle()`
|
||||
не вызывались нигде. → режим передаётся в конструктор `ElytraGenerator`, `getMode()` отдаёт его.
|
||||
|
||||
### Схематики (жизнь в игре)
|
||||
|
||||
|
|
@ -549,6 +557,32 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар
|
|||
> Урок на будущее: после любой отменённой пачки правок сверять `git status` + полный `check`, а не только счётчики.
|
||||
> - **Остаток бэклога (135):** `ReturnCount` 69, `IllegalCatch` 23, `ClassFanOutComplexity` 17, `NPathComplexity` 12,
|
||||
> `ParameterNumber` 8, `MethodLength` 4, `ClassDataAbstractionCoupling` 2.
|
||||
>
|
||||
> ## ✅ ПРОГРЕСС-8 (Задача 2: починены реальные баги 167 и 168, 2026-10-06)
|
||||
> По решению владельца вместо механической зачистки варнингов взяты два мёртвых куска логики.
|
||||
> - **167 — elytra-под-режимы научились представляться.** `ElytraGenerator` больше не возвращает хардкод
|
||||
> `Modes.elytra()`: режим передаётся в конструктор (`private final Mode mode`, канонический конструктор
|
||||
> `(session, pointType, mode)` + two convenience-перегрузки), `getMode()` отдаёт его. Под-генераторы
|
||||
> объявляют свою идентичность одной строкой `super(session, Modes.speedDemon())` — вместо пяти копий `getMode()`
|
||||
> (у `ObstacleGenerator` не хватило бы строк: лимит FileLength = 200, а файл — 199).
|
||||
> Попутно удалено дублирование: `ObstacleGenerator` заново читал `elytra.radius` и держал свой `DEFAULT_RADIUS = 3` —
|
||||
> теперь общий `radius()` из родителя (199 → 194 строки).
|
||||
> - **Что реально поменялось для игрока (следствие, а не побочка):**
|
||||
> 1) guard в `create()` стал достижим → повторная `/parkour speeddemon` в том же режиме больше не пересоздаёт сессию;
|
||||
> 2) обратная сторона — `/parkour elytra` внутри под-режима раньше **не работал** (под-генератор выдавал себя за
|
||||
> `ElytraMode`, `instanceof ElytraMode` был истинен и делал early-return), теперь переключает;
|
||||
> 3) очки, rewards (`%mode:`-префикс в `rewards-v2.yml`), голограммы и PAPI теперь попадают в **свой** лидерборд
|
||||
> под-режима, а не в общий `elytra`. ⚠️ Старые записи в `leaderboards/elytra.json`/SQL не теряются, но
|
||||
> пер-режимные доски начинаются с нуля; реальных прогонов на сервере не было (только `check`).
|
||||
> - **168 — переключатель области чата подключён к меню.** Фича была спроектирована, но не собрана: блоки
|
||||
> `settings.chat` (material/name/lore/values + formats) есть и в `en.yml`, и в `ru.yml`, `ParkourOption.CHAT` зарегистрирован,
|
||||
> `ParkourRestrictionListener.onChat` уже умеет маршрутизировать `LOBBY_ONLY`/`PLAYERS_ONLY`, но кнопка не
|
||||
> создавалась, сеттера не было, и `chatType` навечно оставался `PUBLIC`.
|
||||
> Сделано: `Session.ChatType.next()` (цикл по порядку объявления = порядку `settings.chat.values`),
|
||||
> `ParkourUser.setChatType(...)`, кнопка в `SettingsMenu` (слот 15, gate `ParkourOption.CHAT.mayPerform`) по образцу
|
||||
> `LobbyMenu.visibilityItem` — клик меняет область и перерисовывает меню. Новатье — `ChatTypeTest` (2 теста).
|
||||
> - **Счётчики:** checkstyle **0 error / 135 warning** (тот же бэклог, что в ПРОГРЕСС-7 — баги чинились без
|
||||
> новых варнингов), PMD **0**, тесты **96 / 0 fail (1 skipped)**, `./gradlew check` зелёный.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dev.loki.loparkour.elytra.close;
|
|||
|
||||
import dev.loki.loparkour.elytra.core.ElytraGenerator;
|
||||
import dev.loki.loparkour.elytra.section.ElytraPointType;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
|
||||
/**
|
||||
|
|
@ -10,6 +11,6 @@ import dev.loki.loparkour.session.core.Session;
|
|||
public class CloseGenerator extends ElytraGenerator {
|
||||
|
||||
public CloseGenerator(Session session) {
|
||||
super(session, ElytraPointType.FLAT);
|
||||
super(session, ElytraPointType.FLAT, Modes.close());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,17 +31,23 @@ public class ElytraGenerator extends ParkourGenerator {
|
|||
private Vector blockSpawn;
|
||||
private Random random;
|
||||
protected final ElytraPointType pointType;
|
||||
private final Mode mode;
|
||||
private final int radius;
|
||||
private final Material material;
|
||||
private final ElytraBoost boost = new ElytraBoost();
|
||||
|
||||
public ElytraGenerator(Session session) {
|
||||
this(session, ElytraPointType.CIRCLE);
|
||||
this(session, ElytraPointType.CIRCLE, Modes.elytra());
|
||||
}
|
||||
|
||||
protected ElytraGenerator(Session session, ElytraPointType pointType) {
|
||||
protected ElytraGenerator(Session session, Mode mode) {
|
||||
this(session, ElytraPointType.CIRCLE, mode);
|
||||
}
|
||||
|
||||
protected ElytraGenerator(Session session, ElytraPointType pointType, Mode mode) {
|
||||
super(session);
|
||||
this.pointType = pointType;
|
||||
this.mode = mode;
|
||||
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);
|
||||
}
|
||||
|
|
@ -133,7 +139,12 @@ public class ElytraGenerator extends ParkourGenerator {
|
|||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.elytra();
|
||||
return mode;
|
||||
}
|
||||
|
||||
/** The configured {@code elytra.radius}. Sub-modes build their own geometry on top of the same value. */
|
||||
protected int radius() {
|
||||
return radius;
|
||||
}
|
||||
|
||||
public int getElytraScore() {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dev.loki.loparkour.elytra.minspeed;
|
|||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.elytra.core.ElytraGenerator;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ public class MinSpeedGenerator extends ElytraGenerator {
|
|||
private int ticksTooSlow = 0;
|
||||
|
||||
public MinSpeedGenerator(Session session) {
|
||||
super(session);
|
||||
super(session, Modes.minSpeed());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.elytra.obstacle;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.elytra.core.ElytraGenerator;
|
||||
import dev.loki.loparkour.elytra.section.ElytraPointType;
|
||||
import dev.loki.loparkour.elytra.section.ElytraSection;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -22,16 +22,13 @@ import java.util.concurrent.ThreadLocalRandom;
|
|||
*/
|
||||
public class ObstacleGenerator extends ElytraGenerator {
|
||||
|
||||
private static final int DEFAULT_RADIUS = 3;
|
||||
private static final int OBSTACLE_STEP = 3;
|
||||
private static final int OBSTACLE_KNOT = 3;
|
||||
|
||||
private final Map<Integer, List<Vector>> obstacles = new HashMap<>();
|
||||
private final int obstacleRadius;
|
||||
|
||||
public ObstacleGenerator(Session session) {
|
||||
super(session);
|
||||
obstacleRadius = Config.GENERATION.getInt("elytra.radius", DEFAULT_RADIUS);
|
||||
super(session, Modes.obstacle());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -48,7 +45,7 @@ public class ObstacleGenerator extends ElytraGenerator {
|
|||
|
||||
private void generateObstacle(int idx, ElytraSection section, int knotIdx) {
|
||||
final Obstacle obstacle = Obstacle.values()[ThreadLocalRandom.current().nextInt(Obstacle.values().length)];
|
||||
final List<Vector> points = obstacle.getPoints(section.getKnot(knotIdx), obstacleRadius);
|
||||
final List<Vector> points = obstacle.getPoints(section.getKnot(knotIdx), radius());
|
||||
if (points.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.loparkour.elytra.speeddemon;
|
||||
|
||||
import dev.loki.loparkour.elytra.core.ElytraGenerator;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ public class SpeedDemonGenerator extends ElytraGenerator {
|
|||
private double maxSpeed = 0;
|
||||
|
||||
public SpeedDemonGenerator(Session session) {
|
||||
super(session);
|
||||
super(session, Modes.speedDemon());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dev.loki.loparkour.elytra.timetrial;
|
|||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.elytra.core.ElytraGenerator;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
|
||||
/**
|
||||
|
|
@ -12,7 +13,7 @@ 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);
|
||||
super(session, Modes.timeTrial());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -8,14 +8,19 @@ import dev.loki.loparkour.player.core.ParkourPlayer;
|
|||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.menu.gui.inventory.InventoryGui;
|
||||
import dev.loki.loparkour.player.service.UserRegistry;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SettingsMenu extends LPMenu {
|
||||
|
||||
private static final int ROWS = 3;
|
||||
private static final int SLOT_PARKOUR_SETTINGS = 11;
|
||||
private static final int SLOT_LANG = 13;
|
||||
private static final int SLOT_CHAT = 15;
|
||||
private static final int SLOT_CLOSE = 22;
|
||||
|
||||
@Override
|
||||
|
|
@ -44,7 +49,24 @@ public class SettingsMenu extends LPMenu {
|
|||
});
|
||||
}
|
||||
|
||||
final ParkourUser user = ParkourUser.getUser(player);
|
||||
if (ParkourOption.CHAT.mayPerform(player) && user != null) {
|
||||
gui = gui.setItem(SLOT_CHAT, chatItem(user), e -> {
|
||||
user.setChatType(user.chatType().next());
|
||||
open(player);
|
||||
});
|
||||
}
|
||||
|
||||
gui.setItem(SLOT_CLOSE, closeButton(player), e -> player.closeInventory())
|
||||
.open(player);
|
||||
}
|
||||
|
||||
/** Scope button — the locale lists its values in {@link Session.ChatType#next()} order. */
|
||||
private ItemStack chatItem(@NotNull ParkourUser user) {
|
||||
final String locale = user.locale();
|
||||
final List<String> values = Locales.getStringList(locale, "settings.chat.values");
|
||||
final Session.ChatType chatType = user.chatType();
|
||||
final String label = chatType.ordinal() < values.size() ? values.get(chatType.ordinal()) : chatType.name();
|
||||
return localeItem(locale, "settings.chat", label);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,10 +113,15 @@ public abstract class ParkourUser {
|
|||
return previousData;
|
||||
}
|
||||
|
||||
public Session.ChatType chatType() {
|
||||
public @NotNull Session.ChatType chatType() {
|
||||
return chatType;
|
||||
}
|
||||
|
||||
/** Who this player's messages are routed to while inside a parkour session. */
|
||||
public void setChatType(@NotNull Session.ChatType chatType) {
|
||||
this.chatType = chatType;
|
||||
}
|
||||
|
||||
public boolean unregistering() {
|
||||
return unregistering;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,6 +161,11 @@ public final class Session {
|
|||
}
|
||||
|
||||
public enum ChatType {
|
||||
LOBBY_ONLY, PLAYERS_ONLY, PUBLIC
|
||||
LOBBY_ONLY, PLAYERS_ONLY, PUBLIC;
|
||||
|
||||
/** Scope the chat button switches to; order matches {@code settings.chat.values} in the locales. */
|
||||
public ChatType next() {
|
||||
return values()[(ordinal() + 1) % values().length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.loparkour.session.core;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
/**
|
||||
* The chat scope button cycles {@link Session.ChatType}; the locales list their labels in this order.
|
||||
*/
|
||||
class ChatTypeTest {
|
||||
|
||||
@Test
|
||||
void cyclesThroughEveryScopeInDeclaredOrder() {
|
||||
assertSame(Session.ChatType.PLAYERS_ONLY, Session.ChatType.LOBBY_ONLY.next());
|
||||
assertSame(Session.ChatType.PUBLIC, Session.ChatType.PLAYERS_ONLY.next());
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapsBackToTheFirstScope() {
|
||||
Session.ChatType type = Session.ChatType.PUBLIC;
|
||||
for (int click = 0; click < Session.ChatType.values().length; click++) {
|
||||
type = type.next();
|
||||
}
|
||||
assertEquals(Session.ChatType.PUBLIC, type);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue