diff --git a/TODO.md b/TODO.md index 05b04b7..1504d03 100644 --- a/TODO.md +++ b/TODO.md @@ -86,14 +86,22 @@ - **47. `UserRegistry.getUsers()` строит полный Set на каждый BlockBreak/Place/Drop/Damage** (UserRegistry.java:94-98) → O(n) аллокаций на горячем пути. → Кэш `Map` + привязка через 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` зелёный. --- diff --git a/src/main/java/dev/loki/loparkour/elytra/close/CloseGenerator.java b/src/main/java/dev/loki/loparkour/elytra/close/CloseGenerator.java index ee253af..dc1ac58 100644 --- a/src/main/java/dev/loki/loparkour/elytra/close/CloseGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/close/CloseGenerator.java @@ -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()); } } diff --git a/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java b/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java index 767e88f..be218f9 100644 --- a/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/core/ElytraGenerator.java @@ -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() { diff --git a/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedGenerator.java b/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedGenerator.java index 8514849..ea32e3c 100644 --- a/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/minspeed/MinSpeedGenerator.java @@ -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 diff --git a/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java b/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java index c01e129..edc3913 100644 --- a/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/obstacle/ObstacleGenerator.java @@ -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> 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 points = obstacle.getPoints(section.getKnot(knotIdx), obstacleRadius); + final List points = obstacle.getPoints(section.getKnot(knotIdx), radius()); if (points.isEmpty()) { return; } diff --git a/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonGenerator.java b/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonGenerator.java index cf92499..a54949c 100644 --- a/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/speeddemon/SpeedDemonGenerator.java @@ -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 diff --git a/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialGenerator.java b/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialGenerator.java index e659844..87b0389 100644 --- a/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialGenerator.java +++ b/src/main/java/dev/loki/loparkour/elytra/timetrial/TimeTrialGenerator.java @@ -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 diff --git a/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java b/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java index 09b7f6a..ea221cf 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java @@ -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 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); + } } diff --git a/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java b/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java index 12b5a5f..8cf170b 100644 --- a/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java +++ b/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java @@ -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; } diff --git a/src/main/java/dev/loki/loparkour/session/core/Session.java b/src/main/java/dev/loki/loparkour/session/core/Session.java index b0024f7..2b020be 100644 --- a/src/main/java/dev/loki/loparkour/session/core/Session.java +++ b/src/main/java/dev/loki/loparkour/session/core/Session.java @@ -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]; + } } } diff --git a/src/test/java/dev/loki/loparkour/session/core/ChatTypeTest.java b/src/test/java/dev/loki/loparkour/session/core/ChatTypeTest.java new file mode 100644 index 0000000..054eda8 --- /dev/null +++ b/src/test/java/dev/loki/loparkour/session/core/ChatTypeTest.java @@ -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); + } +}