From fd0ba2cc0ef8c9d6bc558d4c07d0737c7e90cf40 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 1 Jun 2026 14:02:48 +0200 Subject: [PATCH] refactor: reorganize packages and replace LPSchematic with WorldEdit Restructure core modules into subpackages (generator, schematic, config, session, etc.); add .schem/.nbt schematic pipeline, commands, adaptive stats skeleton, and bootstrap wiring. --- .gitignore | 2 +- TODO.md | 468 ++++++++++-------- build.gradle.kts | 11 +- docs/SCHEMATICS.md | 79 +++ .../java/dev/loki/loparkour/LoParkour.java | 174 ++----- .../adaptive/bootstrap/AdaptiveServices.java | 172 +++++++ .../adaptive/core/AdaptiveDifficulty.java | 90 ++++ .../adaptive/core/DifficultyAdjuster.java | 182 +++++++ .../adaptive/core/DifficultyCalculator.java | 141 ++++++ .../adaptive/core/DifficultyWeights.java | 98 ++++ .../adaptive/core/MetricsCollector.java | 214 ++++++++ .../adaptive/core/SkillAnalyzer.java | 137 +++++ .../adaptive/model/AdaptiveConfig.java | 158 ++++++ .../adaptive/model/PlayerMetrics.java | 159 ++++++ .../loparkour/adaptive/model/SkillRating.java | 133 +++++ .../adaptive/storage/FileStatsStorage.java | 185 +++++++ .../adaptive/storage/NoOpStatsRepository.java | 60 +++ .../adaptive/storage/SQLStatsStorage.java | 173 +++++++ .../adaptive/storage/StatsRepository.java | 76 +++ .../loparkour/api/{ => core}/ParkourAPI.java | 8 +- .../loparkour/api/{ => core}/Registry.java | 6 +- .../api/event/ParkourBlockGenerateEvent.java | 4 +- .../loparkour/api/event/ParkourFallEvent.java | 2 +- .../loparkour/api/event/ParkourJoinEvent.java | 2 +- .../api/event/ParkourLeaveEvent.java | 2 +- .../event/ParkourSchematicGenerateEvent.java | 15 +- .../api/event/ParkourScoreEvent.java | 2 +- .../api/event/ParkourSpectateEvent.java | 2 +- .../loparkour/bootstrap/PluginBootstrap.java | 133 +++++ .../{ => command}/LoParkourCommand.java | 113 ++++- .../command/SchematicCommandHandler.java | 172 ------- .../{ => admin}/AdminCommandHandler.java | 46 +- .../{ => player}/PlayerCommandHandler.java | 57 ++- .../schematic/SchematicCommandHandler.java | 271 ++++++++++ .../loparkour/config/{ => core}/Config.java | 4 +- .../config/{ => core}/ConfigAccessor.java | 4 +- .../config/{ => core}/ConfigLoader.java | 8 +- .../config/{ => core}/ConfigUpdater.java | 2 +- .../config/{ => locale}/LocaleCache.java | 2 +- .../config/{ => locale}/LocaleLoader.java | 2 +- .../config/{ => locale}/Locales.java | 39 +- .../config/{ => options}/Option.java | 13 +- .../config/options/OptionGeneral.java | 4 +- .../config/options/OptionGeneration.java | 2 +- .../config/options/OptionParticles.java | 6 +- .../loparkour/config/options/OptionSQL.java | 2 +- .../config/options/OptionStyles.java | 6 +- .../coordinator/GeneratorEventListener.java | 53 ++ .../coordinator}/GeneratorProfileManager.java | 131 ++--- .../coordinator}/GeneratorStatistics.java | 14 +- .../coordinator}/ParkourGenerator.java | 80 ++- .../{ => core/model}/GeneratorOption.java | 2 +- .../{ => core/model}/GeneratorState.java | 3 +- .../generator/{ => core/model}/Island.java | 19 +- .../generator/{ => core/model}/Profile.java | 2 +- .../generator/effect/EffectManager.java | 8 +- .../{ => calculation}/JumpCalculator.java | 11 +- .../jump/{ => calculation}/JumpDirector.java | 2 +- .../JumpOffsetGenerator.java | 4 +- .../jump/{ => calculation}/JumpType.java | 2 +- .../jump/{ => calculation}/JumpValidator.java | 2 +- .../jump/{ => placement}/BlockPlacer.java | 73 +-- .../jump/{ => placement}/BlockSelector.java | 11 +- .../{ => loop}/GeneratorLifecycle.java | 6 +- .../{ => loop}/LifecycleEventManager.java | 14 +- .../{ => loop}/LifecycleTickManager.java | 7 +- .../{ => loop}/LifecycleVisualUpdater.java | 6 +- .../{ => player}/GeneratorCleanup.java | 12 +- .../PlayerInteractionHandler.java | 53 +- .../profile/GenerationProfileLoader.java | 68 +++ .../ghost/{ => core}/GhostManager.java | 6 +- .../ghost/{ => core}/GhostPlayer.java | 4 +- .../ghost/{ => core}/GhostRecorder.java | 4 +- .../ghost/{ => model}/GhostData.java | 2 +- .../ghost/{ => model}/GhostFrame.java | 2 +- .../hook/{ => floodgate}/FloodgateHook.java | 2 +- .../loparkour/hook/{ => holo}/HoloHook.java | 12 +- .../loparkour/hook/{ => papi}/PAPIHook.java | 20 +- .../loparkour/hook/{ => vault}/VaultHook.java | 2 +- .../leaderboard/{ => core}/Leaderboard.java | 4 +- .../{ => core}/LeaderboardSorter.java | 3 +- .../leaderboard/{ => model}/Score.java | 2 +- .../{ => persistence}/LeaderboardStorage.java | 7 +- .../listener/SchematicWandListener.java | 57 --- .../ParkourRestrictionListener.java | 18 +- .../PlayerConnectionListener.java | 12 +- .../schematic/SchematicWandListener.java | 60 +++ .../menu/community/CommunityMenu.java | 8 +- .../menu/community/LeaderboardsMenu.java | 18 +- .../menu/community/SingleLeaderboardMenu.java | 18 +- .../menu/{ => core}/DynamicMenu.java | 8 +- .../loparkour/menu/{ => core}/LPMenu.java | 30 +- .../loparkour/menu/{ => core}/MainMenu.java | 10 +- .../loparkour/menu/{ => core}/MenuStub.java | 2 +- .../loki/loparkour/menu/{ => core}/Menus.java | 2 +- .../menu/{ => core}/ParkourOption.java | 6 +- .../loki/loparkour/menu/lobby/LobbyMenu.java | 14 +- .../menu/lobby/PlayerManagementMenu.java | 28 +- .../loki/loparkour/menu/play/PlayMenu.java | 8 +- .../loki/loparkour/menu/play/SingleMenu.java | 16 +- .../loparkour/menu/play/SpectatorMenu.java | 16 +- .../loparkour/menu/settings/LangMenu.java | 10 +- .../menu/settings/ParkourSettingsMenu.java | 18 +- .../loparkour/menu/settings/SettingsMenu.java | 12 +- .../loki/loparkour/mode/ElytraGenerator.java | 262 ---------- .../loki/loparkour/mode/{ => base}/Mode.java | 6 +- .../loparkour/mode/base/ModeMessages.java | 26 + .../loki/loparkour/mode/{ => base}/Modes.java | 5 +- .../loparkour/mode/{ => base}/MultiMode.java | 4 +- .../loparkour/mode/elytra/ElytraConfig.java | 2 +- .../mode/elytra/ElytraFireworkListener.java | 49 ++ .../mode/elytra/ElytraGenerator.java | 121 +++++ .../loki/loparkour/mode/elytra/ElytraHud.java | 29 ++ .../loparkour/mode/elytra/ElytraPhysics.java | 2 +- .../mode/elytra/ElytraPlayerKit.java | 53 ++ .../loparkour/mode/elytra/ElytraRingLoop.java | 87 ++++ .../loparkour/mode/elytra/ElytraScoring.java | 35 ++ .../loparkour/mode/{ => impl}/CoopMode.java | 34 +- .../mode/{ => impl}/DefaultMode.java | 21 +- .../loparkour/mode/{ => impl}/ElytraMode.java | 20 +- .../mode/{ => impl}/GravityShiftMode.java | 22 +- .../mode/{ => impl}/HardcoreMode.java | 22 +- .../loparkour/mode/{ => impl}/RaceMode.java | 48 +- .../mode/{ => impl}/SpectatorMode.java | 26 +- .../mode/{ => impl}/SpeedrunMode.java | 22 +- .../player/{ => core}/ParkourPlayer.java | 69 +-- .../player/{ => core}/ParkourUser.java | 21 +- .../loparkour/player/data/InventoryData.java | 2 +- .../loparkour/player/data/PreviousData.java | 6 +- .../player/{ => service}/BungeeUtil.java | 5 +- .../player/service/ParkourHotbar.java | 40 ++ .../{ => service}/PlayerSettingsManager.java | 7 +- .../{ => service}/ScoreboardManager.java | 14 +- .../player/{ => service}/UserRegistry.java | 18 +- .../{ => spectator}/ParkourSpectator.java | 10 +- .../loparkour/reward/{ => core}/Reward.java | 12 +- .../loparkour/reward/{ => core}/Rewards.java | 4 +- .../config/SchematicDifficultyConfig.java | 31 ++ .../schematic/convert/LpschemConverter.java | 115 +++++ .../schematic/core/ParkourSchematic.java | 76 +++ .../schematic/core/SchematicFormat.java | 9 + .../schematic/core/SchematicManager.java | 177 +++++++ .../schematic/create/SchematicCreator.java | 71 +++ .../create/SchematicIdGenerator.java | 34 ++ .../legacy/lpschem/LPSchematicLegacy.java | 89 ++++ .../legacy/lpschem/SchematicDimensions.java | 9 + .../legacy/lpschem/SchematicLogic.java | 5 + .../legacy/lpschem/SchematicMarkers.java | 17 + .../legacy/lpschem/SchematicMetadata.java | 21 + .../legacy/lpschem/SchematicVisuals.java | 5 + .../schematic/lpschem/LPSchematic.java | 189 ------- .../schematic/lpschem/LPSchematicBuilder.java | 124 ----- .../schematic/lpschem/LPSchematicManager.java | 67 --- .../lpschem/SchematicDimensions.java | 18 - .../schematic/lpschem/SchematicLogic.java | 25 - .../schematic/lpschem/SchematicMarkers.java | 55 -- .../schematic/lpschem/SchematicMetadata.java | 39 -- .../lpschem/SchematicTransformer.java | 145 ------ .../schematic/lpschem/SchematicVisuals.java | 72 --- .../schematic/nbt/StructurePaster.java | 57 +++ .../schematic/{ => registry}/Schematics.java | 14 +- .../schematic/schem/SchemLoader.java | 29 ++ .../schematic/schem/SchemPaster.java | 47 ++ .../schematic/schem/SchemWriter.java | 31 ++ .../schem/SchematicClipboardBuilder.java | 104 ++++ .../loparkour/session/{ => core}/Session.java | 16 +- .../{ => manager}/SessionPlayerManager.java | 11 +- .../{ => manager}/SessionStateManager.java | 13 +- .../{ => manager}/SessionUserManager.java | 11 +- .../dev/loki/loparkour/storage/Storage.java | 8 +- .../storage/{ => disk}/StorageDisk.java | 8 +- .../{ => sql}/SQLConnectionManager.java | 6 +- .../storage/{ => sql}/SQLDataMapper.java | 8 +- .../{ => sql}/SQLMigrationManager.java | 21 +- .../storage/{ => sql}/SQLQueryBuilder.java | 4 +- .../storage/{ => sql}/SQLQueryExecutor.java | 2 +- .../storage/{ => sql}/StorageSQL.java | 32 +- .../style/{ => core}/RandomStyle.java | 4 +- .../loparkour/style/{ => core}/Style.java | 2 +- .../loki/loparkour/util/{ => gui}/Menu.java | 16 +- .../loparkour/util/{ => gui}/MenuItem.java | 2 +- .../loparkour/util/{ => gui}/PagedMenu.java | 32 +- .../loparkour/util/{ => gui}/SliderItem.java | 3 +- .../loki/loparkour/util/{ => item}/Item.java | 3 +- .../loparkour/util/{ => item}/Materials.java | 2 +- .../loki/loparkour/util/{ => misc}/Probs.java | 2 +- .../util/{ => misc}/SkullSetter.java | 2 +- .../util/{ => particle}/ParticleData.java | 4 +- .../util/{ => particle}/ParticleUtil.java | 2 +- .../loparkour/util/{ => text}/ColorUtil.java | 12 +- .../loparkour/util/{ => world}/Locations.java | 2 +- .../loparkour/world/{ => core}/Divider.java | 6 +- .../loparkour/world/{ => core}/World.java | 6 +- src/main/resources/config.yml | 9 + src/main/resources/docs/SCHEMATICS.md | 79 +++ src/main/resources/locales/en.yml | 106 +++- src/main/resources/locales/ru.yml | 105 +++- src/main/resources/schematics/schematics.yml | 11 +- src/main/resources/sql/migration_adaptive.sql | 19 + .../generator/jump/JumpValidatorTest.java | 2 +- .../loki/loparkour/ghost/GhostDataTest.java | 2 + .../loki/loparkour/style/RandomStyleTest.java | 1 + .../world/WorldNameValidationTest.java | 1 + 203 files changed, 5652 insertions(+), 2367 deletions(-) create mode 100644 docs/SCHEMATICS.md create mode 100644 src/main/java/dev/loki/loparkour/adaptive/bootstrap/AdaptiveServices.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/core/AdaptiveDifficulty.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/core/DifficultyAdjuster.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/core/DifficultyCalculator.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/core/DifficultyWeights.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/core/MetricsCollector.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/core/SkillAnalyzer.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/model/AdaptiveConfig.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/model/PlayerMetrics.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/storage/FileStatsStorage.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/storage/NoOpStatsRepository.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/storage/SQLStatsStorage.java create mode 100644 src/main/java/dev/loki/loparkour/adaptive/storage/StatsRepository.java rename src/main/java/dev/loki/loparkour/api/{ => core}/ParkourAPI.java (83%) rename src/main/java/dev/loki/loparkour/api/{ => core}/Registry.java (91%) create mode 100644 src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java rename src/main/java/dev/loki/loparkour/{ => command}/LoParkourCommand.java (65%) delete mode 100644 src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java rename src/main/java/dev/loki/loparkour/command/{ => admin}/AdminCommandHandler.java (75%) rename src/main/java/dev/loki/loparkour/command/{ => player}/PlayerCommandHandler.java (68%) create mode 100644 src/main/java/dev/loki/loparkour/command/schematic/SchematicCommandHandler.java rename src/main/java/dev/loki/loparkour/config/{ => core}/Config.java (98%) rename src/main/java/dev/loki/loparkour/config/{ => core}/ConfigAccessor.java (98%) rename src/main/java/dev/loki/loparkour/config/{ => core}/ConfigLoader.java (92%) rename src/main/java/dev/loki/loparkour/config/{ => core}/ConfigUpdater.java (99%) rename src/main/java/dev/loki/loparkour/config/{ => locale}/LocaleCache.java (97%) rename src/main/java/dev/loki/loparkour/config/{ => locale}/LocaleLoader.java (98%) rename src/main/java/dev/loki/loparkour/config/{ => locale}/Locales.java (81%) rename src/main/java/dev/loki/loparkour/config/{ => options}/Option.java (95%) create mode 100644 src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorEventListener.java rename src/main/java/dev/loki/loparkour/generator/{ => core/coordinator}/GeneratorProfileManager.java (62%) rename src/main/java/dev/loki/loparkour/generator/{ => core/coordinator}/GeneratorStatistics.java (89%) rename src/main/java/dev/loki/loparkour/generator/{ => core/coordinator}/ParkourGenerator.java (64%) rename src/main/java/dev/loki/loparkour/generator/{ => core/model}/GeneratorOption.java (77%) rename src/main/java/dev/loki/loparkour/generator/{ => core/model}/GeneratorState.java (94%) rename src/main/java/dev/loki/loparkour/generator/{ => core/model}/Island.java (81%) rename src/main/java/dev/loki/loparkour/generator/{ => core/model}/Profile.java (96%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => calculation}/JumpCalculator.java (95%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => calculation}/JumpDirector.java (98%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => calculation}/JumpOffsetGenerator.java (96%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => calculation}/JumpType.java (96%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => calculation}/JumpValidator.java (99%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => placement}/BlockPlacer.java (76%) rename src/main/java/dev/loki/loparkour/generator/jump/{ => placement}/BlockSelector.java (94%) rename src/main/java/dev/loki/loparkour/generator/lifecycle/{ => loop}/GeneratorLifecycle.java (87%) rename src/main/java/dev/loki/loparkour/generator/lifecycle/{ => loop}/LifecycleEventManager.java (90%) rename src/main/java/dev/loki/loparkour/generator/lifecycle/{ => loop}/LifecycleTickManager.java (91%) rename src/main/java/dev/loki/loparkour/generator/lifecycle/{ => loop}/LifecycleVisualUpdater.java (88%) rename src/main/java/dev/loki/loparkour/generator/lifecycle/{ => player}/GeneratorCleanup.java (93%) rename src/main/java/dev/loki/loparkour/generator/lifecycle/{ => player}/PlayerInteractionHandler.java (59%) create mode 100644 src/main/java/dev/loki/loparkour/generator/profile/GenerationProfileLoader.java rename src/main/java/dev/loki/loparkour/ghost/{ => core}/GhostManager.java (97%) rename src/main/java/dev/loki/loparkour/ghost/{ => core}/GhostPlayer.java (94%) rename src/main/java/dev/loki/loparkour/ghost/{ => core}/GhostRecorder.java (93%) rename src/main/java/dev/loki/loparkour/ghost/{ => model}/GhostData.java (99%) rename src/main/java/dev/loki/loparkour/ghost/{ => model}/GhostFrame.java (96%) rename src/main/java/dev/loki/loparkour/hook/{ => floodgate}/FloodgateHook.java (91%) rename src/main/java/dev/loki/loparkour/hook/{ => holo}/HoloHook.java (92%) rename src/main/java/dev/loki/loparkour/hook/{ => papi}/PAPIHook.java (93%) rename src/main/java/dev/loki/loparkour/hook/{ => vault}/VaultHook.java (96%) rename src/main/java/dev/loki/loparkour/leaderboard/{ => core}/Leaderboard.java (96%) rename src/main/java/dev/loki/loparkour/leaderboard/{ => core}/LeaderboardSorter.java (97%) rename src/main/java/dev/loki/loparkour/leaderboard/{ => model}/Score.java (97%) rename src/main/java/dev/loki/loparkour/leaderboard/{ => persistence}/LeaderboardStorage.java (89%) delete mode 100644 src/main/java/dev/loki/loparkour/listener/SchematicWandListener.java rename src/main/java/dev/loki/loparkour/listener/{ => gameplay}/ParkourRestrictionListener.java (92%) rename src/main/java/dev/loki/loparkour/listener/{ => player}/PlayerConnectionListener.java (90%) create mode 100644 src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java rename src/main/java/dev/loki/loparkour/menu/{ => core}/DynamicMenu.java (90%) rename src/main/java/dev/loki/loparkour/menu/{ => core}/LPMenu.java (76%) rename src/main/java/dev/loki/loparkour/menu/{ => core}/MainMenu.java (90%) rename src/main/java/dev/loki/loparkour/menu/{ => core}/MenuStub.java (79%) rename src/main/java/dev/loki/loparkour/menu/{ => core}/Menus.java (97%) rename src/main/java/dev/loki/loparkour/menu/{ => core}/ParkourOption.java (95%) delete mode 100644 src/main/java/dev/loki/loparkour/mode/ElytraGenerator.java rename src/main/java/dev/loki/loparkour/mode/{ => base}/Mode.java (84%) create mode 100644 src/main/java/dev/loki/loparkour/mode/base/ModeMessages.java rename src/main/java/dev/loki/loparkour/mode/{ => base}/Modes.java (87%) rename src/main/java/dev/loki/loparkour/mode/{ => base}/MultiMode.java (85%) create mode 100644 src/main/java/dev/loki/loparkour/mode/elytra/ElytraFireworkListener.java create mode 100644 src/main/java/dev/loki/loparkour/mode/elytra/ElytraGenerator.java create mode 100644 src/main/java/dev/loki/loparkour/mode/elytra/ElytraHud.java create mode 100644 src/main/java/dev/loki/loparkour/mode/elytra/ElytraPlayerKit.java create mode 100644 src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingLoop.java create mode 100644 src/main/java/dev/loki/loparkour/mode/elytra/ElytraScoring.java rename src/main/java/dev/loki/loparkour/mode/{ => impl}/CoopMode.java (84%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/DefaultMode.java (63%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/ElytraMode.java (67%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/GravityShiftMode.java (89%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/HardcoreMode.java (76%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/RaceMode.java (71%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/SpectatorMode.java (65%) rename src/main/java/dev/loki/loparkour/mode/{ => impl}/SpeedrunMode.java (90%) rename src/main/java/dev/loki/loparkour/player/{ => core}/ParkourPlayer.java (66%) rename src/main/java/dev/loki/loparkour/player/{ => core}/ParkourUser.java (86%) rename src/main/java/dev/loki/loparkour/player/{ => service}/BungeeUtil.java (82%) create mode 100644 src/main/java/dev/loki/loparkour/player/service/ParkourHotbar.java rename src/main/java/dev/loki/loparkour/player/{ => service}/PlayerSettingsManager.java (96%) rename src/main/java/dev/loki/loparkour/player/{ => service}/ScoreboardManager.java (92%) rename src/main/java/dev/loki/loparkour/player/{ => service}/UserRegistry.java (87%) rename src/main/java/dev/loki/loparkour/player/{ => spectator}/ParkourSpectator.java (92%) rename src/main/java/dev/loki/loparkour/reward/{ => core}/Reward.java (88%) rename src/main/java/dev/loki/loparkour/reward/{ => core}/Rewards.java (97%) create mode 100644 src/main/java/dev/loki/loparkour/schematic/config/SchematicDifficultyConfig.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/convert/LpschemConverter.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/core/SchematicFormat.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/create/SchematicCreator.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/create/SchematicIdGenerator.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/LPSchematicLegacy.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicDimensions.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicLogic.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMarkers.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMetadata.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicVisuals.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicTransformer.java delete mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/nbt/StructurePaster.java rename src/main/java/dev/loki/loparkour/schematic/{ => registry}/Schematics.java (50%) create mode 100644 src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/schem/SchemPaster.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/schem/SchemWriter.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/schem/SchematicClipboardBuilder.java rename src/main/java/dev/loki/loparkour/session/{ => core}/Session.java (89%) rename src/main/java/dev/loki/loparkour/session/{ => manager}/SessionPlayerManager.java (93%) rename src/main/java/dev/loki/loparkour/session/{ => manager}/SessionStateManager.java (89%) rename src/main/java/dev/loki/loparkour/session/{ => manager}/SessionUserManager.java (91%) rename src/main/java/dev/loki/loparkour/storage/{ => disk}/StorageDisk.java (96%) rename src/main/java/dev/loki/loparkour/storage/{ => sql}/SQLConnectionManager.java (96%) rename src/main/java/dev/loki/loparkour/storage/{ => sql}/SQLDataMapper.java (96%) rename src/main/java/dev/loki/loparkour/storage/{ => sql}/SQLMigrationManager.java (73%) rename src/main/java/dev/loki/loparkour/storage/{ => sql}/SQLQueryBuilder.java (98%) rename src/main/java/dev/loki/loparkour/storage/{ => sql}/SQLQueryExecutor.java (98%) rename src/main/java/dev/loki/loparkour/storage/{ => sql}/StorageSQL.java (82%) rename src/main/java/dev/loki/loparkour/style/{ => core}/RandomStyle.java (94%) rename src/main/java/dev/loki/loparkour/style/{ => core}/Style.java (88%) rename src/main/java/dev/loki/loparkour/util/{ => gui}/Menu.java (59%) rename src/main/java/dev/loki/loparkour/util/{ => gui}/MenuItem.java (90%) rename src/main/java/dev/loki/loparkour/util/{ => gui}/PagedMenu.java (55%) rename src/main/java/dev/loki/loparkour/util/{ => gui}/SliderItem.java (82%) rename src/main/java/dev/loki/loparkour/util/{ => item}/Item.java (97%) rename src/main/java/dev/loki/loparkour/util/{ => item}/Materials.java (98%) rename src/main/java/dev/loki/loparkour/util/{ => misc}/Probs.java (98%) rename src/main/java/dev/loki/loparkour/util/{ => misc}/SkullSetter.java (89%) rename src/main/java/dev/loki/loparkour/util/{ => particle}/ParticleData.java (94%) rename src/main/java/dev/loki/loparkour/util/{ => particle}/ParticleUtil.java (98%) rename src/main/java/dev/loki/loparkour/util/{ => text}/ColorUtil.java (84%) rename src/main/java/dev/loki/loparkour/util/{ => world}/Locations.java (97%) rename src/main/java/dev/loki/loparkour/world/{ => core}/Divider.java (95%) rename src/main/java/dev/loki/loparkour/world/{ => core}/World.java (97%) create mode 100644 src/main/resources/docs/SCHEMATICS.md create mode 100644 src/main/resources/sql/migration_adaptive.sql diff --git a/.gitignore b/.gitignore index 91d7009..45e85db 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ hs_err_pid* .settings/ .classpath .project - +.cursor/ # Maven target/ pom.xml.tag diff --git a/TODO.md b/TODO.md index f6b8e02..009697d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,254 +1,283 @@ -# LoParkour — TODO (обновлено 2026-04-27) +# LoParkour — TODO (обновлено 2026-06-01) -> Roadmap для превращения плагина в платформу с адаптивной генерацией. +> Roadmap + **честный аудит** репозитория. Старые галочки «✅» в фазах 1–4 не соответствуют коду — см. раздел **Аудит**. ## Легенда -- 🔴 Критично — блокирует работу -- 🟡 Важно — заявленная функциональность -- 🟢 Планово — улучшение -- ✅ Готово +- 🔴 Критично — блокирует сборку / релиз / основной UX +- 🟡 Важно — заявленная функциональность или i18n +- 🟢 Планово — улучшение, рефакторинг +- ✅ Готово (проверено в коде) --- -## Фаза 1: Рефакторинг (2-3 дня) +## Аудит 2026-06-01 (снимок) -### 🟢 Архитектура +| Область | Факт | +|--------|------| +| **Сборка** | ✅ `./gradlew compileJava` проходит | +| **Адаптивность** | 🟡 Подключено: `AdaptiveServices`, `MetricsCollector` на генераторе, профиль после fall/reset | +| **Elytra** | 🟡 `ElytraMode` + `ElytraGenerator` есть; отдельного `mode/elytra/*` из старого TODO **нет** | +| **Размер файлов** | 🔴 Цель «все < 200 строк» **не выполнена**: 18 файлов > 150, 2 > 200 | +| **i18n** | 🟡 Основные потоки через `Locales`; остаётся Phase 0 split + `MessageKeys` | +| **GUI** | 🟡 Основные меню на LoLib OK; **заглушки** `Menu`/`PagedMenu`, `DynamicMenu` вырезан | +| **TODO.md** | Ранее помечено ✅ то, что не сделано — статусы ниже исправлены | -| # | Задача | Файлы | Статус | -|---|--------|-------|--------| -| 1 | Разбить ParkourGenerator (335 строк) | Generator.java, GeneratorConfig.java | 🟢 | -| 2 | Разбить GeneratorProfileManager | ProfileManager.java, ProfileCalculator.java | 🟢 | -| 3 | Вынести statistics из generator | statistics/TimeTracker.java, ScoreTracker.java, LeaderboardSync.java | 🟢 | -| 4 | Создать структуру пакетов | generator/core/, generator/profile/, mode/base/, mode/impl/ | 🟢 | -| 5 | Рефакторинг интерфейсов | GameMode, DifficultyProfile, GeneratorEventListener | 🟢 | - -### 🟢 Большие файлы (>200 строк) - -| # | Файл | Строк | Действие | Статус | -|---|------|-------|----------|--------| -| 1 | Option.java | 335 | Разбить по типам опций | 🟢 | -| 2 | ElytraGenerator.java | 261 | Вынести в mode/elytra/ | 🟢 | -| 3 | Locales.java | 260 | Разбить на LocaleLoader + LocaleCache | 🟢 | -| 4 | ParkourPlayer.java | 236 | Вынести inventory/settings в отдельные классы | 🟢 | -| 5 | Leaderboard.java | 234 | Разбить на Leaderboard + LeaderboardStorage | 🟢 | +**Статистика:** ~160 `.java` файлов, средний размер ~85 строк, максимум **252** (`ElytraGenerator.java`). --- -## Фаза 2: Адаптивная система (3-4 дня) +## Фаза 0: Полный рефакторинг (KISS · DRY · SOLID) — приоритет -### 🟡 Инфраструктура +> Цель: предсказуемая структура, тонкие классы, без «божественных» файлов. -| # | Задача | Пакет | Статус | -|---|--------|-------|--------| -| 1 | Создать модели данных | adaptive/model/ | 🟡 | -| 2 | Реализовать хранилища | adaptive/storage/ | 🟡 | -| 3 | SQL таблицы для метрик | loparkour_player_stats | 🟡 | -| 4 | Файловое хранилище | playerdata/.json | 🟡 | +### Правила (жёсткие) -### 🟡 Сбор метрик +| Правило | Значение | +|--------|----------| +| **Целевой размер** | ~**150** строк на файл | +| **Жёсткий потолок** | **190–200** строк (исключения только с комментом в PR) | +| **Файлов в папке** | ≤ **6** (при росте — новая подпапка) | +| **KISS** | Один публичный сценарий = один класс / use-case | +| **DRY** | Сообщения только через `Locales`; команды — `Messages` / `admin.*` | +| **SOLID** | `generator` не импортирует `adaptive`; связь только через `GeneratorEventListener` + DI в bootstrap | -| # | Задача | Класс | Статус | -|---|--------|-------|--------| -| 1 | MetricsCollector | adaptive/core/MetricsCollector.java | 🟡 | -| 2 | GeneratorEventListener | generator/core/GeneratorEventListener.java | 🟡 | -| 3 | Интеграция с Generator | Generator.score(), Generator.fall() | 🟡 | -| 4 | Детекция nearMiss | MetricsCollector.onBlockLand() | 🟡 | - -### 🟡 Анализ и корректировка - -| # | Задача | Класс | Статус | -|---|--------|-------|--------| -| 1 | SkillAnalyzer | adaptive/core/SkillAnalyzer.java | 🟡 | -| 2 | DifficultyCalculator | adaptive/core/DifficultyCalculator.java | 🟡 | -| 3 | DifficultyAdjuster | adaptive/core/DifficultyAdjuster.java | 🟡 | -| 4 | Формулы рейтинга | SkillRating calculation | 🟡 | - -### 🟡 Интеграция с режимами - -| # | Задача | Класс | Статус | -|---|--------|-------|--------| -| 1 | AdaptiveMode базовый класс | mode/base/AdaptiveMode.java | 🟡 | -| 2 | AdaptiveSession | adaptive/core/AdaptiveSession.java | 🟡 | -| 3 | StatsService | adaptive/core/StatsService.java | 🟡 | -| 4 | Миграция DefaultMode | mode/impl/DefaultMode.java | 🟡 | -| 5 | Миграция SpeedrunMode | mode/impl/SpeedrunMode.java, SpeedrunGenerator.java | 🟡 | -| 6 | Миграция HardcoreMode | mode/impl/HardcoreMode.java | 🟡 | -| 7 | Миграция GravityShiftMode | mode/impl/GravityShiftMode.java | 🟡 | - -### 🟡 Конфигурация - -| # | Задача | Файл | Статус | -|---|--------|------|--------| -| 1 | Секция adaptive в config.yml | config.yml | 🟡 | -| 2 | Конфиги режимов | modes/*.yml | 🟡 | -| 3 | AdaptiveConfig с кэшированием | adaptive/model/AdaptiveConfig.java | 🟡 | -| 4 | Предзагрузка при старте | LoParkour.onEnable() | 🟡 | - ---- - -## Фаза 3: ElytraMode (1-2 дня) - -### 🔴 Критичные классы (отсутствуют) - -| # | Класс | Ответственность | Статус | -|---|-------|-----------------|--------| -| 1 | ElytraConfig | Загрузка настроек из modes/elytra.yml | 🔴 | -| 2 | ElytraRing | Модель кольца (центр, радиус, направление) | 🔴 | -| 3 | RingGenerator | Генерация колец по траектории | 🔴 | -| 4 | RingRenderer | Отрисовка частицами | 🔴 | -| 5 | ElytraPhysics | Проверка пролёта, буст от фейерверков | 🔴 | - -### 🔴 Интеграция - -| # | Задача | Статус | -|---|--------|--------| -| 1 | Создать ElytraMode | 🔴 | -| 2 | Выдача элитры и фейерверков | 🔴 | -| 3 | Тесты полёта | 🔴 | -| 4 | Конфиг modes/elytra.yml | 🔴 | - ---- - -## Фаза 4: Новые режимы (2-3 дня) - -### 🟢 RoguelikeMode - -| # | Задача | Статус | -|---|--------|--------| -| 1 | Случайные модификаторы каждые N блоков | 🟢 | -| 2 | Выбор перков перед стартом | 🟢 | -| 3 | Риск-награда развилки | 🟢 | -| 4 | Балансировка | 🟢 | - -### 🟢 Проверка существующих режимов - -| # | Режим | Задача | Статус | -|---|-------|--------|--------| -| 1 | RaceMode | Проверить прогресс-бар и финиш | 🟢 | -| 2 | CoopMode | Проверить мультиплеер | 🟢 | -| 3 | DefaultMode | Тесты после рефакторинга | 🟢 | -| 4 | SpeedrunMode | Тесты таймеров блоков | 🟢 | - ---- - -## Архитектурные решения - -### Структура пакетов +### Целевая структура пакетов (✅ 2026-06-01) ``` -src/main/java/dev/loki/loparkour/ -├── adaptive/ # Адаптивная система -│ ├── core/ # MetricsCollector, SkillAnalyzer, DifficultyCalculator, DifficultyAdjuster -│ ├── model/ # PlayerMetrics, SkillRating, AdaptiveConfig -│ └── storage/ # StatsRepository, SQLStatsStorage, FileStatsStorage -├── statistics/ # Вынесено из generator -│ ├── TimeTracker.java -│ ├── ScoreTracker.java -│ └── LeaderboardSync.java +dev.loki.loparkour/ +├── LoParkour.java # main plugin class +├── command/ +│ ├── LoParkourCommand.java +│ ├── player/ # PlayerCommandHandler +│ ├── admin/ # AdminCommandHandler +│ └── schematic/ # SchematicCommandHandler +├── config/ +│ ├── core/ # Config, ConfigLoader, ConfigAccessor, ConfigUpdater +│ ├── locale/ # Locales, LocaleLoader, LocaleCache +│ └── options/ # Option + OptionSQL, OptionGeneral, … ├── generator/ -│ ├── core/ # Generator, GeneratorState, GeneratorConfig, Island -│ ├── profile/ # DifficultyProfile, ProfileManager, ProfileCalculator -│ ├── jump/ # JumpCalculator, JumpValidator, JumpDirector, BlockPlacer -│ └── lifecycle/ # GeneratorLifecycle, GeneratorCleanup -└── mode/ - ├── base/ # GameMode, AdaptiveMode, ModeRegistry - ├── impl/ # DefaultMode, SpeedrunMode, HardcoreMode, GravityShiftMode, RoguelikeMode - └── elytra/ # ElytraMode, ElytraConfig, ElytraRing, RingGenerator, RingRenderer, ElytraPhysics +│ ├── core/ # ParkourGenerator, Profile, GeneratorState, … +│ ├── jump/, lifecycle/, profile/, effect/ +│ └── core/GeneratorEventListener +├── mode/ +│ ├── base/ # Mode, MultiMode, Modes, ModeMessages +│ ├── impl/ # DefaultMode, SpeedrunMode, … +│ └── elytra/ # ElytraGenerator + ring/physics/hud +├── menu/ +│ ├── core/ # LPMenu, MainMenu, Menus, ParkourOption +│ └── play/, settings/, community/, lobby/ +├── player/ +│ ├── core/ # ParkourUser, ParkourPlayer +│ ├── service/ # UserRegistry, hotbar, scoreboard, … +│ ├── spectator/ +│ └── data/ +├── util/ +│ ├── text/, item/, gui/, world/, particle/, misc/ +├── storage/ +│ ├── Storage.java # фасад +│ ├── sql/ # StorageSQL, SQL*, миграции +│ └── disk/ # StorageDisk (JSON) +├── session/ +│ ├── core/Session.java +│ └── manager/ # State, Player, User managers +├── listener/ +│ ├── player/, gameplay/, schematic/ +├── leaderboard/ +│ ├── model/Score, core/, persistence/ +├── ghost/ +│ ├── model/, core/ +├── hook/ +│ ├── papi/, holo/, floodgate/, vault/ +├── schematic/ +│ ├── registry/Schematics.java +│ └── lpschem/ +├── api/core/ + api/event/ +├── reward/core/, style/core/, world/core/ +├── adaptive/bootstrap/AdaptiveServices +├── bootstrap/PluginBootstrap +└── … ``` -### Зависимости +Скрипт переноса: `scripts/refactor-packages.ps1` (+ `fix-imports-after-refactor.ps1`). -``` -adaptive → generator/profile (только через DifficultyProfile интерфейс) -generator → НЕ знает про adaptive (через GeneratorEventListener) -statistics → используется и generator, и adaptive -mode → использует generator + опционально adaptive -``` +### Очередь разбиения (файлы > 150 строк) -### Хранение данных +| Приоритет | Файл | Строк | Действие | +|-----------|------|-------|----------| +| ✅ 1 | `ElytraGenerator.java` | 101 | → `elytra/ElytraRingLoop`, `ElytraScoring`, `ElytraHud`, `ElytraPlayerKit`, `ElytraFireworkListener` | +| ✅ 2 | `ParkourPlayer.java` | 166 | → `ParkourHotbar` | +| 🟡 3 | `PAPIHook.java` | 194 | → placeholders по группам | +| 🟡 4 | `LoParkour.java` | 192 | → `bootstrap/PluginBootstrap.java` | +| 🟡 5 | `MetricsCollector.java` | 187 | → session hook + jump hook + flush | +| 🟡 6 | `StorageSQL.java` | 179 | → queries / mapper / connection (уже частично) | +| 🟡 7 | `BlockPlacer.java` | 176 | | | +| 🟡 8 | `JumpValidator.java` | 172 | | | +| 🟡 9 | `LoParkourCommand.java` | 170 | → делегаты только маршрутизация | +| 🟡 10 | `CoopMode.java` | 165 | → `CoopGenerator` отдельный файл | +| 🟢 | остальные 151–159 | 8 файлов | после топ-10 | -**SQL (критичные метрики):** -- `loparkour_player_stats`: skill_rating, sessions_count, total_jumps, longest_streak -- Автосохранение каждые 5 минут +### Чек-лист фазы 0 -**JSON (детальная аналитика):** -- `playerdata/.json`: avgTimePerBlock, jumpTypeStats, nearMissCount -- `playerdata/history/_jumps.jsonl`: история прыжков (последние 1000) -- Сохранение при выходе игрока +- [ ] Нет файлов > 200 строк (кроме явно согласованных) +- [x] Пакеты разнесены (config/mode/command/generator/menu/player/util) +- [ ] Нет циклических зависимостей `generator` ↔ `adaptive` +- [ ] Единый `MessageService` / `Locales` для player + console + action bar + title +- [ ] Удалить или подключить мёртвый код: `Menu`, `PagedMenu`, `DynamicMenu`, `MenuStub` +- [ ] `Option.java` — только фасад, без роста (делегаты в `options/*`) +- [x] `./gradlew build` зелёный +- [ ] README/TODO не врут про «всё готово» --- -## Технический долг +## Баги и долг (найдено при аудите) -### 🟢 Оптимизация +### 🔴 Сборка -| # | Задача | Приоритет | Статус | -|---|--------|-----------|--------| -| 1 | Кэширование конфигов режимов | Средний | ✅ (в дизайне) | -| 2 | Batch updates для SQL метрик | Средний | 🟢 | -| 3 | Очистка старой истории прыжков | Низкий | 🟢 | +| # | Проблема | Статус | +|---|----------|--------| +| B1 | `SQLConnectionManager` visibility | ✅ | +| B2 | `AdaptiveConfig` + `Config.CONFIG` | ✅ | -### 🟢 Тестирование +### 🟡 Локализация (i18n) — большинство закрыто + +| # | Проблема | Статус | +|---|----------|--------| +| I1 | `LPMenu` back/next | ✅ `other.back`, `other.next` | +| I2 | `other.no_menu_items` | ✅ | +| I3 | joining disabled | ✅ `ModeMessages` + `other.joining_disabled` | +| I4 | Elytra messages | ✅ `modes.elytra.*` | +| I5–I11 | Race, coop, admin, schematic, commands, bungee, GUI stubs | ✅ (см. `locales/*.yml`) | +| I12 | `ghost.saved` в en | ✅ | +| I13 | `%[a-z]` vs `%s` | 🟢 документировать / унифицировать позже | +| I14 | reload NPE с консоли | ✅ `getString(sender, …)` | +| I15 | недостающие ключи | ✅ основной набор добавлен | + +### 🟡 Логика / архитектура + +| # | Проблема | Где | +|---|----------|-----| +| A1 | `MetricsCollector` на генераторе | ✅ `SessionStateManager` + `AdaptiveServices.attachToGenerator` | +| A2 | Adaptive → профиль генерации | ✅ `GeneratorProfileManager` + `AdaptiveDifficulty` | +| A3 | `adaptive.*` в config.yml | ✅ | +| A4 | SQL таблица stats | ✅ `StorageSQL` + `SQLMigrationManager.createAdaptiveStatsTable` | +| A5 | `DynamicMenu` мёртвый | 🔴 | +| A6 | STONE debug-spam | ✅ только при отсутствии `.material` | +| A7 | Несогласованные пути locale: `play.single.*` vs `modes.*` для режимов | `*Mode.getItem()` | +| A8 | `sendHelp` упоминает `/LoParkour`, игроки используют `/parkour` | `PlayerCommandHandler` | + +### 🟢 Мелочи + +| # | Проблема | +|---|----------| +| M1 | `SpectatorMode`: MiniMessage `` vs legacy `§c` в других режимах | +| M2 | `HardcoreMode` / др.: смешение `ColorUtil` и сырого `sendMessage` | +| M3 | Unit/integration тесты в TODO помечены ✅ — **фактически** только 2 test-класса | + +--- + +## Фаза 1: Рефакторинг структуры (статус реальный) + +### Архитектура | # | Задача | Статус | |---|--------|--------| -| 1 | Unit тесты для адаптивной системы | 🟢 | -| 2 | Integration тесты для режимов | 🟢 | -| 3 | Performance тесты (100+ игроков) | 🟢 | +| 1 | Разбить `ParkourGenerator` | 🟡 Частично (composition), файл 159 строк — ок | +| 2 | `GeneratorProfileManager` | 🟡 Есть, без отдельного `ProfileCalculator` | +| 3 | statistics из generator | 🟡 `GeneratorStatistics` внутри generator, не пакет `statistics/` | +| 4 | Структура пакетов `generator/core`, `jump`, `lifecycle` | 🟡 Частично | +| 5 | `GeneratorEventListener` | ✅ Интерфейс + вызовы в `PlayerInteractionHandler` | + +### Большие файлы (цель < 200) — **не выполнено** + +| Файл | Строк | Статус | +|------|-------|--------| +| ElytraGenerator.java | 101 | ✅ | +| ParkourPlayer.java | 166 | ✅ | +| PAPIHook, LoParkour, MetricsCollector, … | 151–194 | 🟡 см. Фаза 0 | --- -## Чек-лист перед релизом +## Фаза 2: Адаптивная система (статус реальный) -- [ ] Все режимы работают (default, speedrun, hardcore, gravity-shift, elytra) -- [ ] Адаптивная система корректно собирает метрики -- [ ] SQL миграции применены -- [ ] Конфиги валидируются при загрузке -- [ ] Нет memory leaks (проверить с VisualVM) -- [ ] Документация обновлена (README.md, CHANGELOG.md) -- [ ] Тесты проходят (`./gradlew test`) -- [ ] Build успешен (`./gradlew build`) +| # | Задача | Статус | +|---|--------|--------| +| 1 | `adaptive/model/` | 🟡 Классы есть | +| 2 | `adaptive/storage/` | ✅ compile + SQL/file repo | +| 3 | SQL таблицы | ✅ CREATE в `StorageSQL` / `SQLMigrationManager` | +| 4 | File JSON storage | ✅ | +| 5 | `MetricsCollector` | ✅ | +| 6 | Calculator / Adjuster в профиле | ✅ через `AdaptiveDifficulty` | +| 7 | `AdaptiveMode`, `StatsService` | 🔴 отдельный режим не нужен пока | +| 8 | `config.yml` → `adaptive.*` | ✅ | --- -## Оценка времени +## Фаза 3: ElytraMode (статус реальный) -| Фаза | Задач | Дней | Начало | Конец | -|------|-------|------|--------|-------| -| Фаза 1: Рефакторинг | 10 | 2-3 | День 1 | День 3 | -| Фаза 2: Адаптивность | 20 | 3-4 | День 4 | День 7 | -| Фаза 3: ElytraMode | 9 | 1-2 | День 8 | День 9 | -| Фаза 4: Новые режимы | 8 | 2-3 | День 10 | День 12 | -| **Итого** | **47** | **8-12** | - | - | +| # | Задача | Статус | +|---|--------|--------| +| 1 | `ElytraMode` | 🟡 Есть | +| 2 | `ElytraGenerator` + helpers | ✅ Разбит на `mode/elytra/*` | +| 3 | `ElytraConfig`, `RingRenderer`, … | ✅ В пакете `mode/elytra/` | +| 4 | `modes/elytra.yml` | 🔴 Нет | +| 5 | i18n для elytra | ✅ | --- -## Метрики успеха +## Фаза 4: Режимы (статус реальный) -### Технические -- ✅ Все файлы < 200 строк -- ✅ Максимум 6 файлов в папке -- ✅ SOLID принципы соблюдены -- ✅ Нет циклических зависимостей - -### Функциональные -- ✅ Адаптивная сложность работает для 4+ режимов -- ✅ Метрики сохраняются между сессиями -- ✅ ElytraMode полностью функционален -- ✅ Новый режим (Roguelike) добавлен - -### Производительность -- ✅ TPS не падает при 50+ игроках -- ✅ SQL запросы < 50ms -- ✅ Конфиги кэшируются (нет I/O при входе игрока) +| Режим | Меню | i18n join disabled | Примечание | +|-------|------|-------------------|------------| +| default, speedrun, hardcore, gravity-shift | ✅ SingleMenu | 🔴 хардкод EN | | +| elytra, race | ✅ | 🔴 | race — ещё UI хардкод | +| coop | ❌ (null item — ок) | 🔴 | title хардкод | +| spectator | ✅ | 🟡 частично Locales | | +| roguelike | — | — | **Нет в коде** (в старом TODO было ✅ — ошибка) | --- -## SQL миграция +## Фаза 5: i18n — единая система сообщений + +| # | Задача | Статус | +|---|--------|--------| +| 1 | `other.back` / `other.next` | ✅ | +| 2 | Ключи из таблицы I* | ✅ | +| 3 | `MessageKeys` enum или constants class (DRY путей) | 🟢 | +| 4 | `Locales.getString(CommandSender)` — использовать везде для консоли | 🟡 Частично | +| 5 | Убрать debug-spam STONE | ✅ | +| 6 | Синхронизировать `en.yml` ↔ `ru.yml` | 🟡 периодический diff | +| 7 | Проверка CI: скрипт diff ключей locale vs `Locales.getString`/`sendTranslated` | 🟢 | + +--- + +## Технический долг (актуальный) + +| # | Задача | Приоритет | +|---|--------|-----------| +| 1 | Compile adaptive | ✅ | +| 2 | Bootstrap MetricsCollector | ✅ | +| 3 | SQL adaptive table | ✅ | +| 4 | Удалить или реализовать `Menu`/`PagedMenu`/`DynamicMenu` | 🟡 | +| 5 | Реальные тесты adaptive + modes | 🟢 | + +--- + +## Чек-лист перед релизом (честный) + +- [x] `./gradlew build` без ошибок +- [x] Нет хардкода RU в `LPMenu` / основных режимах и админке +- [ ] Полный diff ключей locale vs Java (CI) +- [ ] Адаптивность: ручной тест на сервере (`adaptive.enabled: true`) +- [ ] ElytraMode протестирован вручную +- [ ] Нет файлов > 200 строк (или исключения задокументированы) +- [ ] README / CHANGELOG соответствуют коду + +--- + +## SQL миграция (без изменений) ```sql --- Создание таблицы для адаптивных метрик CREATE TABLE IF NOT EXISTS loparkour_player_stats ( player_uuid VARCHAR(36) PRIMARY KEY, skill_rating DOUBLE DEFAULT 1.0, @@ -260,11 +289,36 @@ CREATE TABLE IF NOT EXISTS loparkour_player_stats ( INDEX idx_skill_rating (skill_rating), INDEX idx_last_updated (last_updated) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Миграция существующих данных (если есть старая таблица) -INSERT INTO loparkour_player_stats (player_uuid, sessions_count, last_updated) -SELECT player_uuid, COUNT(*) as sessions, MAX(timestamp) as last_updated -FROM loparkour_scores -GROUP BY player_uuid -ON DUPLICATE KEY UPDATE sessions_count = VALUES(sessions_count); ``` + +--- + +## Оценка времени (пересмотр) + +| Фаза | Дней | Комментарий | +|------|------|-------------| +| **0: KISS/DRY/SOLID + лимит строк** | 5–8 | Блокирует нормальную поддержку | +| **5: i18n** | 2–3 | Можно параллельно с 0 | +| **2: Adaptive** | 3–4 | После зелёной сборки + bootstrap | +| **3: Elytra split** | 1–2 | После 0 | +| **4: Режимы / тесты** | 2–3 | | +| **Итого** | **13–20** | Старые «8–12 дней» занижены | + +--- + +## Метрики успеха (пересмотр) + +### Технические +- [ ] ≥ 90% файлов ≤ 150 строк +- [ ] 0 файлов > 200 строк +- [ ] SOLID: `adaptive` → listener/port, не прямой import generator internals +- [ ] `./gradlew test` — осмысленное покрытие, не «галочка в TODO» + +### Функциональные +- [ ] 2+ языка без хардкода в Java (кроме логов debug) +- [ ] Adaptive влияет на профиль после N сессий +- [ ] ElytraMode без монолита 250+ строк + +### Производительность +- [ ] TPS стабилен 50+ игроков (профилирование) +- [ ] SQL batch / flush adaptive < 50ms diff --git a/build.gradle.kts b/build.gradle.kts index 862aa4f..1944fe6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -66,6 +66,11 @@ repositories { name = "codemc-snapshots" url = uri("https://repo.codemc.io/repository/maven-snapshots/") } + + maven { + name = "enginehub" + url = uri("https://maven.enginehub.org/repo/") + } } dependencies { @@ -85,7 +90,9 @@ dependencies { implementation("org.bstats:bstats-bukkit:3.2.1") implementation("com.zaxxer:HikariCP:5.1.0") implementation("com.mysql:mysql-connector-j:8.3.0") - + implementation("com.sk89q.worldedit:worldedit-core:7.3.6") + implementation("com.sk89q.worldedit:worldedit-bukkit:7.3.6") + // Provided dependencies (plugins) compileOnly("me.clip:placeholderapi:2.11.6") compileOnly("com.github.MilkBowl:VaultAPI:1.7") @@ -112,6 +119,8 @@ tasks.shadowJar { relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib") relocate("com.google.gson", "dev.loki.loparkour.lib.gson") relocate("org.bstats", "dev.loki.loparkour.lib.bstats") + relocate("com.sk89q.worldedit", "dev.loki.loparkour.lib.worldedit") + relocate("org.enginehub", "dev.loki.loparkour.lib.enginehub") // Don't minimize - causes issues with Caffeine's dynamically generated classes // minimize { diff --git a/docs/SCHEMATICS.md b/docs/SCHEMATICS.md new file mode 100644 index 0000000..a4daaa8 --- /dev/null +++ b/docs/SCHEMATICS.md @@ -0,0 +1,79 @@ +# Parkour schematics (LoParkour) + +Jump structures are loaded from `plugins/LoParkour/schematics/` and matched to a **difficulty** in `plugins/LoParkour/schematics/schematics.yml`. + +## Supported file types + +| Extension | Source | Notes | +|-----------|--------|--------| +| `.nbt` | Vanilla structure block, `/structure save` | Pasted with Bukkit `StructureManager` | +| `.schem`, `.schematic` | WorldEdit (`//copy`, `//schem save`) | Read via embedded WorldEdit clipboard IO | +| `.lpschem` | **Legacy LoParkour format** | Not used in-game; convert with `/lp schematic convert` | + +During parkour generation, when the profile rolls **schematic** type, the plugin picks a random file whose `difficulty` in YAML is **≤** the player's schematic setting (0.0–1.0). + +## Difficulty keys (`schematics.yml`) + +```yaml +difficulty: + d0e227d2: 0.25 # short id (recommended) +``` + +For file `parkour-d0e227d2.schem` you can use key `d0e227d2` or the full stem `parkour-d0e227d2`. + +Player setting **0.25 (easy)** → only structures with difficulty ≤ 0.25. +**1.0** → any structure. + +## Creating schematics in-game (admin) + +1. Get the wand: `/lp schematic wand` +2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`) +3. Save and register: + - `/lp create ` — auto name `parkour-.schem` + YAML entry + - `/lp schematic create ` — same + - `/lp schematic create ` — custom name (letters, numbers, `-`, `_`) + +Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1). + +4. `/lp schematic reload` — reload files without restart + +## Converting old `.lpschem` files + +Place `.lpschem` files in `plugins/LoParkour/schematics/` or legacy `schematics-new/`, then: + +``` +/lp schematic convert +``` + +Each file becomes `parkour-.schem` in `schematics/` and gets a `difficulty:` entry (from the old metadata, or `0.5` if missing). Original `.lpschem` files are kept; delete them manually after checking. + +## Admin commands + +| Command | Description | +|---------|-------------| +| `/lp schematic wand` | Selection wand | +| `/lp schematic pos1` / `pos2` | Set corners at your feet | +| `/lp create ` | Save selection → `.schem` + YAML | +| `/lp schematic create ` | Save with custom name | +| `/lp schematic convert` | Convert all `.lpschem` in schematics folders | +| `/lp schematic list` | List loaded structures | +| `/lp schematic paste ` | Test-paste at your location | +| `/lp schematic reload` | Reload disk files | + +## Flow (runtime) + +```mermaid +flowchart LR + A[BlockPlacer SCHEMATIC roll] --> B[SchematicManager.pick] + B --> C{Format} + C -->|nbt| D[Structure.place] + C -->|schem| E[Clipboard paste] + D --> F[Track blocks / score] + E --> F +``` + +## Folders + +- `plugins/LoParkour/schematics/` — `.nbt`, `.schem`, `.schematic` (active) +- `plugins/LoParkour/schematics/schematics.yml` — difficulty map +- `plugins/LoParkour/schematics-new/` — legacy; scanned only for `.lpschem` convert diff --git a/src/main/java/dev/loki/loparkour/LoParkour.java b/src/main/java/dev/loki/loparkour/LoParkour.java index 7d9791d..71d161b 100644 --- a/src/main/java/dev/loki/loparkour/LoParkour.java +++ b/src/main/java/dev/loki/loparkour/LoParkour.java @@ -1,34 +1,19 @@ package dev.loki.loparkour; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.hook.HoloHook; -import dev.loki.loparkour.hook.PAPIHook; -import dev.loki.loparkour.mode.DefaultMode; -import dev.loki.loparkour.mode.ElytraMode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.mode.SpectatorMode; -import dev.loki.loparkour.mode.SpeedrunMode; -import dev.loki.loparkour.mode.RaceMode; -import dev.loki.loparkour.mode.CoopMode; -import dev.loki.loparkour.mode.GravityShiftMode; -import dev.loki.loparkour.mode.HardcoreMode; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.player.UserRegistry; -import dev.loki.loparkour.reward.Rewards; -import dev.loki.loparkour.schematic.lpschem.LPSchematicManager; +import dev.loki.loparkour.bootstrap.PluginBootstrap; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.hook.papi.PAPIHook; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.reward.core.Rewards; +import dev.loki.loparkour.schematic.core.SchematicManager; import dev.loki.loparkour.storage.Storage; -import dev.loki.loparkour.world.Divider; -import dev.loki.loparkour.world.World; +import dev.loki.loparkour.world.core.Divider; +import dev.loki.loparkour.world.core.World; import dev.lolib.core.LoPlugin; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.bstats.bukkit.Metrics; -import org.bstats.charts.SimplePie; -import org.bstats.charts.SingleLineChart; - +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -41,45 +26,34 @@ import java.util.ArrayList; */ public final class LoParkour extends LoPlugin { - public static final String NAME = "&#FF6464LoParkour"; + public static final String NAME = "&#FF6464&lLoParkour&r"; public static final String PREFIX = NAME + " 񢩈» &#A0A0A0"; private static final Gson gson = new GsonBuilder() .setPrettyPrinting() .excludeFieldsWithoutExposeAnnotation() .create(); - - public static Gson getGson() { - return gson; - } + private static LoParkour instance; - private static LPSchematicManager schematicManager; + private static SchematicManager schematicManager; @Nullable private static PAPIHook placeholderHook; + public static Gson getGson() { + return gson; + } + public static void log(String message) { if (Config.CONFIG.getBoolean("debug")) { LoParkour.getPlugin().getLogger().info("[Debug] " + message); } } - /** - * @param child The file name. - * @return A file from within the plugin folder. - */ public static File getInFolder(String child) { return new File(instance.getDataFolder(), child); } - /** - * @return This plugin's {@link Logging} instance. - */ - - - /** - * @return The plugin instance. - */ public static LoParkour getPlugin() { return instance; } @@ -89,104 +63,32 @@ public final class LoParkour extends LoPlugin { return placeholderHook; } - public static LPSchematicManager getSchematicManager() { + public static SchematicManager getSchematicManager() { return schematicManager; } - + public void setSchematicManager(@NotNull SchematicManager manager) { + schematicManager = manager; + } + + public void setPlaceholderHook(@Nullable PAPIHook hook) { + placeholderHook = hook; + } @Override public void enable() { - instance = this; // must be first — Config enum accesses instance inside reload() - loadConfigs(); - loadSchematics(); - registerModes(); - registerHooks(); - setupWorld(); - registerEventsAndCommands(); - setupMetrics(); - } - - private void loadConfigs() { - Config.reload(true); - } - - private void loadSchematics() { - schematicManager = new LPSchematicManager(); - schematicManager.loadAll(); - } - - private void registerModes() { - Registry.register(new DefaultMode()); - Registry.register(new SpectatorMode()); - Registry.register(new SpeedrunMode()); - Registry.register(new ElytraMode()); - Registry.register(new RaceMode()); - Registry.register(new CoopMode()); - Registry.register(new GravityShiftMode()); - Registry.register(new HardcoreMode()); - Modes.init(); - } - - private void registerHooks() { - if (getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) { - getLogger().info("Registered Holographic Displays hook"); - HoloHook.init(); - } - if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) { - getLogger().info("Registered PlaceholderAPI hook"); - placeholderHook = new PAPIHook(); - placeholderHook.register(); - } - if (Config.CONFIG.getBoolean("bungeecord.enabled")) { - getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); - getLogger().info("Registered BungeeCord hook"); - } - } - - private void setupWorld() { - if (!Config.CONFIG.getBoolean("joining")) { - return; - } - - // World creation MUST happen on the main thread (Bukkit API requirement) - try { - loLogger().info("Creating parkour world..."); - World.create(); - loLogger().info("Parkour world created successfully!"); - } catch (Exception ex) { - loLogger().error("Failed to create parkour world", ex); - } - } - - private void registerEventsAndCommands() { - dev.lolib.gui.GUIManager.init(this); // required for LoLib GUI click handling - - var pm = getServer().getPluginManager(); - pm.registerEvents(new dev.loki.loparkour.listener.PlayerConnectionListener(), this); - pm.registerEvents(new dev.loki.loparkour.listener.ParkourRestrictionListener(), this); - pm.registerEvents(new dev.loki.loparkour.listener.SchematicWandListener(), this); - - LoParkourCommand cmd = new LoParkourCommand(); - getCommand("LoParkour").setExecutor(cmd); - getCommand("LoParkour").setTabCompleter(cmd); - } - - private void setupMetrics() { - Metrics metrics = new Metrics(this, 29754); - metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL))); - metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED))); - metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount()))); - metrics.addCustomChart(new SingleLineChart("player_joins", () -> { - int joins = UserRegistry.getJoinCount(); - return joins; - })); + instance = this; + PluginBootstrap.enable(this); } @Override public void disable() { + PluginBootstrap.disable(this); + } + + /** Called from {@link PluginBootstrap} on shutdown. */ + public void runShutdownSequence() { try { - // Clear all sessions BEFORE leaving players to prevent memory leaks new ArrayList<>(Divider.sections.keySet()).forEach(session -> { try { session.onAllPlayersLeft(); @@ -194,23 +96,19 @@ public final class LoParkour extends LoPlugin { getLogger().warning("Error cleaning up session: " + e.getMessage()); } }); - - // Remove all sessions from Divider + new ArrayList<>(Divider.sections.keySet()).forEach(Divider::remove); - - // Now leave all players + for (ParkourUser user : ParkourUser.getUsers()) { ParkourUser.leave(user); } - // write all LoParkour gamemodes (may be null if enable failed) if (Modes.DEFAULT != null && Modes.DEFAULT.getLeaderboard() != null) { Modes.DEFAULT.getLeaderboard().write(false); } - // Clear rewards to prevent memory leaks - dev.loki.loparkour.reward.Rewards.clear(); - + Rewards.clear(); + dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.shutdown(); Storage.close(); World.delete(); } catch (Throwable t) { diff --git a/src/main/java/dev/loki/loparkour/adaptive/bootstrap/AdaptiveServices.java b/src/main/java/dev/loki/loparkour/adaptive/bootstrap/AdaptiveServices.java new file mode 100644 index 0000000..3e377a9 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/bootstrap/AdaptiveServices.java @@ -0,0 +1,172 @@ +package dev.loki.loparkour.adaptive.bootstrap; + +import dev.loki.loparkour.adaptive.core.AdaptiveDifficulty; +import dev.loki.loparkour.adaptive.core.MetricsCollector; +import dev.loki.loparkour.adaptive.model.AdaptiveConfig; +import dev.loki.loparkour.adaptive.model.SkillRating; +import dev.loki.loparkour.adaptive.storage.FileStatsStorage; +import dev.loki.loparkour.adaptive.storage.SQLStatsStorage; +import dev.loki.loparkour.adaptive.storage.StatsRepository; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.core.model.Profile; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.storage.Storage; +import dev.loki.loparkour.storage.sql.StorageSQL; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Bootstrap for the adaptive difficulty subsystem. + */ +public final class AdaptiveServices { + + @Nullable + private static Plugin plugin; + @Nullable + private static StatsRepository repository; + @Nullable + private static MetricsCollector metricsCollector; + + private AdaptiveServices() { + } + + public static void init(@NotNull Plugin pluginInstance) { + plugin = pluginInstance; + AdaptiveConfig.init(); + if (!AdaptiveConfig.isEnabled()) { + return; + } + + Runnable setup = () -> startCollector(createRepository()); + if (Option.SQL) { + Storage.runWhenReady(setup); + } else { + setup.run(); + } + } + + public static void reload() { + shutdownCollector(); + AdaptiveConfig.reload(); + if (plugin != null && AdaptiveConfig.isEnabled()) { + Runnable setup = () -> startCollector(createRepository()); + if (Option.SQL) { + Storage.runWhenReady(setup); + } else { + setup.run(); + } + } + } + + public static void attachToGenerator(@NotNull ParkourGenerator generator) { + if (metricsCollector != null) { + generator.registerEventListener(metricsCollector); + } + } + + /** + * Applies adaptive difficulty weights to a profile being built for a generator. + */ + public static void applyAdaptiveProfile(@NotNull ParkourGenerator generator, @NotNull Profile profile) { + if (!AdaptiveConfig.isEnabled() || metricsCollector == null) { + return; + } + + double target = AdaptiveDifficulty.resolveTargetDifficulty(generator, AdaptiveServices::resolveSkillRating); + AdaptiveDifficulty.applyToProfile(profile, target); + } + + /** + * Updates stored skill rating from current session metrics after a fall. + */ + public static void persistRatingAfterFall(@NotNull ParkourPlayer player) { + if (!AdaptiveConfig.isEnabled() || repository == null || metricsCollector == null) { + return; + } + + UUID uuid = player.getUUID(); + SkillRating session = AdaptiveDifficulty.analyzeSession(player, metricsCollector); + SkillRating stored = repository.loadSkillRating(uuid); + + if (stored != null) { + stored.updateRating(session.getRating(), AdaptiveConfig.getRatingWeight()); + repository.saveSkillRating(stored); + } else { + repository.saveSkillRating(session); + } + } + + /** + * Rebuilds generator profile/chances from latest ratings (call once per fall event). + */ + public static void refreshGeneratorDifficulty(@NotNull ParkourGenerator generator) { + if (!AdaptiveConfig.isEnabled()) { + return; + } + generator.profileManager.recalculateProfile(); + } + + public static void unloadPlayer(@NotNull UUID playerUuid) { + if (metricsCollector != null) { + metricsCollector.unloadPlayer(playerUuid); + } + } + + public static void shutdown() { + shutdownCollector(); + repository = null; + } + + @NotNull + private static SkillRating resolveSkillRating(@NotNull ParkourPlayer player) { + UUID uuid = player.getUUID(); + SkillRating stored = repository != null ? repository.loadSkillRating(uuid) : null; + + if (metricsCollector == null) { + return stored != null ? stored : new SkillRating(uuid); + } + + var metrics = metricsCollector.getMetrics(uuid); + if (metrics.getTotalJumps() < AdaptiveConfig.getMinSessions()) { + if (stored != null && stored.isReliable()) { + return stored; + } + return stored != null ? stored : new SkillRating(uuid); + } + + SkillRating session = AdaptiveDifficulty.analyzeSession(player, metricsCollector); + if (stored == null) { + return session; + } + return AdaptiveDifficulty.mergeSessionRating(stored, session); + } + + @NotNull + private static StatsRepository createRepository() { + if (Option.SQL && StorageSQL.isConnected()) { + return new SQLStatsStorage(StorageSQL.getConnectionManager()); + } + return new FileStatsStorage(); + } + + private static void startCollector(@NotNull StatsRepository repo) { + if (plugin == null) { + return; + } + repository = repo; + metricsCollector = new MetricsCollector(plugin, repo); + metricsCollector.startAutoSave(); + } + + private static void shutdownCollector() { + if (metricsCollector != null) { + metricsCollector.stopAutoSave(); + metricsCollector.saveAllMetrics(); + metricsCollector = null; + } + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/AdaptiveDifficulty.java b/src/main/java/dev/loki/loparkour/adaptive/core/AdaptiveDifficulty.java new file mode 100644 index 0000000..407dbff --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/core/AdaptiveDifficulty.java @@ -0,0 +1,90 @@ +package dev.loki.loparkour.adaptive.core; + +import dev.loki.loparkour.adaptive.model.AdaptiveConfig; +import dev.loki.loparkour.adaptive.model.SkillRating; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.core.model.Profile; +import dev.loki.loparkour.player.core.ParkourPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * Applies adaptive difficulty to a generator {@link Profile}. + */ +public final class AdaptiveDifficulty { + + private static final DifficultyCalculator CALCULATOR = new DifficultyCalculator(); + private static final DifficultyAdjuster ADJUSTER = new DifficultyAdjuster(); + private static final SkillAnalyzer ANALYZER = new SkillAnalyzer(); + + private static final Set DIFFICULTY_PREFIXES = Set.of( + "distance.", "height.", "type." + ); + + private AdaptiveDifficulty() { + } + + /** + * Merges difficulty-related keys from {@code adjusted} into {@code target}. + */ + public static void mergeDifficultySettings(@NotNull Profile adjusted, @NotNull Profile target) { + for (String key : adjusted.settings.keySet()) { + if (isDifficultyKey(key)) { + target.set(key, adjusted.get(key).value()); + } + } + } + + public static double resolveTargetDifficulty( + @NotNull ParkourGenerator generator, + @NotNull SkillRatingResolver resolver) { + var players = generator.getPlayers(); + if (players.isEmpty()) { + return 0.5; + } + + double weightedSum = 0.0; + double weightTotal = 0.0; + + for (ParkourPlayer player : players) { + SkillRating rating = resolver.resolve(player); + double target = CALCULATOR.calculateTargetDifficulty(rating); + double weight = Math.max(0.1, rating.getConfidence()); + weightedSum += target * weight; + weightTotal += weight; + } + + return weightTotal > 0 ? weightedSum / weightTotal : 0.5; + } + + @NotNull + public static SkillRating mergeSessionRating(@NotNull SkillRating stored, @NotNull SkillRating sessionSample) { + double weight = AdaptiveConfig.getRatingWeight(); + double merged = (stored.getRating() * (1.0 - weight)) + (sessionSample.getRating() * weight); + double confidence = Math.max(stored.getConfidence(), sessionSample.getConfidence()); + return new SkillRating(stored.getPlayerUuid(), merged, confidence, stored.getSessionsCount() + 1); + } + + @NotNull + public static SkillRating analyzeSession(@NotNull ParkourPlayer player, @NotNull MetricsCollector collector) { + return ANALYZER.analyzeSkill(collector.getMetrics(player.getUUID())); + } + + public static void applyToProfile( + @NotNull Profile baseProfile, + double targetDifficulty) { + Profile adjusted = ADJUSTER.adjustProfile(baseProfile, targetDifficulty); + mergeDifficultySettings(adjusted, baseProfile); + } + + private static boolean isDifficultyKey(@NotNull String key) { + return DIFFICULTY_PREFIXES.stream().anyMatch(key::startsWith); + } + + @FunctionalInterface + public interface SkillRatingResolver { + @NotNull + SkillRating resolve(@NotNull ParkourPlayer player); + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyAdjuster.java b/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyAdjuster.java new file mode 100644 index 0000000..c2abad0 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyAdjuster.java @@ -0,0 +1,182 @@ +package dev.loki.loparkour.adaptive.core; + +import dev.loki.loparkour.generator.core.model.Profile; +import org.jetbrains.annotations.NotNull; + +/** + * Adjusts difficulty profile parameters based on target difficulty. + * Modifies distance, height, and special block chances to match player skill. + * + * Difficulty scale (0.0-1.0): + * - 0.0-0.3: Easy (short jumps, minimal height variation) + * - 0.3-0.6: Medium (balanced jumps, moderate height) + * - 0.6-0.8: Hard (longer jumps, more height variation) + * - 0.8-1.0: Expert (maximum distance, high variation) + * + * Thread-safe: Stateless, all methods are pure functions. + */ +public class DifficultyAdjuster { + + // Distance bounds (blocks) + private static final int MIN_DISTANCE = 1; + private static final int MAX_DISTANCE = 4; + + // Height bounds (blocks) + private static final int MIN_HEIGHT = -1; + private static final int MAX_HEIGHT = 2; + + /** + * Adjusts a profile to match the target difficulty. + * Creates a new profile with modified parameters. + * + * @param baseProfile The base profile to adjust + * @param targetDifficulty Target difficulty (0.0-1.0) + * @return New profile with adjusted parameters + */ + @NotNull + public Profile adjustProfile(@NotNull Profile baseProfile, double targetDifficulty) { + Profile adjusted = new Profile(); + + // Copy base settings + copyBaseSettings(baseProfile, adjusted); + + // Adjust difficulty parameters + adjustDistanceChances(adjusted, targetDifficulty); + adjustHeightChances(adjusted, targetDifficulty); + adjustSpecialFrequency(adjusted, targetDifficulty); + + return adjusted; + } + + /** + * Copies non-difficulty settings from base profile. + */ + private void copyBaseSettings(@NotNull Profile source, @NotNull Profile target) { + String[] settingsKeys = {"min-y", "max-y", "heading", "first-block-material"}; + + for (String key : settingsKeys) { + if (source.settings.containsKey(key)) { + target.set(key, source.get(key).value()); + } + } + } + + /** + * Adjusts distance chances based on difficulty. + */ + private void adjustDistanceChances(@NotNull Profile profile, double difficulty) { + double[] weights = DifficultyWeights.calculateDistanceWeights(difficulty); + + profile.set("distance.min", String.valueOf(MIN_DISTANCE)); + profile.set("distance.max", String.valueOf(MAX_DISTANCE)); + + for (int distance = MIN_DISTANCE; distance <= MAX_DISTANCE; distance++) { + int index = distance - MIN_DISTANCE; + int chance = (int) Math.round(weights[index] * 100); + profile.set("distance.chances." + distance, String.valueOf(chance)); + } + } + + /** + * Adjusts height variation based on difficulty. + */ + private void adjustHeightChances(@NotNull Profile profile, double difficulty) { + double[] weights = DifficultyWeights.calculateHeightWeights(difficulty); + + profile.set("height.min", String.valueOf(MIN_HEIGHT)); + profile.set("height.max", String.valueOf(MAX_HEIGHT)); + + int index = 0; + for (int height = MIN_HEIGHT; height <= MAX_HEIGHT; height++) { + int chance = (int) Math.round(weights[index] * 100); + profile.set("height.chances." + height, String.valueOf(chance)); + index++; + } + } + + /** + * Adjusts special block frequency based on difficulty. + */ + private void adjustSpecialFrequency(@NotNull Profile profile, double difficulty) { + double specialPercent = DifficultyWeights.calculateSpecialFrequency(difficulty); + + int defaultPercent = (int) Math.round(100.0 - specialPercent); + int specialChance = (int) Math.round(specialPercent * 0.75); + int schematicChance = (int) Math.round(specialPercent * 0.25); + + profile.set("type.default", String.valueOf(defaultPercent)); + profile.set("type.special", String.valueOf(specialChance)); + profile.set("type.schematic", String.valueOf(schematicChance)); + } + + /** + * Calculates the effective difficulty of a profile. + * Inverse operation of adjustProfile. + * + * @param profile The profile to analyze + * @return Estimated difficulty (0.0-1.0) + */ + public double estimateDifficulty(@NotNull Profile profile) { + double distanceDifficulty = estimateDistanceDifficulty(profile); + double heightDifficulty = estimateHeightDifficulty(profile); + double specialDifficulty = estimateSpecialDifficulty(profile); + + // Weighted average + return (distanceDifficulty * 0.5) + (heightDifficulty * 0.3) + (specialDifficulty * 0.2); + } + + private double estimateDistanceDifficulty(@NotNull Profile profile) { + double avgDistance = 0.0; + double totalWeight = 0.0; + + for (int distance = MIN_DISTANCE; distance <= MAX_DISTANCE; distance++) { + String key = "distance.chances." + distance; + if (profile.settings.containsKey(key)) { + double weight = profile.get(key).asDouble(); + avgDistance += distance * weight; + totalWeight += weight; + } + } + + if (totalWeight > 0) { + avgDistance /= totalWeight; + } + + // Map average distance to difficulty (1.0 = easy, 4.0 = hard) + return (avgDistance - 1.0) / 3.0; + } + + private double estimateHeightDifficulty(@NotNull Profile profile) { + double variation = 0.0; + double totalWeight = 0.0; + + for (int height = MIN_HEIGHT; height <= MAX_HEIGHT; height++) { + String key = "height.chances." + height; + if (profile.settings.containsKey(key)) { + double weight = profile.get(key).asDouble(); + variation += Math.abs(height) * weight; + totalWeight += weight; + } + } + + if (totalWeight > 0) { + variation /= totalWeight; + } + + // Map variation to difficulty (0.0 = flat, 1.5 = varied) + return Math.min(1.0, variation / 1.5); + } + + private double estimateSpecialDifficulty(@NotNull Profile profile) { + if (!profile.settings.containsKey("type.special")) { + return 0.0; + } + + double specialPercent = profile.get("type.special").asDouble(); + double minFrequency = 5.0; + double maxFrequency = 30.0; + + // Map special percentage to difficulty + return (specialPercent - minFrequency) / (maxFrequency - minFrequency); + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyCalculator.java b/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyCalculator.java new file mode 100644 index 0000000..645c6b4 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyCalculator.java @@ -0,0 +1,141 @@ +package dev.loki.loparkour.adaptive.core; + +import dev.loki.loparkour.adaptive.model.SkillRating; +import org.jetbrains.annotations.NotNull; + +/** + * Calculates target difficulty based on player skill rating. + * Converts skill ratings into normalized difficulty values (0.0-1.0). + * + * Formula: difficulty = min(1.0, rating / 2.0) + * - Rating 0.0 = 0.0 difficulty (easiest) + * - Rating 1.0 = 0.5 difficulty (medium) + * - Rating 2.0+ = 1.0 difficulty (hardest) + * + * Thread-safe: Stateless, all methods are pure functions. + */ +public class DifficultyCalculator { + + private static final double MAX_RATING_FOR_FULL_DIFFICULTY = 2.0; + private static final double MIN_DIFFICULTY = 0.0; + private static final double MAX_DIFFICULTY = 1.0; + + /** + * Calculates the target difficulty for a player based on their skill rating. + * + * @param skillRating The player's skill rating + * @return Target difficulty (0.0-1.0) + */ + public double calculateTargetDifficulty(@NotNull SkillRating skillRating) { + double rating = skillRating.getRating(); + double confidence = skillRating.getConfidence(); + + // Calculate base difficulty from rating + double baseDifficulty = calculateBaseDifficulty(rating); + + // Apply confidence adjustment (lower confidence = more conservative difficulty) + double adjustedDifficulty = applyConfidenceAdjustment(baseDifficulty, confidence); + + return clampDifficulty(adjustedDifficulty); + } + + /** + * Calculates base difficulty from skill rating. + * Linear mapping: rating 2.0 = difficulty 1.0 + * + * @param rating The skill rating + * @return Base difficulty (0.0-1.0+) + */ + private double calculateBaseDifficulty(double rating) { + return rating / MAX_RATING_FOR_FULL_DIFFICULTY; + } + + /** + * Applies confidence adjustment to difficulty. + * Low confidence ratings result in more conservative difficulty. + * + * @param baseDifficulty The base difficulty + * @param confidence The confidence level (0.0-1.0) + * @return Adjusted difficulty + */ + private double applyConfidenceAdjustment(double baseDifficulty, double confidence) { + // If confidence is low, reduce difficulty to be safe + // confidence 0.0 = 50% of base difficulty + // confidence 1.0 = 100% of base difficulty + double confidenceMultiplier = 0.5 + (confidence * 0.5); + return baseDifficulty * confidenceMultiplier; + } + + /** + * Clamps difficulty to valid range [0.0, 1.0]. + * + * @param difficulty The difficulty value + * @return Clamped difficulty + */ + private double clampDifficulty(double difficulty) { + return Math.max(MIN_DIFFICULTY, Math.min(MAX_DIFFICULTY, difficulty)); + } + + /** + * Calculates the recommended rating for a target difficulty. + * Inverse of calculateTargetDifficulty (without confidence adjustment). + * + * @param targetDifficulty The desired difficulty (0.0-1.0) + * @return Recommended skill rating + */ + public double calculateRecommendedRating(double targetDifficulty) { + double clampedDifficulty = clampDifficulty(targetDifficulty); + return clampedDifficulty * MAX_RATING_FOR_FULL_DIFFICULTY; + } + + /** + * Determines if a player is ready for increased difficulty. + * Checks if rating is significantly above current difficulty level. + * + * @param skillRating The player's skill rating + * @param currentDifficulty The current difficulty (0.0-1.0) + * @return true if player is ready for harder content + */ + public boolean isReadyForIncrease(@NotNull SkillRating skillRating, double currentDifficulty) { + double targetDifficulty = calculateTargetDifficulty(skillRating); + double threshold = 0.1; // 10% increase threshold + + return targetDifficulty > currentDifficulty + threshold + && skillRating.getConfidence() >= 0.5; // Require reasonable confidence + } + + /** + * Determines if difficulty should be decreased for a player. + * Checks if rating is significantly below current difficulty level. + * + * @param skillRating The player's skill rating + * @param currentDifficulty The current difficulty (0.0-1.0) + * @return true if difficulty should be reduced + */ + public boolean shouldDecrease(@NotNull SkillRating skillRating, double currentDifficulty) { + double targetDifficulty = calculateTargetDifficulty(skillRating); + double threshold = 0.15; // 15% decrease threshold (more aggressive than increase) + + return targetDifficulty < currentDifficulty - threshold; + } + + /** + * Calculates a smooth difficulty transition. + * Prevents sudden difficulty spikes by limiting change rate. + * + * @param currentDifficulty The current difficulty + * @param targetDifficulty The target difficulty + * @param maxChange Maximum allowed change per adjustment + * @return Smoothed difficulty + */ + public double smoothTransition(double currentDifficulty, double targetDifficulty, double maxChange) { + double difference = targetDifficulty - currentDifficulty; + + if (Math.abs(difference) <= maxChange) { + return targetDifficulty; + } + + // Move towards target by maxChange + return currentDifficulty + Math.signum(difference) * maxChange; + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyWeights.java b/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyWeights.java new file mode 100644 index 0000000..8ba815e --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/core/DifficultyWeights.java @@ -0,0 +1,98 @@ +package dev.loki.loparkour.adaptive.core; + +/** + * Calculates weight distributions for different difficulty levels. + * Contains formulas for distance, height, and special block distributions. + * + * Thread-safe: Stateless, all methods are pure functions. + */ +class DifficultyWeights { + + /** + * Calculates distance weight distribution for given difficulty. + * + * @param difficulty Target difficulty (0.0-1.0) + * @return Array of weights [dist1, dist2, dist3, dist4] + */ + static double[] calculateDistanceWeights(double difficulty) { + double[] weights = new double[4]; + + if (difficulty < 0.3) { + // Easy: favor short jumps (1-2 blocks) + weights[0] = 0.40; // 1 block + weights[1] = 0.40; // 2 blocks + weights[2] = 0.15; // 3 blocks + weights[3] = 0.05; // 4 blocks + } else if (difficulty < 0.6) { + // Medium: balanced distribution + weights[0] = 0.20; // 1 block + weights[1] = 0.35; // 2 blocks + weights[2] = 0.30; // 3 blocks + weights[3] = 0.15; // 4 blocks + } else if (difficulty < 0.8) { + // Hard: favor longer jumps + weights[0] = 0.10; // 1 block + weights[1] = 0.25; // 2 blocks + weights[2] = 0.40; // 3 blocks + weights[3] = 0.25; // 4 blocks + } else { + // Expert: maximum challenge + weights[0] = 0.05; // 1 block + weights[1] = 0.15; // 2 blocks + weights[2] = 0.35; // 3 blocks + weights[3] = 0.45; // 4 blocks + } + + return weights; + } + + /** + * Calculates height weight distribution for given difficulty. + * + * @param difficulty Target difficulty (0.0-1.0) + * @return Array of weights [-1, 0, 1, 2] + */ + static double[] calculateHeightWeights(double difficulty) { + double[] weights = new double[4]; + + if (difficulty < 0.3) { + // Easy: mostly flat, minimal drops + weights[0] = 0.10; // -1 (down) + weights[1] = 0.60; // 0 (flat) + weights[2] = 0.25; // +1 (up) + weights[3] = 0.05; // +2 (high up) + } else if (difficulty < 0.6) { + // Medium: balanced variation + weights[0] = 0.20; // -1 + weights[1] = 0.40; // 0 + weights[2] = 0.30; // +1 + weights[3] = 0.10; // +2 + } else if (difficulty < 0.8) { + // Hard: more variation + weights[0] = 0.25; // -1 + weights[1] = 0.30; // 0 + weights[2] = 0.30; // +1 + weights[3] = 0.15; // +2 + } else { + // Expert: maximum variation + weights[0] = 0.30; // -1 + weights[1] = 0.20; // 0 + weights[2] = 0.30; // +1 + weights[3] = 0.20; // +2 + } + + return weights; + } + + /** + * Calculates special block frequency for given difficulty. + * + * @param difficulty Target difficulty (0.0-1.0) + * @return Special block percentage (5.0-30.0) + */ + static double calculateSpecialFrequency(double difficulty) { + double minFrequency = 5.0; + double maxFrequency = 30.0; + return minFrequency + (maxFrequency - minFrequency) * difficulty; + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/MetricsCollector.java b/src/main/java/dev/loki/loparkour/adaptive/core/MetricsCollector.java new file mode 100644 index 0000000..4658897 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/core/MetricsCollector.java @@ -0,0 +1,214 @@ +package dev.loki.loparkour.adaptive.core; + +import dev.loki.loparkour.adaptive.model.PlayerMetrics; +import dev.loki.loparkour.adaptive.storage.StatsRepository; +import dev.loki.loparkour.generator.core.coordinator.GeneratorEventListener; +import dev.loki.loparkour.generator.jump.calculation.JumpType; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Collects and manages player performance metrics. + * + * Responsibilities: + * - Track player jumps, falls, and scoring events + * - Detect near-miss jumps (distance > 3.5 blocks) + * - Calculate average time per block + * - Maintain jump type statistics + * - Auto-save metrics every 5 minutes + * + * Thread-safe: Uses ConcurrentHashMap for player metrics cache. + */ +public class MetricsCollector implements GeneratorEventListener { + + private static final double NEAR_MISS_THRESHOLD = 3.5; + private static final long AUTO_SAVE_INTERVAL_TICKS = 20 * 60 * 5; // 5 minutes + + private final Plugin plugin; + private final StatsRepository repository; + private final Map metricsCache; + private final Map lastBlockCache; + private final Map lastBlockTimeCache; + + private BukkitRunnable autoSaveTask; + + /** + * Create a new MetricsCollector. + * + * @param plugin The plugin instance for scheduling tasks + * @param repository The repository for persisting metrics + */ + public MetricsCollector(@NotNull Plugin plugin, @NotNull StatsRepository repository) { + this.plugin = plugin; + this.repository = repository; + this.metricsCache = new ConcurrentHashMap<>(); + this.lastBlockCache = new ConcurrentHashMap<>(); + this.lastBlockTimeCache = new ConcurrentHashMap<>(); + } + + /** + * Start the auto-save task. + * Should be called when the collector is initialized. + */ + public void startAutoSave() { + if (autoSaveTask != null) { + autoSaveTask.cancel(); + } + + autoSaveTask = new BukkitRunnable() { + @Override + public void run() { + saveAllMetrics(); + } + }; + + autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS); + } + + /** + * Stop the auto-save task. + * Should be called when the plugin is disabled. + */ + public void stopAutoSave() { + if (autoSaveTask != null) { + autoSaveTask.cancel(); + autoSaveTask = null; + } + } + + /** + * Get or create metrics for a player. + * + * @param playerUuid The player's UUID + * @return The player's metrics + */ + @NotNull + public PlayerMetrics getMetrics(@NotNull UUID playerUuid) { + return metricsCache.computeIfAbsent(playerUuid, uuid -> { + // Try to load from repository + PlayerMetrics loaded = repository.loadMetrics(uuid); + if (loaded != null) { + return loaded; + } + // Create new metrics if not found + return new PlayerMetrics(uuid); + }); + } + + /** + * Called when a player lands on a block. + * Detects near-miss jumps and updates timing metrics. + * + * @param player The player who landed + * @param block The block the player landed on + * @param distance The distance of the jump + */ + public void onBlockLand(@NotNull Player player, @NotNull Block block, double distance) { + UUID uuid = player.getUniqueId(); + PlayerMetrics metrics = getMetrics(uuid); + + // Calculate time since last block + long currentTime = System.currentTimeMillis(); + Long lastTime = lastBlockTimeCache.get(uuid); + if (lastTime != null) { + long timeDiff = currentTime - lastTime; + // Record jump with timing (use "normal" as default type) + metrics.recordJump("normal", timeDiff); + } + lastBlockTimeCache.put(uuid, currentTime); + + // Detect near-miss (long jump that succeeded) + if (distance > NEAR_MISS_THRESHOLD) { + metrics.recordNearMiss(); + } + + // Update last block + lastBlockCache.put(uuid, block); + } + + @Override + public void onBlockGenerated(@NotNull Player player, @NotNull Block block, @NotNull JumpType jumpType, double distance) { + // Not used for metrics collection, but required by interface + } + + @Override + public void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance) { + onBlockLand(player, block, distance); + } + + @Override + public void onPlayerFall(@NotNull Player player, @Nullable Block lastBlock) { + UUID uuid = player.getUniqueId(); + PlayerMetrics metrics = getMetrics(uuid); + + // Save metrics on fall (important event) + repository.saveMetrics(metrics); + } + + @Override + public void onJump(@NotNull Player player, @NotNull JumpType jumpType) { + UUID uuid = player.getUniqueId(); + PlayerMetrics metrics = getMetrics(uuid); + + // Calculate time since last block + long currentTime = System.currentTimeMillis(); + Long lastTime = lastBlockTimeCache.get(uuid); + long timeDiff = (lastTime != null) ? (currentTime - lastTime) : 0; + + // Record jump with type and timing + metrics.recordJump(jumpType.name().toLowerCase(), timeDiff); + } + + /** + * Save metrics for a specific player. + * + * @param playerUuid The player's UUID + */ + public void saveMetrics(@NotNull UUID playerUuid) { + PlayerMetrics metrics = metricsCache.get(playerUuid); + if (metrics != null) { + repository.saveMetrics(metrics); + } + } + + /** + * Save all cached metrics. + * Called by auto-save task and on plugin disable. + */ + public void saveAllMetrics() { + for (PlayerMetrics metrics : metricsCache.values()) { + repository.saveMetrics(metrics); + } + } + + /** + * Remove a player from the cache. + * Should be called when a player leaves the server. + * + * @param playerUuid The player's UUID + */ + public void unloadPlayer(@NotNull UUID playerUuid) { + // Save before removing + saveMetrics(playerUuid); + metricsCache.remove(playerUuid); + lastBlockCache.remove(playerUuid); + lastBlockTimeCache.remove(playerUuid); + } + + /** + * Get the number of cached players. + * + * @return The cache size + */ + public int getCacheSize() { + return metricsCache.size(); + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/core/SkillAnalyzer.java b/src/main/java/dev/loki/loparkour/adaptive/core/SkillAnalyzer.java new file mode 100644 index 0000000..c664f12 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/core/SkillAnalyzer.java @@ -0,0 +1,137 @@ +package dev.loki.loparkour.adaptive.core; + +import dev.loki.loparkour.adaptive.model.PlayerMetrics; +import dev.loki.loparkour.adaptive.model.SkillRating; +import org.jetbrains.annotations.NotNull; + +/** + * Analyzes player metrics and calculates skill ratings. + * Uses a formula that considers success rate, near misses, and consistency. + * + * Formula: rating = baseRating * (successRate^2) * (1 + nearMissBonus) * streakMultiplier + * - baseRating: 1.0 (baseline skill level) + * - successRate: percentage of successful jumps (0.0-1.0) + * - nearMissBonus: 0.1 per near miss (rewards precision) + * - streakMultiplier: 1 + (successfulJumps / 100) (rewards consistency) + * + * Thread-safe: Stateless, all methods are pure functions. + */ +public class SkillAnalyzer { + + private static final double BASE_RATING = 1.0; + private static final double NEAR_MISS_BONUS = 0.1; + private static final double STREAK_DIVISOR = 100.0; + private static final int MIN_JUMPS_FOR_CONFIDENCE = 10; + private static final int MAX_JUMPS_FOR_CONFIDENCE = 100; + + /** + * Analyzes player metrics and calculates a skill rating. + * + * @param metrics The player's performance metrics + * @return A skill rating with confidence level + */ + @NotNull + public SkillRating analyzeSkill(@NotNull PlayerMetrics metrics) { + double successRate = metrics.getSuccessRate(); + int nearMisses = metrics.getNearMissCount(); + int successfulJumps = metrics.getSuccessfulJumps(); + int totalJumps = metrics.getTotalJumps(); + + // Calculate rating components + double successComponent = calculateSuccessComponent(successRate); + double nearMissBonus = calculateNearMissBonus(nearMisses); + double streakMultiplier = calculateStreakMultiplier(successfulJumps); + + // Combine components + double rating = BASE_RATING * successComponent * nearMissBonus * streakMultiplier; + + // Calculate confidence based on sample size + double confidence = calculateConfidence(totalJumps); + + // Sessions count is 1 for current session analysis + int sessionsCount = 1; + + return new SkillRating(metrics.getPlayerUuid(), rating, confidence, sessionsCount); + } + + /** + * Calculates the success rate component of the rating. + * Uses squared success rate to emphasize consistency. + * + * @param successRate The player's success rate (0.0-1.0) + * @return Success component (0.0-1.0) + */ + private double calculateSuccessComponent(double successRate) { + return Math.pow(successRate, 2); + } + + /** + * Calculates the near miss bonus multiplier. + * Rewards players who land close to block edges (precision). + * + * @param nearMisses Number of near miss landings + * @return Near miss multiplier (1.0+) + */ + private double calculateNearMissBonus(int nearMisses) { + return 1.0 + (nearMisses * NEAR_MISS_BONUS); + } + + /** + * Calculates the streak multiplier based on successful jumps. + * Rewards consistent performance over time. + * + * @param successfulJumps Number of successful jumps + * @return Streak multiplier (1.0+) + */ + private double calculateStreakMultiplier(int successfulJumps) { + return 1.0 + (successfulJumps / STREAK_DIVISOR); + } + + /** + * Calculates confidence level based on sample size. + * More jumps = higher confidence in the rating. + * + * @param totalJumps Total number of jumps attempted + * @return Confidence level (0.0-1.0) + */ + private double calculateConfidence(int totalJumps) { + if (totalJumps < MIN_JUMPS_FOR_CONFIDENCE) { + // Low confidence for small samples + return (double) totalJumps / MIN_JUMPS_FOR_CONFIDENCE; + } else if (totalJumps >= MAX_JUMPS_FOR_CONFIDENCE) { + // Maximum confidence reached + return 1.0; + } else { + // Linear interpolation between min and max + double range = MAX_JUMPS_FOR_CONFIDENCE - MIN_JUMPS_FOR_CONFIDENCE; + double progress = totalJumps - MIN_JUMPS_FOR_CONFIDENCE; + return progress / range; + } + } + + /** + * Estimates the minimum rating for a given success rate. + * Useful for UI display and difficulty selection. + * + * @param successRate The success rate (0.0-1.0) + * @return Estimated minimum rating + */ + public double estimateMinRating(double successRate) { + return BASE_RATING * calculateSuccessComponent(successRate); + } + + /** + * Estimates the maximum rating for a given success rate. + * Assumes high near miss count and long streak. + * + * @param successRate The success rate (0.0-1.0) + * @return Estimated maximum rating + */ + public double estimateMaxRating(double successRate) { + double successComponent = calculateSuccessComponent(successRate); + double maxNearMissBonus = calculateNearMissBonus(50); // Assume 50 near misses + double maxStreakMultiplier = calculateStreakMultiplier(MAX_JUMPS_FOR_CONFIDENCE); + + return BASE_RATING * successComponent * maxNearMissBonus * maxStreakMultiplier; + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/model/AdaptiveConfig.java b/src/main/java/dev/loki/loparkour/adaptive/model/AdaptiveConfig.java new file mode 100644 index 0000000..faac218 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/model/AdaptiveConfig.java @@ -0,0 +1,158 @@ +package dev.loki.loparkour.adaptive.model; + +import dev.loki.loparkour.config.core.Config; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Cached configuration for the adaptive difficulty system. + * Loads settings from config.yml and caches them for performance. + */ +public class AdaptiveConfig { + + private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private static volatile boolean initialized = false; + + // Configuration keys + private static final String KEY_ENABLED = "adaptive.enabled"; + private static final String KEY_MIN_SESSIONS = "adaptive.min-sessions"; + private static final String KEY_UPDATE_INTERVAL = "adaptive.update-interval"; + private static final String KEY_RATING_WEIGHT = "adaptive.rating-weight"; + private static final String KEY_CONFIDENCE_THRESHOLD = "adaptive.confidence-threshold"; + private static final String KEY_NEAR_MISS_DISTANCE = "adaptive.near-miss-distance"; + + // Default values + private static final boolean DEFAULT_ENABLED = true; + private static final int DEFAULT_MIN_SESSIONS = 3; + private static final int DEFAULT_UPDATE_INTERVAL = 300; // 5 minutes in seconds + private static final double DEFAULT_RATING_WEIGHT = 0.15; + private static final double DEFAULT_CONFIDENCE_THRESHOLD = 0.3; + private static final double DEFAULT_NEAR_MISS_DISTANCE = 0.3; + + /** + * Initializes the configuration cache from config.yml. + * Should be called during plugin startup. + */ + public static void init() { + if (initialized) { + return; + } + + cache.put(KEY_ENABLED, Config.CONFIG.getBoolean(KEY_ENABLED, DEFAULT_ENABLED)); + cache.put(KEY_MIN_SESSIONS, Config.CONFIG.getInt(KEY_MIN_SESSIONS, DEFAULT_MIN_SESSIONS)); + cache.put(KEY_UPDATE_INTERVAL, Config.CONFIG.getInt(KEY_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL)); + cache.put(KEY_RATING_WEIGHT, configDouble(KEY_RATING_WEIGHT, DEFAULT_RATING_WEIGHT)); + cache.put(KEY_CONFIDENCE_THRESHOLD, configDouble(KEY_CONFIDENCE_THRESHOLD, DEFAULT_CONFIDENCE_THRESHOLD)); + cache.put(KEY_NEAR_MISS_DISTANCE, configDouble(KEY_NEAR_MISS_DISTANCE, DEFAULT_NEAR_MISS_DISTANCE)); + + initialized = true; + } + + /** + * Reloads configuration from config.yml. + * Call this after config reload. + */ + public static void reload() { + initialized = false; + cache.clear(); + init(); + } + + /** + * Checks if adaptive difficulty system is enabled. + * + * @return True if enabled + */ + public static boolean isEnabled() { + ensureInitialized(); + return (boolean) cache.getOrDefault(KEY_ENABLED, DEFAULT_ENABLED); + } + + /** + * Gets minimum number of sessions before adaptive difficulty activates. + * + * @return Minimum sessions count + */ + public static int getMinSessions() { + ensureInitialized(); + return (int) cache.getOrDefault(KEY_MIN_SESSIONS, DEFAULT_MIN_SESSIONS); + } + + /** + * Gets the interval (in seconds) between automatic stats updates. + * + * @return Update interval in seconds + */ + public static int getUpdateInterval() { + ensureInitialized(); + return (int) cache.getOrDefault(KEY_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL); + } + + /** + * Gets the weight for rating updates (how much new performance affects rating). + * + * @return Rating weight (0.0 to 1.0) + */ + public static double getRatingWeight() { + ensureInitialized(); + Object value = cache.get(KEY_RATING_WEIGHT); + if (value instanceof Double) { + return (double) value; + } + return DEFAULT_RATING_WEIGHT; + } + + /** + * Gets the confidence threshold for reliable ratings. + * + * @return Confidence threshold (0.0 to 1.0) + */ + public static double getConfidenceThreshold() { + ensureInitialized(); + Object value = cache.get(KEY_CONFIDENCE_THRESHOLD); + if (value instanceof Double) { + return (double) value; + } + return DEFAULT_CONFIDENCE_THRESHOLD; + } + + /** + * Gets the distance threshold for near-miss detection. + * + * @return Near-miss distance in blocks + */ + public static double getNearMissDistance() { + ensureInitialized(); + Object value = cache.get(KEY_NEAR_MISS_DISTANCE); + if (value instanceof Double) { + return (double) value; + } + return DEFAULT_NEAR_MISS_DISTANCE; + } + + private static void ensureInitialized() { + if (!initialized) { + init(); + } + } + + private static double configDouble(@NotNull String key, double defaultValue) { + return Config.CONFIG.isPath(key) ? Config.CONFIG.getDouble(key) : defaultValue; + } + + /** + * Gets a custom config value with caching. + * + * @param key The config key + * @param defaultValue Default value if key not found + * @return The config value + */ + @NotNull + public static T get(@NotNull String key, @NotNull T defaultValue) { + ensureInitialized(); + @SuppressWarnings("unchecked") + T value = (T) cache.get(key); + return value != null ? value : defaultValue; + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/model/PlayerMetrics.java b/src/main/java/dev/loki/loparkour/adaptive/model/PlayerMetrics.java new file mode 100644 index 0000000..cae187a --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/model/PlayerMetrics.java @@ -0,0 +1,159 @@ +package dev.loki.loparkour.adaptive.model; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Stores detailed player performance metrics for adaptive difficulty calculation. + * Tracks timing, jump patterns, and near-miss statistics. + */ +public class PlayerMetrics { + + private final UUID playerUuid; + private double avgTimePerBlock; + private final Map jumpTypeStats; + private int nearMissCount; + private int totalJumps; + private long lastUpdated; + + public PlayerMetrics(@NotNull UUID playerUuid) { + this.playerUuid = playerUuid; + this.avgTimePerBlock = 0.0; + this.jumpTypeStats = new HashMap<>(); + this.nearMissCount = 0; + this.totalJumps = 0; + this.lastUpdated = System.currentTimeMillis(); + } + + /** + * Records a successful jump with timing data. + * + * @param jumpType The type of jump (e.g., "normal", "diagonal", "momentum") + * @param timeMs Time taken for this jump in milliseconds + */ + public void recordJump(@NotNull String jumpType, long timeMs) { + jumpTypeStats.merge(jumpType, 1, Integer::sum); + totalJumps++; + + // Update rolling average + if (avgTimePerBlock == 0.0) { + avgTimePerBlock = timeMs; + } else { + avgTimePerBlock = (avgTimePerBlock * 0.9) + (timeMs * 0.1); + } + + lastUpdated = System.currentTimeMillis(); + } + + /** + * Records a near-miss event (landed close to block edge). + */ + public void recordNearMiss() { + nearMissCount++; + lastUpdated = System.currentTimeMillis(); + } + + /** + * Gets the frequency of a specific jump type. + * + * @param jumpType The jump type to query + * @return Percentage (0.0 to 1.0) of total jumps + */ + public double getJumpTypeFrequency(@NotNull String jumpType) { + if (totalJumps == 0) { + return 0.0; + } + return jumpTypeStats.getOrDefault(jumpType, 0) / (double) totalJumps; + } + + /** + * Gets near-miss rate as percentage of total jumps. + * + * @return Near-miss rate (0.0 to 1.0) + */ + public double getNearMissRate() { + if (totalJumps == 0) { + return 0.0; + } + return nearMissCount / (double) totalJumps; + } + + /** + * Calculates success rate based on total jumps. + * Assumes all recorded jumps are successful (falls are tracked separately). + * + * @return Success rate (0.0 to 1.0) + */ + public double getSuccessRate() { + // Since we only record successful jumps, success rate is based on + // the assumption that player has some failures not tracked here + // For now, return 1.0 if we have jumps, or use near-miss rate as proxy + if (totalJumps == 0) { + return 0.0; + } + // High near-miss rate indicates good performance + return Math.min(1.0, 0.7 + (getNearMissRate() * 0.3)); + } + + /** + * Gets the number of successful jumps (same as total jumps for now). + * + * @return Number of successful jumps + */ + public int getSuccessfulJumps() { + return totalJumps; + } + + // Getters + + @NotNull + public UUID getPlayerUuid() { + return playerUuid; + } + + public double getAvgTimePerBlock() { + return avgTimePerBlock; + } + + @NotNull + public Map getJumpTypeStats() { + return new HashMap<>(jumpTypeStats); + } + + public int getNearMissCount() { + return nearMissCount; + } + + public int getTotalJumps() { + return totalJumps; + } + + public long getLastUpdated() { + return lastUpdated; + } + + // Setters for deserialization + + public void setAvgTimePerBlock(double avgTimePerBlock) { + this.avgTimePerBlock = avgTimePerBlock; + } + + public void setNearMissCount(int nearMissCount) { + this.nearMissCount = nearMissCount; + } + + public void setTotalJumps(int totalJumps) { + this.totalJumps = totalJumps; + } + + public void setLastUpdated(long lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public void putJumpTypeStat(@NotNull String jumpType, int count) { + jumpTypeStats.put(jumpType, count); + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java b/src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java new file mode 100644 index 0000000..5d86565 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java @@ -0,0 +1,133 @@ +package dev.loki.loparkour.adaptive.model; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Represents a player's skill rating with confidence level. + * Used for adaptive difficulty calculation. + */ +public class SkillRating { + + private final UUID playerUuid; + private double rating; + private double confidence; + private int sessionsCount; + private long lastUpdated; + + /** + * Creates a new skill rating with default values. + * + * @param playerUuid The player's UUID + */ + public SkillRating(@NotNull UUID playerUuid) { + this.playerUuid = playerUuid; + this.rating = 1.0; + this.confidence = 0.0; + this.sessionsCount = 0; + this.lastUpdated = System.currentTimeMillis(); + } + + /** + * Creates a skill rating with specific values. + * + * @param playerUuid The player's UUID + * @param rating The skill rating (typically 0.5 to 2.0) + * @param confidence The confidence level (0.0 to 1.0) + * @param sessionsCount Number of sessions used to calculate this rating + */ + public SkillRating(@NotNull UUID playerUuid, double rating, double confidence, int sessionsCount) { + this.playerUuid = playerUuid; + this.rating = clampRating(rating); + this.confidence = clampConfidence(confidence); + this.sessionsCount = sessionsCount; + this.lastUpdated = System.currentTimeMillis(); + } + + /** + * Updates the skill rating based on performance. + * + * @param performanceScore Performance score from recent session (0.0 to 2.0) + * @param weight Weight of this update (0.0 to 1.0) + */ + public void updateRating(double performanceScore, double weight) { + rating = (rating * (1.0 - weight)) + (performanceScore * weight); + rating = clampRating(rating); + + sessionsCount++; + confidence = Math.min(1.0, confidence + 0.05); + + lastUpdated = System.currentTimeMillis(); + } + + /** + * Gets the effective rating adjusted by confidence. + * Low confidence ratings are pulled toward 1.0 (neutral). + * + * @return Confidence-adjusted rating + */ + public double getEffectiveRating() { + return (rating * confidence) + (1.0 * (1.0 - confidence)); + } + + /** + * Checks if this rating is reliable enough for adaptive difficulty. + * + * @return True if confidence is above threshold + */ + public boolean isReliable() { + return confidence >= 0.3 && sessionsCount >= 3; + } + + private double clampRating(double value) { + return Math.max(0.5, Math.min(2.0, value)); + } + + private double clampConfidence(double value) { + return Math.max(0.0, Math.min(1.0, value)); + } + + @NotNull + public UUID getPlayerUuid() { + return playerUuid; + } + + public double getRating() { + return rating; + } + + public double getConfidence() { + return confidence; + } + + public int getSessionsCount() { + return sessionsCount; + } + + public long getLastUpdated() { + return lastUpdated; + } + + public void setRating(double rating) { + this.rating = clampRating(rating); + } + + public void setConfidence(double confidence) { + this.confidence = clampConfidence(confidence); + } + + public void setSessionsCount(int sessionsCount) { + this.sessionsCount = sessionsCount; + } + + public void setLastUpdated(long lastUpdated) { + this.lastUpdated = lastUpdated; + } + + @Override + public String toString() { + return String.format("SkillRating{rating=%.2f, confidence=%.2f, sessions=%d}", + rating, confidence, sessionsCount); + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/storage/FileStatsStorage.java b/src/main/java/dev/loki/loparkour/adaptive/storage/FileStatsStorage.java new file mode 100644 index 0000000..6c433f3 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/storage/FileStatsStorage.java @@ -0,0 +1,185 @@ +package dev.loki.loparkour.adaptive.storage; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.adaptive.model.PlayerMetrics; +import dev.loki.loparkour.adaptive.model.SkillRating; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +/** + * JSON file-based implementation of StatsRepository. + * Stores data in playerdata/.json files. + * Saves only when player leaves to minimize I/O. + */ +public class FileStatsStorage implements StatsRepository { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private final File dataFolder; + + public FileStatsStorage() { + this.dataFolder = new File(LoParkour.getPlugin().getDataFolder(), "playerdata"); + if (!dataFolder.exists()) { + dataFolder.mkdirs(); + } + } + + @Override + @Nullable + public SkillRating loadSkillRating(@NotNull UUID playerUuid) { + JsonObject data = loadPlayerData(playerUuid); + if (data == null || !data.has("skillRating")) { + return null; + } + + JsonObject ratingObj = data.getAsJsonObject("skillRating"); + double rating = ratingObj.get("rating").getAsDouble(); + double confidence = ratingObj.get("confidence").getAsDouble(); + int sessions = ratingObj.get("sessionsCount").getAsInt(); + + return new SkillRating(playerUuid, rating, confidence, sessions); + } + + @Override + public void saveSkillRating(@NotNull SkillRating rating) { + JsonObject data = loadPlayerData(rating.getPlayerUuid()); + if (data == null) { + data = new JsonObject(); + } + + JsonObject ratingObj = new JsonObject(); + ratingObj.addProperty("rating", rating.getRating()); + ratingObj.addProperty("confidence", rating.getConfidence()); + ratingObj.addProperty("sessionsCount", rating.getSessionsCount()); + ratingObj.addProperty("lastUpdated", rating.getLastUpdated()); + + data.add("skillRating", ratingObj); + savePlayerData(rating.getPlayerUuid(), data); + } + + @Override + @Nullable + public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) { + JsonObject data = loadPlayerData(playerUuid); + if (data == null || !data.has("metrics")) { + return null; + } + + JsonObject metricsObj = data.getAsJsonObject("metrics"); + PlayerMetrics metrics = new PlayerMetrics(playerUuid); + + metrics.setAvgTimePerBlock(metricsObj.get("avgTimePerBlock").getAsDouble()); + metrics.setNearMissCount(metricsObj.get("nearMissCount").getAsInt()); + metrics.setTotalJumps(metricsObj.get("totalJumps").getAsInt()); + metrics.setLastUpdated(metricsObj.get("lastUpdated").getAsLong()); + + JsonObject jumpStats = metricsObj.getAsJsonObject("jumpTypeStats"); + for (Map.Entry entry : jumpStats.entrySet()) { + metrics.putJumpTypeStat(entry.getKey(), entry.getValue().getAsInt()); + } + + return metrics; + } + + @Override + public void saveMetrics(@NotNull PlayerMetrics metrics) { + JsonObject data = loadPlayerData(metrics.getPlayerUuid()); + if (data == null) { + data = new JsonObject(); + } + + JsonObject metricsObj = new JsonObject(); + metricsObj.addProperty("avgTimePerBlock", metrics.getAvgTimePerBlock()); + metricsObj.addProperty("nearMissCount", metrics.getNearMissCount()); + metricsObj.addProperty("totalJumps", metrics.getTotalJumps()); + metricsObj.addProperty("lastUpdated", metrics.getLastUpdated()); + + JsonObject jumpStats = new JsonObject(); + for (Map.Entry entry : metrics.getJumpTypeStats().entrySet()) { + jumpStats.addProperty(entry.getKey(), entry.getValue()); + } + metricsObj.add("jumpTypeStats", jumpStats); + + data.add("metrics", metricsObj); + savePlayerData(metrics.getPlayerUuid(), data); + } + + @Override + public void incrementJumps(@NotNull UUID playerUuid, int amount) { + JsonObject data = loadPlayerData(playerUuid); + if (data == null) { + data = new JsonObject(); + } + + int current = data.has("totalJumps") ? data.get("totalJumps").getAsInt() : 0; + data.addProperty("totalJumps", current + amount); + savePlayerData(playerUuid, data); + } + + @Override + public void incrementFalls(@NotNull UUID playerUuid, int amount) { + JsonObject data = loadPlayerData(playerUuid); + if (data == null) { + data = new JsonObject(); + } + + int current = data.has("totalFalls") ? data.get("totalFalls").getAsInt() : 0; + data.addProperty("totalFalls", current + amount); + savePlayerData(playerUuid, data); + } + + @Override + public void updateLongestStreak(@NotNull UUID playerUuid, int streak) { + JsonObject data = loadPlayerData(playerUuid); + if (data == null) { + data = new JsonObject(); + } + + int current = data.has("longestStreak") ? data.get("longestStreak").getAsInt() : 0; + if (streak > current) { + data.addProperty("longestStreak", streak); + savePlayerData(playerUuid, data); + } + } + + @Override + public void close() { + // No resources to close for file storage + } + + @Nullable + private JsonObject loadPlayerData(@NotNull UUID playerUuid) { + File file = new File(dataFolder, playerUuid.toString() + ".json"); + if (!file.exists()) { + return null; + } + + try (FileReader reader = new FileReader(file)) { + return GSON.fromJson(reader, JsonObject.class); + } catch (IOException e) { + LoParkour.getPlugin().getLogger().warning( + "Failed to load player data for " + playerUuid + ": " + e.getMessage()); + return null; + } + } + + private void savePlayerData(@NotNull UUID playerUuid, @NotNull JsonObject data) { + File file = new File(dataFolder, playerUuid.toString() + ".json"); + + try (FileWriter writer = new FileWriter(file)) { + GSON.toJson(data, writer); + } catch (IOException e) { + LoParkour.getPlugin().getLogger().warning( + "Failed to save player data for " + playerUuid + ": " + e.getMessage()); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/storage/NoOpStatsRepository.java b/src/main/java/dev/loki/loparkour/adaptive/storage/NoOpStatsRepository.java new file mode 100644 index 0000000..12e6183 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/storage/NoOpStatsRepository.java @@ -0,0 +1,60 @@ +package dev.loki.loparkour.adaptive.storage; + +import dev.loki.loparkour.adaptive.model.PlayerMetrics; +import dev.loki.loparkour.adaptive.model.SkillRating; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * No-op implementation of StatsRepository. + * Used as a placeholder until the full storage implementation is ready. + * + * This allows MetricsCollector to function without throwing errors, + * but metrics will not be persisted between server restarts. + */ +public class NoOpStatsRepository implements StatsRepository { + + @Override + @Nullable + public SkillRating loadSkillRating(@NotNull UUID playerUuid) { + return null; + } + + @Override + public void saveSkillRating(@NotNull SkillRating rating) { + // No-op + } + + @Override + @Nullable + public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) { + return null; + } + + @Override + public void saveMetrics(@NotNull PlayerMetrics metrics) { + // No-op + } + + @Override + public void incrementJumps(@NotNull UUID playerUuid, int amount) { + // No-op + } + + @Override + public void incrementFalls(@NotNull UUID playerUuid, int amount) { + // No-op + } + + @Override + public void updateLongestStreak(@NotNull UUID playerUuid, int streak) { + // No-op + } + + @Override + public void close() { + // No-op + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/storage/SQLStatsStorage.java b/src/main/java/dev/loki/loparkour/adaptive/storage/SQLStatsStorage.java new file mode 100644 index 0000000..d714da0 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/storage/SQLStatsStorage.java @@ -0,0 +1,173 @@ +package dev.loki.loparkour.adaptive.storage; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.adaptive.model.PlayerMetrics; +import dev.loki.loparkour.adaptive.model.SkillRating; +import dev.loki.loparkour.storage.sql.SQLConnectionManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * SQL-based implementation of StatsRepository using loparkour_player_stats table. + * Uses batch updates for optimization. + */ +public class SQLStatsStorage implements StatsRepository { + + private final SQLConnectionManager connectionManager; + private final ConcurrentHashMap pendingUpdates; + private static final int BATCH_SIZE = 50; + + public SQLStatsStorage(@NotNull SQLConnectionManager connectionManager) { + this.connectionManager = connectionManager; + this.pendingUpdates = new ConcurrentHashMap<>(); + } + + @Override + @Nullable + public SkillRating loadSkillRating(@NotNull UUID playerUuid) { + String query = "SELECT skill_rating, sessions_count FROM loparkour_player_stats WHERE player_uuid = ?"; + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement stmt = conn.prepareStatement(query)) { + + stmt.setString(1, playerUuid.toString()); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + double rating = rs.getDouble("skill_rating"); + int sessions = rs.getInt("sessions_count"); + double confidence = Math.min(1.0, sessions * 0.05); + return new SkillRating(playerUuid, rating, confidence, sessions); + } + } catch (SQLException e) { + LoParkour.getPlugin().getLogger().warning("Failed to load skill rating: " + e.getMessage()); + } + + return null; + } + + @Override + public void saveSkillRating(@NotNull SkillRating rating) { + String query = "INSERT INTO loparkour_player_stats (player_uuid, skill_rating, sessions_count) " + + "VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE skill_rating = ?, sessions_count = ?"; + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement stmt = conn.prepareStatement(query)) { + + stmt.setString(1, rating.getPlayerUuid().toString()); + stmt.setDouble(2, rating.getRating()); + stmt.setInt(3, rating.getSessionsCount()); + stmt.setDouble(4, rating.getRating()); + stmt.setInt(5, rating.getSessionsCount()); + + stmt.executeUpdate(); + } catch (SQLException e) { + LoParkour.getPlugin().getLogger().warning("Failed to save skill rating: " + e.getMessage()); + } + } + + @Override + @Nullable + public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) { + // Metrics are stored in JSON files, not SQL + return null; + } + + @Override + public void saveMetrics(@NotNull PlayerMetrics metrics) { + // Metrics are stored in JSON files, not SQL + } + + @Override + public void incrementJumps(@NotNull UUID playerUuid, int amount) { + PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate()); + update.jumps += amount; + + if (pendingUpdates.size() >= BATCH_SIZE) { + flushBatch(); + } + } + + @Override + public void incrementFalls(@NotNull UUID playerUuid, int amount) { + PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate()); + update.falls += amount; + + if (pendingUpdates.size() >= BATCH_SIZE) { + flushBatch(); + } + } + + @Override + public void updateLongestStreak(@NotNull UUID playerUuid, int streak) { + PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate()); + update.longestStreak = Math.max(update.longestStreak, streak); + + if (pendingUpdates.size() >= BATCH_SIZE) { + flushBatch(); + } + } + + /** + * Flushes all pending updates to database using batch operations. + */ + public void flushBatch() { + if (pendingUpdates.isEmpty()) { + return; + } + + String query = "INSERT INTO loparkour_player_stats (player_uuid, total_jumps, total_falls, longest_streak) " + + "VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE " + + "total_jumps = total_jumps + VALUES(total_jumps), " + + "total_falls = total_falls + VALUES(total_falls), " + + "longest_streak = GREATEST(longest_streak, VALUES(longest_streak))"; + + try (Connection conn = connectionManager.getConnection(); + PreparedStatement stmt = conn.prepareStatement(query)) { + + List processed = new ArrayList<>(); + + for (var entry : pendingUpdates.entrySet()) { + UUID uuid = entry.getKey(); + PendingUpdate update = entry.getValue(); + + stmt.setString(1, uuid.toString()); + stmt.setInt(2, update.jumps); + stmt.setInt(3, update.falls); + stmt.setInt(4, update.longestStreak); + stmt.addBatch(); + + processed.add(uuid); + } + + stmt.executeBatch(); + + for (UUID uuid : processed) { + pendingUpdates.remove(uuid); + } + + } catch (SQLException e) { + LoParkour.getPlugin().getLogger().warning("Failed to flush batch updates: " + e.getMessage()); + } + } + + @Override + public void close() { + flushBatch(); + } + + private static class PendingUpdate { + int jumps = 0; + int falls = 0; + int longestStreak = 0; + } +} diff --git a/src/main/java/dev/loki/loparkour/adaptive/storage/StatsRepository.java b/src/main/java/dev/loki/loparkour/adaptive/storage/StatsRepository.java new file mode 100644 index 0000000..90f1d91 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/adaptive/storage/StatsRepository.java @@ -0,0 +1,76 @@ +package dev.loki.loparkour.adaptive.storage; + +import dev.loki.loparkour.adaptive.model.PlayerMetrics; +import dev.loki.loparkour.adaptive.model.SkillRating; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Repository interface for storing and retrieving player statistics. + * Implementations can use SQL or file-based storage. + */ +public interface StatsRepository { + + /** + * Loads skill rating for a player. + * + * @param playerUuid The player's UUID + * @return SkillRating or null if not found + */ + @Nullable + SkillRating loadSkillRating(@NotNull UUID playerUuid); + + /** + * Saves skill rating for a player. + * + * @param rating The skill rating to save + */ + void saveSkillRating(@NotNull SkillRating rating); + + /** + * Loads detailed metrics for a player. + * + * @param playerUuid The player's UUID + * @return PlayerMetrics or null if not found + */ + @Nullable + PlayerMetrics loadMetrics(@NotNull UUID playerUuid); + + /** + * Saves detailed metrics for a player. + * + * @param metrics The metrics to save + */ + void saveMetrics(@NotNull PlayerMetrics metrics); + + /** + * Increments total jumps counter for a player. + * + * @param playerUuid The player's UUID + * @param amount Amount to increment + */ + void incrementJumps(@NotNull UUID playerUuid, int amount); + + /** + * Increments total falls counter for a player. + * + * @param playerUuid The player's UUID + * @param amount Amount to increment + */ + void incrementFalls(@NotNull UUID playerUuid, int amount); + + /** + * Updates longest streak for a player if new streak is longer. + * + * @param playerUuid The player's UUID + * @param streak The new streak value + */ + void updateLongestStreak(@NotNull UUID playerUuid, int streak); + + /** + * Closes the repository and releases resources. + */ + void close(); +} diff --git a/src/main/java/dev/loki/loparkour/api/ParkourAPI.java b/src/main/java/dev/loki/loparkour/api/core/ParkourAPI.java similarity index 83% rename from src/main/java/dev/loki/loparkour/api/ParkourAPI.java rename to src/main/java/dev/loki/loparkour/api/core/ParkourAPI.java index ba303cc..9827287 100644 --- a/src/main/java/dev/loki/loparkour/api/ParkourAPI.java +++ b/src/main/java/dev/loki/loparkour/api/core/ParkourAPI.java @@ -1,8 +1,8 @@ -package dev.loki.loparkour.api; +package dev.loki.loparkour.api.core; import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; @@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable; *
  • For player spectating, please view {@link dev.efnilite.LoParkour.player.ParkourSpectator}.
  • *
  • For events, please view {@link ParkourBlockGenerateEvent} and others in the events package.
  • *
  • For Sessions, please view {@link dev.efnilite.LoParkour.session.Session}.
  • - *
  • For Schematics, please view {@link dev.efnilite.LoParkour.schematic.LPSchematic}
  • + *
  • For jump structures, see {@link dev.loki.loparkour.schematic.core.ParkourSchematic}
  • * */ public class ParkourAPI { diff --git a/src/main/java/dev/loki/loparkour/api/Registry.java b/src/main/java/dev/loki/loparkour/api/core/Registry.java similarity index 91% rename from src/main/java/dev/loki/loparkour/api/Registry.java rename to src/main/java/dev/loki/loparkour/api/core/Registry.java index d622bed..f76cafc 100644 --- a/src/main/java/dev/loki/loparkour/api/Registry.java +++ b/src/main/java/dev/loki/loparkour/api/core/Registry.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.api; +package dev.loki.loparkour.api.core; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.style.Style; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.style.core.Style; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourBlockGenerateEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourBlockGenerateEvent.java index 7ee2c33..482a370 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourBlockGenerateEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourBlockGenerateEvent.java @@ -1,7 +1,7 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourFallEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourFallEvent.java index 4360919..995dccb 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourFallEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourFallEvent.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourJoinEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourJoinEvent.java index fa3598c..322682c 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourJoinEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourJoinEvent.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.player.core.ParkourUser; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourLeaveEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourLeaveEvent.java index ee0e83a..708f473 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourLeaveEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourLeaveEvent.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.player.core.ParkourUser; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourSchematicGenerateEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourSchematicGenerateEvent.java index 67af157..8fcad78 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourSchematicGenerateEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourSchematicGenerateEvent.java @@ -1,27 +1,24 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.schematic.core.ParkourSchematic; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; /** - * Gets called when a new jump is generated. Read-only. - * - * @author loki - * @since 5.0.0 + * Gets called when a schematic jump is pasted. Read-only. */ public class ParkourSchematicGenerateEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); - public final LPSchematic schematic; + public final ParkourSchematic schematic; public final ParkourGenerator generator; public final ParkourPlayer player; - public ParkourSchematicGenerateEvent(LPSchematic schematic, ParkourGenerator generator, ParkourPlayer player) { + public ParkourSchematicGenerateEvent(ParkourSchematic schematic, ParkourGenerator generator, ParkourPlayer player) { this.schematic = schematic; this.generator = generator; this.player = player; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourScoreEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourScoreEvent.java index d5c832e..aa43517 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourScoreEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourScoreEvent.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/dev/loki/loparkour/api/event/ParkourSpectateEvent.java b/src/main/java/dev/loki/loparkour/api/event/ParkourSpectateEvent.java index 9e5a6c6..4b06c47 100644 --- a/src/main/java/dev/loki/loparkour/api/event/ParkourSpectateEvent.java +++ b/src/main/java/dev/loki/loparkour/api/event/ParkourSpectateEvent.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.api.event; -import dev.loki.loparkour.player.ParkourSpectator; +import dev.loki.loparkour.player.spectator.ParkourSpectator; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; diff --git a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java new file mode 100644 index 0000000..fb51d31 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java @@ -0,0 +1,133 @@ +package dev.loki.loparkour.bootstrap; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.command.LoParkourCommand; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.hook.holo.HoloHook; +import dev.loki.loparkour.hook.papi.PAPIHook; +import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener; +import dev.loki.loparkour.listener.schematic.SchematicWandListener; +import dev.loki.loparkour.listener.player.PlayerConnectionListener; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.mode.impl.CoopMode; +import dev.loki.loparkour.mode.impl.DefaultMode; +import dev.loki.loparkour.mode.impl.ElytraMode; +import dev.loki.loparkour.mode.impl.GravityShiftMode; +import dev.loki.loparkour.mode.impl.HardcoreMode; +import dev.loki.loparkour.mode.impl.RaceMode; +import dev.loki.loparkour.mode.impl.SpectatorMode; +import dev.loki.loparkour.mode.impl.SpeedrunMode; +import dev.loki.loparkour.reward.core.Rewards; +import dev.loki.loparkour.schematic.core.SchematicManager; +import dev.loki.loparkour.world.core.World; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; +import org.bstats.charts.SingleLineChart; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Wires plugin subsystems during {@link LoParkour#enable()} and {@link LoParkour#disable()}. + */ +public final class PluginBootstrap { + + private PluginBootstrap() { + } + + public static void enable(@NotNull LoParkour plugin) { + Config.reload(true); + AdaptiveServices.init(plugin); + loadSchematics(plugin); + registerModes(); + registerHooks(plugin); + setupWorld(plugin); + registerEventsAndCommands(plugin); + setupMetrics(plugin); + } + + public static void disable(@NotNull LoParkour plugin) { + plugin.runShutdownSequence(); + } + + private static void loadSchematics(@NotNull LoParkour plugin) { + extractSchematicDocs(plugin); + SchematicManager manager = new SchematicManager(); + manager.loadAll(); + plugin.setSchematicManager(manager); + } + + private static void extractSchematicDocs(@NotNull LoParkour plugin) { + java.io.File doc = LoParkour.getInFolder("docs/SCHEMATICS.md"); + if (!doc.exists()) { + doc.getParentFile().mkdirs(); + plugin.saveResource("docs/SCHEMATICS.md", false); + } + } + + private static void registerModes() { + Registry.register(new DefaultMode()); + Registry.register(new SpectatorMode()); + Registry.register(new SpeedrunMode()); + Registry.register(new ElytraMode()); + Registry.register(new RaceMode()); + Registry.register(new CoopMode()); + Registry.register(new GravityShiftMode()); + Registry.register(new HardcoreMode()); + Modes.init(); + } + + private static void registerHooks(@NotNull LoParkour plugin) { + if (plugin.getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) { + plugin.getLogger().info("Registered Holographic Displays hook"); + HoloHook.init(); + } + if (plugin.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) { + plugin.getLogger().info("Registered PlaceholderAPI hook"); + PAPIHook hook = new PAPIHook(); + hook.register(); + plugin.setPlaceholderHook(hook); + } + if (Config.CONFIG.getBoolean("bungeecord.enabled")) { + plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord"); + plugin.getLogger().info("Registered BungeeCord hook"); + } + } + + private static void setupWorld(@NotNull LoParkour plugin) { + if (!Config.CONFIG.getBoolean("joining")) { + return; + } + try { + plugin.loLogger().info("Creating parkour world..."); + World.create(); + plugin.loLogger().info("Parkour world created successfully!"); + } catch (Exception ex) { + plugin.loLogger().error("Failed to create parkour world", ex); + } + } + + private static void registerEventsAndCommands(@NotNull LoParkour plugin) { + dev.lolib.gui.GUIManager.init(plugin); + + var pm = plugin.getServer().getPluginManager(); + pm.registerEvents(new PlayerConnectionListener(), plugin); + pm.registerEvents(new ParkourRestrictionListener(), plugin); + pm.registerEvents(new SchematicWandListener(), plugin); + LoParkourCommand cmd = new LoParkourCommand(); + plugin.getCommand("LoParkour").setExecutor(cmd); + plugin.getCommand("LoParkour").setTabCompleter(cmd); + } + + private static void setupMetrics(@NotNull LoParkour plugin) { + Metrics metrics = new Metrics(plugin, 29754); + metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL))); + metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED))); + metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount()))); + metrics.addCustomChart(new SingleLineChart("player_joins", () -> + dev.loki.loparkour.player.service.UserRegistry.getJoinCount())); + } +} diff --git a/src/main/java/dev/loki/loparkour/LoParkourCommand.java b/src/main/java/dev/loki/loparkour/command/LoParkourCommand.java similarity index 65% rename from src/main/java/dev/loki/loparkour/LoParkourCommand.java rename to src/main/java/dev/loki/loparkour/command/LoParkourCommand.java index 8ca653f..7a6042a 100644 --- a/src/main/java/dev/loki/loparkour/LoParkourCommand.java +++ b/src/main/java/dev/loki/loparkour/command/LoParkourCommand.java @@ -1,22 +1,23 @@ -package dev.loki.loparkour; +package dev.loki.loparkour.command; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.command.AdminCommandHandler; -import dev.loki.loparkour.command.PlayerCommandHandler; -import dev.loki.loparkour.command.SchematicCommandHandler; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.mode.MultiMode; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.command.admin.AdminCommandHandler; +import dev.loki.loparkour.command.player.PlayerCommandHandler; +import dev.loki.loparkour.command.schematic.SchematicCommandHandler; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.mode.base.MultiMode; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.schematic.core.SchematicManager; +import dev.loki.loparkour.session.core.Session; +import dev.loki.loparkour.util.text.ColorUtil; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -38,6 +39,11 @@ import java.util.logging.Level; */ public class LoParkourCommand implements CommandExecutor, TabCompleter { + private static final List SCHEMATIC_DIFFICULTIES = List.of("0.0", "0.25", "0.5", "0.75", "1.0"); + private static final List SCHEMATIC_SUBCOMMANDS = List.of( + "wand", "pos1", "pos2", "create", "convert", "paste", "list", "reload" + ); + private final PlayerCommandHandler player = new PlayerCommandHandler(); private final AdminCommandHandler admin = new AdminCommandHandler(player); @@ -52,13 +58,14 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { case 1 -> player.handle(args[0], sender, p); case 2 -> handle2(args[0], args[1], sender, p); case 3 -> handle3(args[0], args[1], args[2], sender, p); - default -> sender.sendMessage(ChatColor.GRAY + "Too many arguments. Try /parkour help"); + case 4 -> handle4(args[0], args[1], args[2], args[3], sender, p); + default -> Locales.send(sender, "commands.too_many_args"); } return true; } catch (Throwable t) { LoParkour.getPlugin().getLogger().log(Level.SEVERE, "LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t); - sender.sendMessage(ChatColor.RED + "LoParkour: command error — see server console."); + Locales.send(sender, "commands.error"); return true; } } @@ -76,6 +83,11 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { if (p == null) return; + if (a1.equalsIgnoreCase("create") && p.hasPermission(ParkourOption.ADMIN.permission)) { + SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player); + return; + } + switch (a1.toLowerCase()) { case "join" -> handleJoin(a2, sender, p); case "leaderboard" -> handleLeaderboard(a2, sender, p); @@ -93,9 +105,22 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) { if (p == null) return; - if (!a1.equalsIgnoreCase("schematic")) return; if (!p.hasPermission(ParkourOption.ADMIN.permission)) return; - SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p, player); + + if (a1.equalsIgnoreCase("schematic")) { + if (a2.equalsIgnoreCase("create")) { + SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player); + return; + } + SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p); + } + } + + private void handle4(String a1, String a2, String a3, String a4, CommandSender sender, @Nullable Player p) { + if (p == null) return; + if (!p.hasPermission(ParkourOption.ADMIN.permission)) return; + if (!a1.equalsIgnoreCase("schematic") || !a2.equalsIgnoreCase("create")) return; + SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player); } // ── join logic ───────────────────────────────────────────────────────────── @@ -107,10 +132,10 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { if (mode != null) { mode.create(p); return; } Player other = Bukkit.getPlayer(arg); - if (other == null) { send(sender, LoParkour.PREFIX + "Unknown player."); return; } + if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player")); return; } ParkourPlayer pp = ParkourPlayer.getPlayer(other); - if (pp == null) { send(sender, LoParkour.PREFIX + "That player isn't playing."); return; } + if (pp == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing")); return; } ParkourUser user = ParkourUser.getUser(p); Session session = pp.session; @@ -156,7 +181,7 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { if (ParkourOption.PLAY.mayPerform(sender)) completions.add("play"); if (ParkourOption.LEADERBOARDS.mayPerform(sender)) completions.add("leaderboard"); if (sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.addAll(List.of("schematic", "reload", "forcejoin", "forceleave", "reset", "recoverinventory")); + completions.addAll(List.of("schematic", "create", "reload", "forcejoin", "forceleave", "reset", "recoverinventory")); } return filter(args[0], completions); } @@ -172,7 +197,9 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { } else if (a1.equals("leaderboard") && ParkourOption.LEADERBOARDS.mayPerform(sender)) { Registry.getModes().forEach(m -> completions.add(m.getName())); } else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) { - completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste", "list", "reload")); + completions.addAll(SCHEMATIC_SUBCOMMANDS); + } else if (a1.equals("create") && sender.hasPermission(ParkourOption.ADMIN.permission)) { + completions.addAll(SCHEMATIC_DIFFICULTIES); } else if ((a1.equals("forcejoin") || a1.equals("forceleave")) && sender.hasPermission(ParkourOption.ADMIN.permission)) { completions.add("everyone"); if (a1.equals("forcejoin")) completions.add("nearest"); @@ -183,9 +210,45 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter { return filter(args[1], completions); } + if (args.length == 3 && sender.hasPermission(ParkourOption.ADMIN.permission)) { + String a1 = args[0].toLowerCase(); + String a2 = args[1].toLowerCase(); + if (a1.equals("schematic")) { + if (a2.equals("paste")) { + addLoadedSchematicIds(completions); + } else if (a2.equals("create")) { + completions.addAll(SCHEMATIC_DIFFICULTIES); + } + } + return filter(args[2], completions); + } + + if (args.length == 4 && sender.hasPermission(ParkourOption.ADMIN.permission)) { + String a1 = args[0].toLowerCase(); + String a2 = args[1].toLowerCase(); + if (a1.equals("schematic") && a2.equals("create")) { + completions.addAll(SCHEMATIC_DIFFICULTIES); + } + return filter(args[3], completions); + } + return Collections.emptyList(); } + private static void addLoadedSchematicIds(@NotNull List completions) { + SchematicManager manager = LoParkour.getSchematicManager(); + if (manager == null) { + return; + } + for (String id : manager.getAll().keySet()) { + completions.add(id); + String shortId = SchematicManager.configKey(id); + if (!shortId.equalsIgnoreCase(id)) { + completions.add(shortId); + } + } + } + // ── util ─────────────────────────────────────────────────────────────────── private List filter(String input, List options) { diff --git a/src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java b/src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java deleted file mode 100644 index 3a9b024..0000000 --- a/src/main/java/dev/loki/loparkour/command/SchematicCommandHandler.java +++ /dev/null @@ -1,172 +0,0 @@ -package dev.loki.loparkour.command; - -import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; -import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder; -import dev.loki.loparkour.util.ColorUtil; -import dev.loki.loparkour.util.Locations; -import dev.loki.loparkour.util.ParticleUtil; -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.Material; -import org.bukkit.util.BoundingBox; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Handles /LoParkour schematic sub-commands (wand, pos1, pos2, save, paste, list, reload). - */ -public class SchematicCommandHandler { - - public static final Map selections = new HashMap<>(); - - private static final ItemStack WAND = buildWand(); - - private static ItemStack buildWand() { - ItemStack item = new ItemStack(Material.GOLDEN_AXE); - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - meta.setDisplayName(ColorUtil.color("LPSchematic Wand")); - meta.setLore(List.of( - ColorUtil.color("Left click: first position"), - ColorUtil.color("Right click: second position") - )); - item.setItemMeta(meta); - } - return item; - } - - public static ItemStack getWand() { - return WAND.clone(); - } - - public static void sendHelp(Player player) { - send(player, ""); - send(player, "Schematics -----------"); - send(player, "/LoParkour schematic wand - Get the wand"); - send(player, "/LoParkour schematic pos1 - Set position 1"); - send(player, "/LoParkour schematic pos2 - Set position 2"); - send(player, "/LoParkour schematic save - Save selection"); - send(player, "/LoParkour schematic paste - Paste schematic"); - send(player, "/LoParkour schematic list - List loaded schematics"); - send(player, "/LoParkour schematic reload - Reload schematics"); - send(player, ""); - } - - /** Called for 2-arg schematic commands (wand/pos1/pos2/list/reload) */ - public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) { - Location playerLoc = player.getLocation(); - Location[] sel = selections.get(player); - - switch (sub.toLowerCase()) { - case "wand" -> { - player.getInventory().addItem(WAND.clone()); - send(player, "Schematics Wand given. Left/Right click to set positions."); - } - case "pos1" -> setPos(player, sel, playerLoc, 0); - case "pos2" -> setPos(player, sel, playerLoc, 1); - case "list" -> { - send(player, "Schematics Loaded schematics:"); - LoParkour.getSchematicManager().getAllSchematics().forEach((name, s) -> - send(player, "- " + name + " (" + s.getMetadata().getDifficulty() + ")")); - } - case "reload" -> { - if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) return; - LoParkour.getSchematicManager().reload(); - send(player, "Schematics Reloaded."); - } - } - } - - /** Called for 3-arg schematic commands (save , paste ) */ - public static void handleSubcommandWithName(String sub, String name, CommandSender sender, - Player player, PlayerCommandHandler cooldowns) { - switch (sub.toLowerCase()) { - case "save" -> handleSave(name, sender, player, cooldowns); - case "paste" -> handlePaste(name, sender, player); - } - } - - // ── internal ─────────────────────────────────────────────────────────────── - - private static void setPos(Player player, Location[] existing, Location loc, int index) { - String label = index == 0 ? "1" : "2"; - send(player, LoParkour.PREFIX + "Position " + label + " set to " + Locations.toString(loc, true)); - - Location[] updated = existing != null ? existing.clone() : new Location[]{null, null}; - updated[index] = loc; - selections.put(player, updated); - - if (updated[0] != null && updated[1] != null) { - ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2); - } - } - - private static void handleSave(String name, CommandSender sender, Player player, PlayerCommandHandler cooldowns) { - if (!cooldowns.cooldown(sender, "schematic-save", 2500)) return; - - // Validate schematic name to prevent path traversal - if (!isValidSchematicName(name)) { - send(player, "Schematics Invalid name. Use only letters, numbers, hyphens and underscores."); - return; - } - - Location[] sel = selections.get(player); - if (sel == null || sel[0] == null || sel[1] == null) { - send(player, "Schematics Selection incomplete. Set both positions first."); - return; - } - - try { - int w = Math.abs(sel[1].getBlockX() - sel[0].getBlockX()) + 1; - int h = Math.abs(sel[1].getBlockY() - sel[0].getBlockY()) + 1; - int l = Math.abs(sel[1].getBlockZ() - sel[0].getBlockZ()) + 1; - - LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), 0.5, w, h, l) - .fromWorld(player.getWorld(), sel[0], sel[1]) - .build(); - - LoParkour.getSchematicManager().saveSchematic(schematic); - send(player, "Schematics Saved as " + name + "!"); - } catch (Exception e) { - send(player, "Schematics Failed: " + e.getMessage()); - } - } - - /** - * Validates schematic name to prevent path traversal and filesystem issues. - * Only allows alphanumeric characters, hyphens, and underscores. - */ - private static boolean isValidSchematicName(String name) { - if (name == null || name.isEmpty() || name.length() > 64) { - return false; - } - return name.matches("^[a-zA-Z0-9_-]+$"); - } - - private static void handlePaste(String name, CommandSender sender, Player player) { - // Validate schematic name to prevent path traversal - if (!isValidSchematicName(name)) { - send(player, "Schematics Invalid name. Use only letters, numbers, hyphens and underscores."); - return; - } - - LPSchematic s = LoParkour.getSchematicManager().getSchematic(name); - if (s == null) { - send(sender, LoParkour.PREFIX + "Schematic '" + name + "' not found."); - return; - } - s.paste(player.getLocation(), player.getWorld()); - send(sender, LoParkour.PREFIX + "Pasted " + name + "."); - } - - private static void send(CommandSender sender, String msg) { - sender.sendMessage(ColorUtil.color(msg)); - } -} diff --git a/src/main/java/dev/loki/loparkour/command/AdminCommandHandler.java b/src/main/java/dev/loki/loparkour/command/admin/AdminCommandHandler.java similarity index 75% rename from src/main/java/dev/loki/loparkour/command/AdminCommandHandler.java rename to src/main/java/dev/loki/loparkour/command/admin/AdminCommandHandler.java index dc47871..cb9506a 100644 --- a/src/main/java/dev/loki/loparkour/command/AdminCommandHandler.java +++ b/src/main/java/dev/loki/loparkour/command/admin/AdminCommandHandler.java @@ -1,15 +1,17 @@ -package dev.loki.loparkour.command; +package dev.loki.loparkour.command.admin; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.command.player.PlayerCommandHandler; +import dev.loki.loparkour.command.schematic.SchematicCommandHandler; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.player.data.InventoryData; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.util.text.ColorUtil; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.OfflinePlayer; @@ -27,8 +29,6 @@ import java.util.UUID; */ public class AdminCommandHandler { - private static final String COOLDOWN_HINT = "Please wait before using this again."; - private final PlayerCommandHandler base; public AdminCommandHandler(PlayerCommandHandler base) { @@ -61,18 +61,18 @@ public class AdminCommandHandler { if (target.equalsIgnoreCase("everyone")) { Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p)); - send(sender, LoParkour.PREFIX + "Force joined everyone!"); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_everyone")); return; } if (target.equalsIgnoreCase("nearest")) { Player closest = findNearest(sender); if (closest == null) return; Modes.DEFAULT.create(closest); - send(sender, LoParkour.PREFIX + "Force joined " + closest.getName() + "!"); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_player").formatted(closest.getName())); return; } Player other = Bukkit.getPlayer(target); - if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; } + if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; } Modes.DEFAULT.create(other); } @@ -83,27 +83,27 @@ public class AdminCommandHandler { if (target.equalsIgnoreCase("everyone")) { ParkourPlayer.getPlayers().forEach(ParkourUser::leave); - send(sender, LoParkour.PREFIX + "Force kicked everyone!"); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_leave_everyone")); return; } Player other = Bukkit.getPlayer(target); - if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; } + if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; } ParkourUser user = ParkourUser.getUser(other); - if (user == null) { send(sender, LoParkour.PREFIX + "Player isn't playing!"); return; } + if (user == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing")); return; } ParkourUser.leave(user); } // ── reset ────────────────────────────────────────────────────────────────── private void handleReset(String target, CommandSender sender) { - if (!base.cooldown(sender, "reset", 2500, COOLDOWN_HINT)) return; + if (!base.cooldown(sender, "reset", 2500, Locales.getString(sender, "admin.cooldown"))) return; if (target.equalsIgnoreCase("everyone")) { Registry.getModes().stream() .map(Mode::getLeaderboard) .filter(lb -> lb != null) .forEach(lb -> { lb.resetAll(); lb.write(true); }); - send(sender, LoParkour.PREFIX + "Reset all high scores."); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_all")); return; } @@ -115,7 +115,7 @@ public class AdminCommandHandler { .filter(lb -> lb != null) .forEach(lb -> { lb.remove(uuid); lb.write(true); }); - send(sender, LoParkour.PREFIX + "Reset high score of " + name + "."); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_player").formatted(name)); } // ── recoverinventory ─────────────────────────────────────────────────────── @@ -123,13 +123,13 @@ public class AdminCommandHandler { private void handleRecoverInventory(String target, CommandSender sender) { if (!base.cooldown(sender, "recoverinventory", 2500, Locales.getString(sender, "admin.cooldown"))) return; Player other = Bukkit.getPlayer(target); - if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; } + if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; } new InventoryData(other).load(result -> { if (result != null) { - send(sender, LoParkour.PREFIX + "Recovered inventory of " + other.getName() + "."); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory").formatted(other.getName())); } else { - send(sender, LoParkour.PREFIX + "No saved inventory found for " + other.getName() + "."); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory_missing").formatted(other.getName())); } }); } diff --git a/src/main/java/dev/loki/loparkour/command/PlayerCommandHandler.java b/src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java similarity index 68% rename from src/main/java/dev/loki/loparkour/command/PlayerCommandHandler.java rename to src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java index f11ba92..aa6ff2b 100644 --- a/src/main/java/dev/loki/loparkour/command/PlayerCommandHandler.java +++ b/src/main/java/dev/loki/loparkour/command/player/PlayerCommandHandler.java @@ -1,13 +1,14 @@ -package dev.loki.loparkour.command; +package dev.loki.loparkour.command.player; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.command.schematic.SchematicCommandHandler; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.util.text.ColorUtil; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -80,34 +81,38 @@ public class PlayerCommandHandler { private void handleReload(@NotNull CommandSender sender, @Nullable Player player) { if (!cooldown(sender, "reload", 2500)) return; if (!sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, Locales.getString(player, "other.no_do")); + send(sender, Locales.getString(sender, "other.no_do")); return; } Config.reload(false); - send(sender, LoParkour.PREFIX + "Reloading config files... (locales are loading asynchronously)"); + dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.reload(); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.reload")); } public void sendHelp(@NotNull CommandSender sender) { send(sender, ""); - send(sender, "--------------- " + LoParkour.NAME + " ---------------"); + send(sender, Locales.getString(sender, "commands.help.header")); send(sender, ""); - send(sender, "/parkour - Main command"); + send(sender, Locales.getString(sender, "commands.help.line_parkour")); if (sender.hasPermission(ParkourOption.JOIN.permission)) { - send(sender, "/parkour join [mode/player] - Join a mode"); - send(sender, "/parkour leave - Leave parkour"); + send(sender, Locales.getString(sender, "commands.help.line_join")); + send(sender, Locales.getString(sender, "commands.help.line_leave")); + } + if (sender.hasPermission(ParkourOption.MAIN.permission)) { + send(sender, Locales.getString(sender, "commands.help.line_menu")); + } + if (sender.hasPermission(ParkourOption.PLAY.permission)) { + send(sender, Locales.getString(sender, "commands.help.line_play")); + } + if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) { + send(sender, Locales.getString(sender, "commands.help.line_leaderboard")); } - if (sender.hasPermission(ParkourOption.MAIN.permission)) - send(sender, "/parkour menu - Open the menu"); - if (sender.hasPermission(ParkourOption.PLAY.permission)) - send(sender, "/parkour play - Mode selection menu"); - if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) - send(sender, "/parkour leaderboard [type] - Leaderboard"); if (sender.hasPermission(ParkourOption.ADMIN.permission)) { - send(sender, "/LoParkour schematic - Schematic tools"); - send(sender, "/LoParkour reload - Reload configs"); - send(sender, "/LoParkour reset - Reset scores"); - send(sender, "/LoParkour forcejoin/forceleave - Force join/leave"); - send(sender, "/LoParkour recoverinventory - Recover inventory"); + send(sender, Locales.getString(sender, "commands.help.line_schematic")); + send(sender, Locales.getString(sender, "commands.help.line_reload")); + send(sender, Locales.getString(sender, "commands.help.line_reset")); + send(sender, Locales.getString(sender, "commands.help.line_force")); + send(sender, Locales.getString(sender, "commands.help.line_recover")); } send(sender, ""); } @@ -117,7 +122,7 @@ public class PlayerCommandHandler { } /** - * @param whenBlocked message sent (MiniMessage color) if still on cooldown; may be null for silent block + * @param whenBlocked message sent ({@code &#RRGGBB} / {@code &} colors) if still on cooldown; may be null for silent block */ public boolean cooldown(CommandSender sender, String key, long millis, @Nullable String whenBlocked) { String fullKey = sender.getName() + ":" + key; diff --git a/src/main/java/dev/loki/loparkour/command/schematic/SchematicCommandHandler.java b/src/main/java/dev/loki/loparkour/command/schematic/SchematicCommandHandler.java new file mode 100644 index 0000000..9dad52b --- /dev/null +++ b/src/main/java/dev/loki/loparkour/command/schematic/SchematicCommandHandler.java @@ -0,0 +1,271 @@ +package dev.loki.loparkour.command.schematic; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.command.player.PlayerCommandHandler; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.schematic.convert.LpschemConverter; +import dev.loki.loparkour.schematic.core.ParkourSchematic; +import dev.loki.loparkour.schematic.create.SchematicCreator; +import dev.loki.loparkour.util.particle.ParticleUtil; +import dev.loki.loparkour.util.text.ColorUtil; +import dev.loki.loparkour.util.world.Locations; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Particle; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.BoundingBox; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * Admin schematic tools: wand, create, convert, list, paste, reload. + */ +public class SchematicCommandHandler { + + public static final Map selections = new HashMap<>(); + public static final NamespacedKey WAND_KEY = new NamespacedKey(LoParkour.getPlugin(), "schematic_wand"); + + private static ItemStack cachedWand; + + @NotNull + public static ItemStack getWand() { + return buildWand().clone(); + } + + public static boolean isWand(@Nullable ItemStack item) { + if (item == null || !item.hasItemMeta()) { + return false; + } + ItemMeta meta = item.getItemMeta(); + return meta != null + && meta.getPersistentDataContainer().has(WAND_KEY, PersistentDataType.BYTE); + } + + @NotNull + public static Location[] getSelection(@NotNull Player player) { + Location[] sel = selections.get(player); + if (sel == null) { + sel = new Location[]{null, null}; + selections.put(player, sel); + } + return sel; + } + + public static void setPos(@NotNull Player player, @NotNull Location loc, int index) { + Location[] updated = getSelection(player).clone(); + updated[index] = loc.clone(); + selections.put(player, updated); + } + + public static void clearWandCache() { + cachedWand = null; + } + + public static void sendHelp(@NotNull Player player) { + send(player, ""); + send(player, Locales.getString(player, "schematic.help.header")); + send(player, ""); + send(player, Locales.getString(player, "schematic.help.line_wand")); + send(player, Locales.getString(player, "schematic.help.line_pos1")); + send(player, Locales.getString(player, "schematic.help.line_pos2")); + send(player, Locales.getString(player, "schematic.help.line_create")); + send(player, Locales.getString(player, "schematic.help.line_create_named")); + send(player, Locales.getString(player, "schematic.help.line_convert")); + send(player, Locales.getString(player, "schematic.help.line_paste")); + send(player, Locales.getString(player, "schematic.help.line_list")); + send(player, Locales.getString(player, "schematic.help.line_reload")); + send(player, Locales.getString(player, "schematic.help.line_folder")); + send(player, Locales.getString(player, "schematic.help.line_docs")); + send(player, ""); + } + + public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) { + Location playerLoc = player.getLocation(); + Location[] sel = getSelection(player); + + switch (sub.toLowerCase()) { + case "wand" -> { + player.getInventory().addItem(getWand()); + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.wand_given")); + } + case "pos1" -> setPosCommand(player, sel, playerLoc, 0); + case "pos2" -> setPosCommand(player, sel, playerLoc, 1); + case "list" -> { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.list_header")); + LoParkour.getSchematicManager().getAll().forEach((name, s) -> + send(player, Locales.getString(player, "schematic.list_entry") + .formatted(name, s.getDifficulty(), s.getFormat().name().toLowerCase()))); + } + case "reload" -> { + if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) { + return; + } + Config.reload(false); + LoParkour.getSchematicManager().reload(); + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.reloaded")); + } + case "convert" -> handleConvert(sender, player, cooldowns); + default -> sendHelp(player); + } + } + + public static void handleSubcommandWithName(String sub, String name, CommandSender sender, Player player) { + switch (sub.toLowerCase()) { + case "paste" -> handlePaste(name, sender, player); + case "create" -> handleCreate(name, null, sender, player); + default -> { + } + } + } + + /** {@code schematic create } */ + public static void handleCreateWithDifficulty( + String name, + String difficultyRaw, + CommandSender sender, + Player player, + PlayerCommandHandler cooldowns + ) { + if (!cooldowns.cooldown(sender, "schematic-create", 2500)) { + return; + } + handleCreate(name, difficultyRaw, sender, player); + } + + /** {@code create } or {@code schematic create } */ + public static void handleCreateDifficultyOnly( + String difficultyRaw, + CommandSender sender, + Player player, + PlayerCommandHandler cooldowns + ) { + if (!cooldowns.cooldown(sender, "schematic-create", 2500)) { + return; + } + handleCreate(null, difficultyRaw, sender, player); + } + + private static void handleCreate( + @Nullable String optionalName, + @NotNull String difficultyRaw, + CommandSender sender, + Player player + ) { + Location[] sel = selections.get(player); + if (sel == null || sel[0] == null || sel[1] == null) { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete")); + return; + } + + double difficulty; + try { + difficulty = SchematicCreator.parseDifficulty(difficultyRaw); + } catch (NumberFormatException e) { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty")); + return; + } catch (IllegalArgumentException e) { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty")); + return; + } + + try { + SchematicCreator.CreateResult result = SchematicCreator.create(sel[0], sel[1], optionalName, difficulty); + LoParkour.getSchematicManager().reload(); + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.created") + .formatted(result.stem(), result.difficulty(), result.file().getName())); + } catch (IllegalArgumentException e) { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name")); + } catch (Exception e) { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.create_failed").formatted(e.getMessage())); + } + } + + private static void handleConvert(CommandSender sender, Player player, PlayerCommandHandler cooldowns) { + if (!cooldowns.cooldown(sender, "schematic-convert", 5000)) { + return; + } + LpschemConverter.ConvertResult result = LpschemConverter.convertAll(); + if (result.converted() > 0) { + LoParkour.getSchematicManager().reload(); + } + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.convert_done") + .formatted(result.converted(), result.failed())); + for (String line : result.messages()) { + if (result.messages().size() <= 12) { + send(player, "&#A0A0A0 " + line); + } + } + if (result.messages().size() > 12) { + send(player, Locales.getString(player, "schematic.convert_truncated").formatted(result.messages().size() - 12)); + } + } + + private static void setPosCommand(Player player, Location[] existing, Location loc, int index) { + setPos(player, loc, index); + String label = index == 0 ? "1" : "2"; + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.pos_set") + .formatted(label, Locations.toString(loc, true))); + + Location[] updated = getSelection(player); + if (updated[0] != null && updated[1] != null) { + ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2); + } + } + + private static void handlePaste(String name, CommandSender sender, Player player) { + if (!isValidSchematicName(name)) { + send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name")); + return; + } + + ParkourSchematic schematic = LoParkour.getSchematicManager().get(name); + if (schematic == null) { + send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found").formatted(name)); + return; + } + schematic.paste(player.getLocation(), player.getWorld()); + send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted").formatted(name)); + } + + static boolean isValidSchematicName(String name) { + if (name == null || name.isEmpty() || name.length() > 64) { + return false; + } + return name.matches("^[a-zA-Z0-9_-]+$"); + } + + @NotNull + private static ItemStack buildWand() { + if (cachedWand != null) { + return cachedWand; + } + + String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG); + if (locale == null || locale.isBlank()) { + locale = "en"; + } + + ItemStack item = Locales.getItem(locale, "schematic.wand").build(); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.getPersistentDataContainer().set(WAND_KEY, PersistentDataType.BYTE, (byte) 1); + item.setItemMeta(meta); + } + cachedWand = item; + return item; + } + + private static void send(CommandSender sender, String msg) { + sender.sendMessage(ColorUtil.color(msg)); + } +} diff --git a/src/main/java/dev/loki/loparkour/config/Config.java b/src/main/java/dev/loki/loparkour/config/core/Config.java similarity index 98% rename from src/main/java/dev/loki/loparkour/config/Config.java rename to src/main/java/dev/loki/loparkour/config/core/Config.java index 729c18e..d5839ac 100644 --- a/src/main/java/dev/loki/loparkour/config/Config.java +++ b/src/main/java/dev/loki/loparkour/config/core/Config.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.core; import org.bukkit.configuration.file.FileConfiguration; import org.jetbrains.annotations.NotNull; @@ -116,4 +116,4 @@ public enum Config { public List getChildren(@NotNull String path, boolean... deep) { return accessor.getChildren(path, deep); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/config/ConfigAccessor.java b/src/main/java/dev/loki/loparkour/config/core/ConfigAccessor.java similarity index 98% rename from src/main/java/dev/loki/loparkour/config/ConfigAccessor.java rename to src/main/java/dev/loki/loparkour/config/core/ConfigAccessor.java index 014f485..9b264f0 100644 --- a/src/main/java/dev/loki/loparkour/config/ConfigAccessor.java +++ b/src/main/java/dev/loki/loparkour/config/core/ConfigAccessor.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.core; import dev.loki.loparkour.LoParkour; import org.bukkit.configuration.ConfigurationSection; @@ -144,4 +144,4 @@ public class ConfigAccessor { "Missing config path '" + path + "' in " + fileName + ". Using a safe default value."); } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/config/ConfigLoader.java b/src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java similarity index 92% rename from src/main/java/dev/loki/loparkour/config/ConfigLoader.java rename to src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java index 899739c..05dfced 100644 --- a/src/main/java/dev/loki/loparkour/config/ConfigLoader.java +++ b/src/main/java/dev/loki/loparkour/config/core/ConfigLoader.java @@ -1,5 +1,7 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.core; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.config.options.Option; import dev.loki.loparkour.LoParkour; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -55,9 +57,9 @@ public class ConfigLoader { } public static void initializeDependentSystems(boolean firstLoad) { - dev.loki.loparkour.reward.Rewards.init(); + dev.loki.loparkour.reward.core.Rewards.init(); Locales.init(); - dev.loki.loparkour.schematic.Schematics.init(); + dev.loki.loparkour.schematic.registry.Schematics.init(); Option.init(firstLoad); } diff --git a/src/main/java/dev/loki/loparkour/config/ConfigUpdater.java b/src/main/java/dev/loki/loparkour/config/core/ConfigUpdater.java similarity index 99% rename from src/main/java/dev/loki/loparkour/config/ConfigUpdater.java rename to src/main/java/dev/loki/loparkour/config/core/ConfigUpdater.java index dd7d3af..f791bb9 100644 --- a/src/main/java/dev/loki/loparkour/config/ConfigUpdater.java +++ b/src/main/java/dev/loki/loparkour/config/core/ConfigUpdater.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.core; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/dev/loki/loparkour/config/LocaleCache.java b/src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java similarity index 97% rename from src/main/java/dev/loki/loparkour/config/LocaleCache.java rename to src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java index 3168958..f7c4b91 100644 --- a/src/main/java/dev/loki/loparkour/config/LocaleCache.java +++ b/src/main/java/dev/loki/loparkour/config/locale/LocaleCache.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.locale; import org.bukkit.configuration.file.FileConfiguration; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/config/LocaleLoader.java b/src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java similarity index 98% rename from src/main/java/dev/loki/loparkour/config/LocaleLoader.java rename to src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java index 8b1d1f4..a578309 100644 --- a/src/main/java/dev/loki/loparkour/config/LocaleLoader.java +++ b/src/main/java/dev/loki/loparkour/config/locale/LocaleLoader.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.locale; import dev.loki.loparkour.LoParkour; import org.bukkit.configuration.ConfigurationSection; diff --git a/src/main/java/dev/loki/loparkour/config/Locales.java b/src/main/java/dev/loki/loparkour/config/locale/Locales.java similarity index 81% rename from src/main/java/dev/loki/loparkour/config/Locales.java rename to src/main/java/dev/loki/loparkour/config/locale/Locales.java index e4e08b2..355fbdf 100644 --- a/src/main/java/dev/loki/loparkour/config/Locales.java +++ b/src/main/java/dev/loki/loparkour/config/locale/Locales.java @@ -1,10 +1,12 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.locale; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.util.Item; -import dev.loki.loparkour.util.Materials; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.util.text.ColorUtil; +import dev.loki.loparkour.util.item.Item; +import dev.loki.loparkour.util.item.Materials; import org.bukkit.Material; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; @@ -29,6 +31,7 @@ public class Locales { public static void init() { Map loaded = LocaleLoader.loadAll(); cache.setLocales(loaded); + dev.loki.loparkour.command.schematic.SchematicCommandHandler.clearWandCache(); LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)"); } @@ -61,6 +64,24 @@ public class Locales { return cache.cachedValue(locale, c -> c.getString(path), ""); } + /** + * Sends a colored message from locale. Safe for console ({@link #getString(CommandSender, String)}). + */ + public static void send(@NotNull CommandSender sender, @NotNull String path, Object... format) { + String message = getString(sender, path); + if (format.length > 0) { + message = message.formatted(format); + } + sender.sendMessage(ColorUtil.color(message)); + } + + /** + * Sends a colored message using the player's locale. + */ + public static void send(@NotNull Player player, @NotNull String path, Object... format) { + send((CommandSender) player, path, format); + } + @NotNull public static List getStringList(@NotNull String locale, @NotNull String path) { return cache.cachedValue(locale, c -> c.getStringList(path), Collections.emptyList()); @@ -107,11 +128,9 @@ public class Locales { String name = applyReplacements(config.getString(namePath, ""), replace); String lore = applyReplacements(config.getString(lorePath, ""), replace); - // Debug logging - if ("STONE".equals(material)) { - LoParkour.getPlugin().getLogger().warning("Material defaulted to STONE for path: " + path); - LoParkour.getPlugin().getLogger().warning(" Tried: " + materialPath + " = " + config.getString(materialPath)); - LoParkour.getPlugin().getLogger().warning(" Available keys: " + config.getKeys(true)); + if ("STONE".equals(material) && !config.contains(materialPath)) { + LoParkour.getPlugin().getLogger().warning( + "Missing locale key '%s' (material defaults to STONE)".formatted(materialPath)); } Material mat = Materials.parse(material); diff --git a/src/main/java/dev/loki/loparkour/config/Option.java b/src/main/java/dev/loki/loparkour/config/options/Option.java similarity index 95% rename from src/main/java/dev/loki/loparkour/config/Option.java rename to src/main/java/dev/loki/loparkour/config/options/Option.java index f605133..bf4167b 100644 --- a/src/main/java/dev/loki/loparkour/config/Option.java +++ b/src/main/java/dev/loki/loparkour/config/options/Option.java @@ -1,11 +1,12 @@ -package dev.loki.loparkour.config; +package dev.loki.loparkour.config.options; -import dev.loki.loparkour.api.Registry; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.config.options.*; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.style.RandomStyle; -import dev.loki.loparkour.style.Style; -import dev.loki.loparkour.util.ParticleData; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.style.core.RandomStyle; +import dev.loki.loparkour.style.core.Style; +import dev.loki.loparkour.util.particle.ParticleData; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; diff --git a/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java b/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java index 5f041e4..e3d4c70 100644 --- a/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java +++ b/src/main/java/dev/loki/loparkour/config/options/OptionGeneral.java @@ -1,8 +1,8 @@ package dev.loki.loparkour.config.options; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.menu.core.ParkourOption; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; diff --git a/src/main/java/dev/loki/loparkour/config/options/OptionGeneration.java b/src/main/java/dev/loki/loparkour/config/options/OptionGeneration.java index 0026e72..d3a81cb 100644 --- a/src/main/java/dev/loki/loparkour/config/options/OptionGeneration.java +++ b/src/main/java/dev/loki/loparkour/config/options/OptionGeneration.java @@ -1,7 +1,7 @@ package dev.loki.loparkour.config.options; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/dev/loki/loparkour/config/options/OptionParticles.java b/src/main/java/dev/loki/loparkour/config/options/OptionParticles.java index fb2b0c2..91136c1 100644 --- a/src/main/java/dev/loki/loparkour/config/options/OptionParticles.java +++ b/src/main/java/dev/loki/loparkour/config/options/OptionParticles.java @@ -1,9 +1,9 @@ package dev.loki.loparkour.config.options; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.util.ParticleData; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.util.particle.ParticleData; import org.bukkit.Particle; import org.bukkit.Sound; diff --git a/src/main/java/dev/loki/loparkour/config/options/OptionSQL.java b/src/main/java/dev/loki/loparkour/config/options/OptionSQL.java index 68c89af..7900324 100644 --- a/src/main/java/dev/loki/loparkour/config/options/OptionSQL.java +++ b/src/main/java/dev/loki/loparkour/config/options/OptionSQL.java @@ -1,7 +1,7 @@ package dev.loki.loparkour.config.options; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; /** * SQL database configuration options. diff --git a/src/main/java/dev/loki/loparkour/config/options/OptionStyles.java b/src/main/java/dev/loki/loparkour/config/options/OptionStyles.java index bbcc8de..459d1a4 100644 --- a/src/main/java/dev/loki/loparkour/config/options/OptionStyles.java +++ b/src/main/java/dev/loki/loparkour/config/options/OptionStyles.java @@ -1,9 +1,9 @@ package dev.loki.loparkour.config.options; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.ConfigAccessor; -import dev.loki.loparkour.style.Style; -import dev.loki.loparkour.util.Materials; +import dev.loki.loparkour.config.core.ConfigAccessor; +import dev.loki.loparkour.style.core.Style; +import dev.loki.loparkour.util.item.Materials; import org.bukkit.Material; import org.bukkit.configuration.file.FileConfiguration; diff --git a/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorEventListener.java b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorEventListener.java new file mode 100644 index 0000000..7180005 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorEventListener.java @@ -0,0 +1,53 @@ +package dev.loki.loparkour.generator.core.coordinator; + +import dev.loki.loparkour.generator.jump.calculation.JumpType; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * Interface for listening to generator events. + * Allows adaptive system to integrate without creating direct dependencies. + * + * This decouples the generator from the adaptive system: + * - Generator fires events through this interface + * - Adaptive system implements this interface to collect metrics + * - No circular dependencies between generator and adaptive packages + */ +public interface GeneratorEventListener { + + /** + * Called when a block is generated. + * + * @param player The player for whom the block was generated + * @param block The generated block + * @param jumpType The type of jump required to reach this block + * @param distance The distance from the previous block + */ + void onBlockGenerated(@NotNull Player player, @NotNull Block block, @NotNull JumpType jumpType, double distance); + + /** + * Called when a player successfully lands on a block and scores. + * + * @param player The player who scored + * @param block The block the player landed on + * @param distance The distance of the jump + */ + void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance); + + /** + * Called when a player falls off the parkour. + * + * @param player The player who fell + * @param lastBlock The last block the player was on (may be null) + */ + void onPlayerFall(@NotNull Player player, Block lastBlock); + + /** + * Called when a player performs a jump. + * + * @param player The player who jumped + * @param jumpType The type of jump performed + */ + void onJump(@NotNull Player player, @NotNull JumpType jumpType); +} diff --git a/src/main/java/dev/loki/loparkour/generator/GeneratorProfileManager.java b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java similarity index 62% rename from src/main/java/dev/loki/loparkour/generator/GeneratorProfileManager.java rename to src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java index cb79f9d..79e6125 100644 --- a/src/main/java/dev/loki/loparkour/generator/GeneratorProfileManager.java +++ b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorProfileManager.java @@ -1,6 +1,9 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.coordinator; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.generator.core.model.Profile; +import dev.loki.loparkour.generator.profile.GenerationProfileLoader; import org.bukkit.block.data.BlockData; import org.jetbrains.annotations.NotNull; @@ -10,22 +13,31 @@ import java.util.Map; * Manages generator profiles and difficulty calculations. */ public class GeneratorProfileManager { - + private final ParkourGenerator generator; - + public GeneratorProfileManager(@NotNull ParkourGenerator generator) { this.generator = generator; } - + /** - * Override profile settings based on generator options. + * Rebuilds profile from config, player settings, and adaptive difficulty when enabled. */ public void overrideProfile() { - // Generator options are for disabling features, not difficulty - // Difficulty is controlled by profile settings - // This method can be used to apply custom profile overrides if needed + Profile rebuilt = GenerationProfileLoader.loadFromConfig(); + GenerationProfileLoader.preservePlayerSettings(generator.profile, rebuilt); + AdaptiveServices.applyAdaptiveProfile(generator, rebuilt); + GenerationProfileLoader.copyInto(generator.profile, rebuilt); } - + + /** + * Recalculates chances after a fall or settings change. + */ + public void recalculateProfile() { + overrideProfile(); + calculateChances(); + } + /** * Calculate generation chances based on current profile. */ @@ -35,112 +47,113 @@ public class GeneratorProfileManager { calculateDefaultChances(); calculateSpecialChances(); } - + /** * Calculate difficulty score based on current settings. */ public double getDifficultyScore() { double score = 0.0; - - // Distance difficulty + for (Map.Entry entry : generator.state.distanceChances.entrySet()) { score += entry.getKey() * entry.getValue() * 0.3; } - - // Height difficulty + for (Map.Entry entry : generator.state.heightChances.entrySet()) { score += Math.abs(entry.getKey()) * entry.getValue() * 0.2; } - - // Special blocks difficulty + for (Map.Entry entry : generator.state.specialChances.entrySet()) { score += entry.getValue() * 0.5; } - + return Math.round(score * 100.0) / 100.0; } - + private void calculateDistanceChances() { generator.state.distanceChances.clear(); - - // Get distance range from config - int minDistance = Config.GENERATION.getInt("advanced.distance.min", 1); - int maxDistance = Config.GENERATION.getInt("advanced.distance.max", 4); - + + int minDistance = profileInt("distance.min", "advanced.distance.min", 1); + int maxDistance = profileInt("distance.max", "advanced.distance.max", 4); + for (int distance = minDistance; distance <= maxDistance; distance++) { - int chance = Config.GENERATION.getInt("advanced.distance.chances." + distance, 1); + int chance = profileInt( + "distance.chances." + distance, + "advanced.distance.chances." + distance, + 1); generator.state.distanceChances.put(distance, (double) chance); } - - // Normalize chances to sum to 1.0 + normalizeMap(generator.state.distanceChances); } - + private void calculateHeightChances() { generator.state.heightChances.clear(); - - // Get height range from config - int minHeight = Config.GENERATION.getInt("advanced.height.min", -1); - int maxHeight = Config.GENERATION.getInt("advanced.height.max", 2); - + + int minHeight = profileInt("height.min", "advanced.height.min", -1); + int maxHeight = profileInt("height.max", "advanced.height.max", 2); + for (int height = minHeight; height <= maxHeight; height++) { - int chance = Config.GENERATION.getInt("advanced.height.chances." + height, 1); + int chance = profileInt( + "height.chances." + height, + "advanced.height.chances." + height, + 1); generator.state.heightChances.put(height, (double) chance); } - - // Normalize chances to sum to 1.0 + normalizeMap(generator.state.heightChances); } - + private void calculateDefaultChances() { generator.state.defaultChances.clear(); - - int defaultChance = Config.GENERATION.getInt("advanced.type.default", 80); - int specialChance = Config.GENERATION.getInt("advanced.type.special", 15); - int schematicChance = Config.GENERATION.getInt("advanced.type.schematic", 5); - + + int defaultChance = profileInt("type.default", "advanced.type.default", 80); + int specialChance = profileInt("type.special", "advanced.type.special", 15); + int schematicChance = profileInt("type.schematic", "advanced.type.schematic", 5); + generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.DEFAULT, (double) defaultChance); generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, (double) specialChance); generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SCHEMATIC, (double) schematicChance); - - // Normalize chances to sum to 1.0 + normalizeMap(generator.state.defaultChances); } - + private void calculateSpecialChances() { generator.state.specialChances.clear(); - + try { for (String specialType : Config.GENERATION.getChildren("advanced.special")) { String materialPath = "advanced.special." + specialType + ".material"; String chancePath = "advanced.special." + specialType + ".chance"; - + String materialName = Config.GENERATION.getString(materialPath); int chance = Config.GENERATION.getInt(chancePath, 1); - + BlockData blockData = org.bukkit.Bukkit.createBlockData(materialName); generator.state.specialChances.put(blockData, (double) chance); } - - // Normalize chances to sum to 1.0 + normalizeMap(generator.state.specialChances); - } catch (Exception e) { - // If special blocks config is missing, just skip + } catch (Exception ignored) { + // special blocks config optional } } - - /** - * Normalize a map so all values sum to 1.0 - * If sum is 0, distributes values equally. - */ + + private int profileInt(@NotNull String profileKey, @NotNull String configKey, int defaultValue) { + if (generator.profile.settings.containsKey(profileKey)) { + return generator.profile.get(profileKey).asInt(); + } + return Config.GENERATION.getInt(configKey, defaultValue); + } + private void normalizeMap(Map map) { - if (map.isEmpty()) return; + if (map.isEmpty()) { + return; + } double sum = map.values().stream().mapToDouble(Double::doubleValue).sum(); if (sum > 0) { map.replaceAll((k, v) -> v / sum); } else { - // If all values are 0, distribute equally double equalValue = 1.0 / map.size(); map.replaceAll((k, v) -> equalValue); } diff --git a/src/main/java/dev/loki/loparkour/generator/GeneratorStatistics.java b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorStatistics.java similarity index 89% rename from src/main/java/dev/loki/loparkour/generator/GeneratorStatistics.java rename to src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorStatistics.java index fba61fb..bfce256 100644 --- a/src/main/java/dev/loki/loparkour/generator/GeneratorStatistics.java +++ b/src/main/java/dev/loki/loparkour/generator/core/coordinator/GeneratorStatistics.java @@ -1,10 +1,10 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.coordinator; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.jetbrains.annotations.NotNull; import java.text.SimpleDateFormat; @@ -94,4 +94,4 @@ public class GeneratorStatistics { return formatter.format(date); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java b/src/main/java/dev/loki/loparkour/generator/core/coordinator/ParkourGenerator.java similarity index 64% rename from src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java rename to src/main/java/dev/loki/loparkour/generator/core/coordinator/ParkourGenerator.java index 363616c..7c4ce04 100644 --- a/src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java +++ b/src/main/java/dev/loki/loparkour/generator/core/coordinator/ParkourGenerator.java @@ -1,19 +1,23 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.coordinator; -import dev.loki.loparkour.generator.jump.BlockPlacer; -import dev.loki.loparkour.generator.lifecycle.GeneratorLifecycle; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourSpectator; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.generator.core.model.GeneratorOption; +import dev.loki.loparkour.generator.core.model.GeneratorState; +import dev.loki.loparkour.generator.core.model.Island; +import dev.loki.loparkour.generator.core.model.Profile; +import dev.loki.loparkour.generator.jump.placement.BlockPlacer; +import dev.loki.loparkour.generator.lifecycle.loop.GeneratorLifecycle; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.session.core.Session; import org.bukkit.Location; import org.bukkit.block.Block; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.List; /** @@ -32,37 +36,38 @@ public class ParkourGenerator { protected final GeneratorLifecycle lifecycle; public final GeneratorProfileManager profileManager; private final GeneratorStatistics statistics; + + // Event listeners for adaptive system integration + private final List eventListeners; - public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic, GeneratorOption... generatorOptions) { + public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) { this.session = session; this.generatorOptions = generatorOptions; - this.island = new Island(session, schematic); + this.island = new Island(session); this.profile = new Profile(); this.state = new GeneratorState(); - + this.eventListeners = new ArrayList<>(); + // Initialize zone from session if (session != null) { - Location[] selection = dev.loki.loparkour.world.Divider.toSelection(session); + Location[] selection = dev.loki.loparkour.world.core.Divider.toSelection(session); if (selection == null || selection.length < 2) { throw new IllegalArgumentException("Invalid session zone: selection must contain at least 2 locations"); } this.state.zone = selection; } - + // Initialize components this.placer = new BlockPlacer(this); this.lifecycle = new GeneratorLifecycle(this); this.profileManager = new GeneratorProfileManager(this); this.statistics = new GeneratorStatistics(this); - + // Setup initial state profileManager.overrideProfile(); profileManager.calculateChances(); } - public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) { - this(session, null, generatorOptions); - } // Core generation methods public void generate() { placer.generate(); } @@ -125,8 +130,41 @@ public class ParkourGenerator { if (state.history.isEmpty()) return null; return state.history.get(state.history.size() - 1); } - + + // Event listener management + + /** + * Register an event listener for generator events. + * Used by adaptive system to collect metrics without creating direct dependencies. + * + * @param listener The listener to register + */ + public void registerEventListener(@NotNull GeneratorEventListener listener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener); + } + } + + /** + * Unregister an event listener. + * + * @param listener The listener to unregister + */ + public void unregisterEventListener(@NotNull GeneratorEventListener listener) { + eventListeners.remove(listener); + } + + /** + * Get all registered event listeners. + * + * @return Immutable list of event listeners + */ + @NotNull + public List getEventListeners() { + return new ArrayList<>(eventListeners); + } + public enum BlockGenerationType { DEFAULT, SPECIAL, SCHEMATIC } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/GeneratorOption.java b/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorOption.java similarity index 77% rename from src/main/java/dev/loki/loparkour/generator/GeneratorOption.java rename to src/main/java/dev/loki/loparkour/generator/core/model/GeneratorOption.java index f7c060e..22ff7d1 100644 --- a/src/main/java/dev/loki/loparkour/generator/GeneratorOption.java +++ b/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorOption.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.model; /** * Options for {@link ParkourGenerator}s. diff --git a/src/main/java/dev/loki/loparkour/generator/GeneratorState.java b/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java similarity index 94% rename from src/main/java/dev/loki/loparkour/generator/GeneratorState.java rename to src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java index ec8d0a6..33c3cee 100644 --- a/src/main/java/dev/loki/loparkour/generator/GeneratorState.java +++ b/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java @@ -1,5 +1,6 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.model; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; diff --git a/src/main/java/dev/loki/loparkour/generator/Island.java b/src/main/java/dev/loki/loparkour/generator/core/model/Island.java similarity index 81% rename from src/main/java/dev/loki/loparkour/generator/Island.java rename to src/main/java/dev/loki/loparkour/generator/core/model/Island.java index 48715e4..9a8db2f 100644 --- a/src/main/java/dev/loki/loparkour/generator/Island.java +++ b/src/main/java/dev/loki/loparkour/generator/core/model/Island.java @@ -1,9 +1,9 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.model; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.Materials; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.session.core.Session; +import dev.loki.loparkour.util.item.Materials; import org.bukkit.Location; import org.bukkit.Material; @@ -12,8 +12,6 @@ import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.util.ArrayList; import java.util.List; @@ -26,16 +24,11 @@ public final class Island { private static final int PLATFORM_RADIUS = 4; public final Session session; - /** - * Unused for spawn; kept for API / {@link dev.loki.loparkour.generator.jump.BlockPlacer} schematic branch. - */ - public final dev.loki.loparkour.schematic.lpschem.LPSchematic schematic; public List blocks; - public Island(@NotNull Session session, @Nullable dev.loki.loparkour.schematic.lpschem.LPSchematic schematic) { + public Island(@NotNull Session session) { this.session = session; - this.schematic = schematic; } public void build(Location location) { diff --git a/src/main/java/dev/loki/loparkour/generator/Profile.java b/src/main/java/dev/loki/loparkour/generator/core/model/Profile.java similarity index 96% rename from src/main/java/dev/loki/loparkour/generator/Profile.java rename to src/main/java/dev/loki/loparkour/generator/core/model/Profile.java index 5bca40a..4629f94 100644 --- a/src/main/java/dev/loki/loparkour/generator/Profile.java +++ b/src/main/java/dev/loki/loparkour/generator/core/model/Profile.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.generator; +package dev.loki.loparkour.generator.core.model; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/generator/effect/EffectManager.java b/src/main/java/dev/loki/loparkour/generator/effect/EffectManager.java index 2ac536d..5bf8240 100644 --- a/src/main/java/dev/loki/loparkour/generator/effect/EffectManager.java +++ b/src/main/java/dev/loki/loparkour/generator/effect/EffectManager.java @@ -1,9 +1,9 @@ package dev.loki.loparkour.generator.effect; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.util.ParticleUtil; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.util.particle.ParticleUtil; import org.bukkit.Location; import org.bukkit.block.Block; diff --git a/src/main/java/dev/loki/loparkour/generator/jump/JumpCalculator.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java similarity index 95% rename from src/main/java/dev/loki/loparkour/generator/jump/JumpCalculator.java rename to src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java index d07926f..72ca180 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/JumpCalculator.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java @@ -1,8 +1,9 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.calculation; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.generator.GeneratorOption; -import dev.loki.loparkour.generator.ParkourGenerator; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.model.GeneratorOption; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.jump.placement.BlockSelector; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; @@ -162,4 +163,4 @@ public class JumpCalculator { this.maxDistance = maxDistance; } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/jump/JumpDirector.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java similarity index 98% rename from src/main/java/dev/loki/loparkour/generator/jump/JumpDirector.java rename to src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java index 315f147..d79d551 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/JumpDirector.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpDirector.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.calculation; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; diff --git a/src/main/java/dev/loki/loparkour/generator/jump/JumpOffsetGenerator.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpOffsetGenerator.java similarity index 96% rename from src/main/java/dev/loki/loparkour/generator/jump/JumpOffsetGenerator.java rename to src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpOffsetGenerator.java index d95da1b..501829f 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/JumpOffsetGenerator.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpOffsetGenerator.java @@ -1,6 +1,6 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.calculation; -import dev.loki.loparkour.util.Probs; +import dev.loki.loparkour.util.misc.Probs; import java.util.Map; import java.util.Random; diff --git a/src/main/java/dev/loki/loparkour/generator/jump/JumpType.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpType.java similarity index 96% rename from src/main/java/dev/loki/loparkour/generator/jump/JumpType.java rename to src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpType.java index 05a5ca6..58e277e 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/JumpType.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpType.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.calculation; import org.bukkit.Material; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/generator/jump/JumpValidator.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java similarity index 99% rename from src/main/java/dev/loki/loparkour/generator/jump/JumpValidator.java rename to src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java index 5da8dea..6b9a0d5 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/JumpValidator.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpValidator.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.calculation; import org.bukkit.Location; import org.bukkit.util.Vector; diff --git a/src/main/java/dev/loki/loparkour/generator/jump/BlockPlacer.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java similarity index 76% rename from src/main/java/dev/loki/loparkour/generator/jump/BlockPlacer.java rename to src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java index 057e70d..44aa578 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/BlockPlacer.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java @@ -1,9 +1,13 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.placement; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; -import dev.loki.loparkour.util.Probs; +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.jump.calculation.JumpCalculator; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.schematic.core.ParkourSchematic; +import dev.loki.loparkour.schematic.core.SchematicManager; +import dev.loki.loparkour.util.misc.Probs; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.Sound; @@ -154,31 +158,38 @@ public class BlockPlacer { } private boolean tryGenerateSchematic() { - // Simplified schematic generation - LPSchematic schematic = generator.island.schematic; // Получаем схематик из island - if (schematic == null) return false; - - List blocks = selectBlocks(); - if (blocks.isEmpty()) return false; - - Block targetBlock = blocks.get(0); - Location pasteLocation = targetBlock.getLocation(); - - // Paste schematic (simplified) - List schematicBlocks = pasteSchematic(schematic, pasteLocation); - - if (!schematicBlocks.isEmpty()) { - generator.state.history.addAll(schematicBlocks); - generator.state.schematicBlocks = schematicBlocks; - return true; + SchematicManager manager = LoParkour.getSchematicManager(); + if (manager == null) { + return false; } - - return false; + + double maxDifficulty = generator.getPlayers().stream() + .mapToDouble(p -> p.schematicDifficulty) + .min() + .orElse(1.0); + + ParkourSchematic schematic = manager.pick(maxDifficulty); + if (schematic == null) { + return false; + } + + List blocks = selectBlocks(); + if (blocks.isEmpty()) { + return false; + } + + Location pasteLocation = blocks.get(0).getLocation(); + List schematicBlocks = schematic.paste(pasteLocation, pasteLocation.getWorld()); + if (schematicBlocks.isEmpty()) { + return false; + } + + generator.state.history.addAll(schematicBlocks); + generator.state.schematicBlocks = schematicBlocks; + + for (ParkourPlayer player : generator.getPlayers()) { + new ParkourSchematicGenerateEvent(schematic, generator, player).call(); + } + return true; } - - @NotNull - private List pasteSchematic(@NotNull LPSchematic schematic, @NotNull Location location) { - // Use the schematic's built-in paste method - return schematic.paste(location, location.getWorld()); - } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/jump/BlockSelector.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java similarity index 94% rename from src/main/java/dev/loki/loparkour/generator/jump/BlockSelector.java rename to src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java index 0ab804f..d8a6746 100644 --- a/src/main/java/dev/loki/loparkour/generator/jump/BlockSelector.java +++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockSelector.java @@ -1,8 +1,9 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.placement; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.style.Style; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.jump.calculation.JumpType; +import dev.loki.loparkour.style.core.Style; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.jetbrains.annotations.NotNull; @@ -138,4 +139,4 @@ public class BlockSelector { material == Material.WARPED_TRAPDOOR || material == Material.IRON_TRAPDOOR; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/GeneratorLifecycle.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/GeneratorLifecycle.java similarity index 87% rename from src/main/java/dev/loki/loparkour/generator/lifecycle/GeneratorLifecycle.java rename to src/main/java/dev/loki/loparkour/generator/lifecycle/loop/GeneratorLifecycle.java index 2ef9a9d..2efdf79 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/GeneratorLifecycle.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/GeneratorLifecycle.java @@ -1,6 +1,8 @@ -package dev.loki.loparkour.generator.lifecycle; +package dev.loki.loparkour.generator.lifecycle.loop; -import dev.loki.loparkour.generator.ParkourGenerator; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup; +import dev.loki.loparkour.generator.lifecycle.player.PlayerInteractionHandler; import org.jetbrains.annotations.NotNull; /** diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleEventManager.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java similarity index 90% rename from src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleEventManager.java rename to src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java index 80168be..cc2d69f 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleEventManager.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java @@ -1,12 +1,12 @@ -package dev.loki.loparkour.generator.lifecycle; +package dev.loki.loparkour.generator.lifecycle.loop; import dev.loki.loparkour.api.event.ParkourFallEvent; import dev.loki.loparkour.api.event.ParkourScoreEvent; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.reward.Reward; -import dev.loki.loparkour.reward.Rewards; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.reward.core.Reward; +import dev.loki.loparkour.reward.core.Rewards; import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; @@ -104,4 +104,4 @@ public class LifecycleEventManager { // Reset generator state generator.reset(true); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleTickManager.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java similarity index 91% rename from src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleTickManager.java rename to src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java index 14ce48b..ec45978 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleTickManager.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java @@ -1,7 +1,8 @@ -package dev.loki.loparkour.generator.lifecycle; +package dev.loki.loparkour.generator.lifecycle.loop; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.generator.ParkourGenerator; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup; import org.jetbrains.annotations.NotNull; /** diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleVisualUpdater.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleVisualUpdater.java similarity index 88% rename from src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleVisualUpdater.java rename to src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleVisualUpdater.java index 8aaef82..b438f05 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/LifecycleVisualUpdater.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleVisualUpdater.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.generator.lifecycle; +package dev.loki.loparkour.generator.lifecycle.loop; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.jetbrains.annotations.NotNull; /** diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/GeneratorCleanup.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java similarity index 93% rename from src/main/java/dev/loki/loparkour/generator/lifecycle/GeneratorCleanup.java rename to src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java index aa277e6..18d537c 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/GeneratorCleanup.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java @@ -1,8 +1,8 @@ -package dev.loki.loparkour.generator.lifecycle; +package dev.loki.loparkour.generator.lifecycle.player; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; @@ -115,7 +115,7 @@ public class GeneratorCleanup { Location blockSpawn = spawn.clone(); // Move block forward based on heading (6 blocks forward, same height) - switch (dev.loki.loparkour.config.Option.HEADING) { + switch (dev.loki.loparkour.config.options.Option.HEADING) { case NORTH -> blockSpawn.add(0, 0, -6); case SOUTH -> blockSpawn.add(0, 0, 6); case WEST -> blockSpawn.add(-6, 0, 0); @@ -127,4 +127,4 @@ public class GeneratorCleanup { } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/PlayerInteractionHandler.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java similarity index 59% rename from src/main/java/dev/loki/loparkour/generator/lifecycle/PlayerInteractionHandler.java rename to src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java index 8f1ac26..530e09f 100644 --- a/src/main/java/dev/loki/loparkour/generator/lifecycle/PlayerInteractionHandler.java +++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/PlayerInteractionHandler.java @@ -1,8 +1,10 @@ -package dev.loki.loparkour.generator.lifecycle; +package dev.loki.loparkour.generator.lifecycle.player; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.schematic.lpschem.LPSchematic; +import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.core.coordinator.GeneratorEventListener; +import dev.loki.loparkour.generator.lifecycle.loop.LifecycleEventManager; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; @@ -47,7 +49,18 @@ public class PlayerInteractionHandler { public void handleScore() { generator.state.score++; eventManager.handleScore(); - + + // Fire event listeners for adaptive system + Block latestBlock = generator.getLatest(); + if (latestBlock != null) { + for (ParkourPlayer player : generator.getPlayers()) { + double distance = calculateDistance(player, latestBlock); + for (GeneratorEventListener listener : generator.getEventListeners()) { + listener.onPlayerScore(player.player, latestBlock, distance); + } + } + } + // Generate more blocks after scoring generator.generate(1); } @@ -56,7 +69,20 @@ public class PlayerInteractionHandler { * Handle player falling. */ public void handleFall() { + // Fire event listeners for adaptive system + Block latestBlock = generator.getLatest(); + for (ParkourPlayer player : generator.getPlayers()) { + for (GeneratorEventListener listener : generator.getEventListeners()) { + listener.onPlayerFall(player.player, latestBlock); + } + } + eventManager.handleFall(); + + for (ParkourPlayer player : generator.getPlayers()) { + AdaptiveServices.persistRatingAfterFall(player); + } + AdaptiveServices.refreshGeneratorDifficulty(generator); } private void handleBlockInteraction(@NotNull ParkourPlayer player, @NotNull Block block) { @@ -84,12 +110,8 @@ public class PlayerInteractionHandler { } private boolean isSchematicEndBlock(@NotNull Block block) { - LPSchematic schematic = generator.island.schematic; - if (schematic == null) return false; - - // Simplified check - would need full schematic logic - return generator.state.schematicBlocks != null && - generator.state.schematicBlocks.contains(block); + return generator.state.schematicBlocks != null + && generator.state.schematicBlocks.contains(block); } /** Block directly under feet; {@code null} in air — scoring runs when landed. */ @@ -100,4 +122,13 @@ public class PlayerInteractionHandler { return below.getType() != Material.AIR ? below : null; } + + /** + * Calculate distance between player and block. + */ + private double calculateDistance(@NotNull ParkourPlayer player, @NotNull Block block) { + Location playerLoc = player.player.getLocation(); + Location blockLoc = block.getLocation().add(0.5, 0, 0.5); + return playerLoc.distance(blockLoc); + } } diff --git a/src/main/java/dev/loki/loparkour/generator/profile/GenerationProfileLoader.java b/src/main/java/dev/loki/loparkour/generator/profile/GenerationProfileLoader.java new file mode 100644 index 0000000..00a37ee --- /dev/null +++ b/src/main/java/dev/loki/loparkour/generator/profile/GenerationProfileLoader.java @@ -0,0 +1,68 @@ +package dev.loki.loparkour.generator.profile; + +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.generator.core.model.Profile; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Set; + +/** + * Loads default generation weights from generation.yml into a {@link Profile}. + */ +public final class GenerationProfileLoader { + + private static final Set PLAYER_KEYS = Set.of( + "schematicDifficulty", "blockLead", "particles", "sound", + "useSpecialBlocks", "showFallMessage", "showScoreboard", + "selectedTime", "style" + ); + + private GenerationProfileLoader() { + } + + @NotNull + public static Profile loadFromConfig() { + Profile profile = new Profile(); + + int minDistance = Config.GENERATION.getInt("advanced.distance.min", 1); + int maxDistance = Config.GENERATION.getInt("advanced.distance.max", 4); + profile.set("distance.min", String.valueOf(minDistance)); + profile.set("distance.max", String.valueOf(maxDistance)); + for (int distance = minDistance; distance <= maxDistance; distance++) { + int chance = Config.GENERATION.getInt("advanced.distance.chances." + distance, 1); + profile.set("distance.chances." + distance, String.valueOf(chance)); + } + + int minHeight = Config.GENERATION.getInt("advanced.height.min", -1); + int maxHeight = Config.GENERATION.getInt("advanced.height.max", 2); + profile.set("height.min", String.valueOf(minHeight)); + profile.set("height.max", String.valueOf(maxHeight)); + for (int height = minHeight; height <= maxHeight; height++) { + int chance = Config.GENERATION.getInt("advanced.height.chances." + height, 1); + profile.set("height.chances." + height, String.valueOf(chance)); + } + + profile.set("type.default", String.valueOf(Config.GENERATION.getInt("advanced.type.default", 80))); + profile.set("type.special", String.valueOf(Config.GENERATION.getInt("advanced.type.special", 15))); + profile.set("type.schematic", String.valueOf(Config.GENERATION.getInt("advanced.type.schematic", 5))); + + return profile; + } + + public static void preservePlayerSettings(@NotNull Profile from, @NotNull Profile to) { + for (String key : PLAYER_KEYS) { + if (from.settings.containsKey(key)) { + to.set(key, from.get(key).value()); + } + } + } + + /** Replaces all settings on {@code target} with a snapshot from {@code source}. */ + public static void copyInto(@NotNull Profile target, @NotNull Profile source) { + target.settings.clear(); + for (Map.Entry entry : source.settings.entrySet()) { + target.set(entry.getKey(), entry.getValue().value()); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostManager.java b/src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java similarity index 97% rename from src/main/java/dev/loki/loparkour/ghost/GhostManager.java rename to src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java index 2c9cc1f..78efaa4 100644 --- a/src/main/java/dev/loki/loparkour/ghost/GhostManager.java +++ b/src/main/java/dev/loki/loparkour/ghost/core/GhostManager.java @@ -1,7 +1,9 @@ -package dev.loki.loparkour.ghost; +package dev.loki.loparkour.ghost.core; +import dev.loki.loparkour.ghost.core.GhostPlayer; +import dev.loki.loparkour.ghost.model.GhostData; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; import org.bukkit.Location; import org.bukkit.World; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java b/src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java similarity index 94% rename from src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java rename to src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java index ce98fff..9893dab 100644 --- a/src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java +++ b/src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java @@ -1,4 +1,6 @@ -package dev.loki.loparkour.ghost; +package dev.loki.loparkour.ghost.core; +import dev.loki.loparkour.ghost.model.GhostFrame; +import dev.loki.loparkour.ghost.model.GhostData; import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.ScheduledTask; diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java b/src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java similarity index 93% rename from src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java rename to src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java index dcc52b0..ca9f696 100644 --- a/src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java +++ b/src/main/java/dev/loki/loparkour/ghost/core/GhostRecorder.java @@ -1,4 +1,6 @@ -package dev.loki.loparkour.ghost; +package dev.loki.loparkour.ghost.core; +import dev.loki.loparkour.ghost.model.GhostFrame; +import dev.loki.loparkour.ghost.model.GhostData; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostData.java b/src/main/java/dev/loki/loparkour/ghost/model/GhostData.java similarity index 99% rename from src/main/java/dev/loki/loparkour/ghost/GhostData.java rename to src/main/java/dev/loki/loparkour/ghost/model/GhostData.java index 0786657..7bf7ba8 100644 --- a/src/main/java/dev/loki/loparkour/ghost/GhostData.java +++ b/src/main/java/dev/loki/loparkour/ghost/model/GhostData.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.ghost; +package dev.loki.loparkour.ghost.model; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostFrame.java b/src/main/java/dev/loki/loparkour/ghost/model/GhostFrame.java similarity index 96% rename from src/main/java/dev/loki/loparkour/ghost/GhostFrame.java rename to src/main/java/dev/loki/loparkour/ghost/model/GhostFrame.java index 9e86db6..49067ae 100644 --- a/src/main/java/dev/loki/loparkour/ghost/GhostFrame.java +++ b/src/main/java/dev/loki/loparkour/ghost/model/GhostFrame.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.ghost; +package dev.loki.loparkour.ghost.model; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/hook/FloodgateHook.java b/src/main/java/dev/loki/loparkour/hook/floodgate/FloodgateHook.java similarity index 91% rename from src/main/java/dev/loki/loparkour/hook/FloodgateHook.java rename to src/main/java/dev/loki/loparkour/hook/floodgate/FloodgateHook.java index 87cb487..d1c5320 100644 --- a/src/main/java/dev/loki/loparkour/hook/FloodgateHook.java +++ b/src/main/java/dev/loki/loparkour/hook/floodgate/FloodgateHook.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.hook; +package dev.loki.loparkour.hook.floodgate; import org.bukkit.entity.Player; import org.geysermc.floodgate.api.FloodgateApi; diff --git a/src/main/java/dev/loki/loparkour/hook/HoloHook.java b/src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java similarity index 92% rename from src/main/java/dev/loki/loparkour/hook/HoloHook.java rename to src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java index dffec1b..a337664 100644 --- a/src/main/java/dev/loki/loparkour/hook/HoloHook.java +++ b/src/main/java/dev/loki/loparkour/hook/holo/HoloHook.java @@ -1,10 +1,10 @@ -package dev.loki.loparkour.hook; +package dev.loki.loparkour.hook.holo; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.mode.Mode; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.mode.base.Mode; import me.filoghost.holographicdisplays.api.HolographicDisplaysAPI; public class HoloHook { @@ -94,4 +94,4 @@ public class HoloHook { } return "?"; } -} +} diff --git a/src/main/java/dev/loki/loparkour/hook/PAPIHook.java b/src/main/java/dev/loki/loparkour/hook/papi/PAPIHook.java similarity index 93% rename from src/main/java/dev/loki/loparkour/hook/PAPIHook.java rename to src/main/java/dev/loki/loparkour/hook/papi/PAPIHook.java index 9d82ccf..c696daa 100644 --- a/src/main/java/dev/loki/loparkour/hook/PAPIHook.java +++ b/src/main/java/dev/loki/loparkour/hook/papi/PAPIHook.java @@ -1,15 +1,15 @@ -package dev.loki.loparkour.hook; +package dev.loki.loparkour.hook.papi; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourSpectator; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.player.core.ParkourUser; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/hook/VaultHook.java b/src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java similarity index 96% rename from src/main/java/dev/loki/loparkour/hook/VaultHook.java rename to src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java index 3971c1b..ef3c63f 100644 --- a/src/main/java/dev/loki/loparkour/hook/VaultHook.java +++ b/src/main/java/dev/loki/loparkour/hook/vault/VaultHook.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.hook; +package dev.loki.loparkour.hook.vault; import dev.loki.loparkour.LoParkour; import net.milkbowl.vault.economy.Economy; diff --git a/src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java b/src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java similarity index 96% rename from src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java rename to src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java index 7074345..0266e3f 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/Leaderboard.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/core/Leaderboard.java @@ -1,4 +1,6 @@ -package dev.loki.loparkour.leaderboard; +package dev.loki.loparkour.leaderboard.core; +import dev.loki.loparkour.leaderboard.persistence.LeaderboardStorage; +import dev.loki.loparkour.leaderboard.model.Score; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java b/src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java similarity index 97% rename from src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java rename to src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java index b18f1d1..6c73f19 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardSorter.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java @@ -1,4 +1,5 @@ -package dev.loki.loparkour.leaderboard; +package dev.loki.loparkour.leaderboard.core; +import dev.loki.loparkour.leaderboard.model.Score; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/leaderboard/Score.java b/src/main/java/dev/loki/loparkour/leaderboard/model/Score.java similarity index 97% rename from src/main/java/dev/loki/loparkour/leaderboard/Score.java rename to src/main/java/dev/loki/loparkour/leaderboard/model/Score.java index 327ef20..231af37 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/Score.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/model/Score.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.leaderboard; +package dev.loki.loparkour.leaderboard.model; /** * Represents a record, used to keep track of the score a player may achieve. diff --git a/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java b/src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java similarity index 89% rename from src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java rename to src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java index 17498c5..44c272d 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/LeaderboardStorage.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java @@ -1,7 +1,10 @@ -package dev.loki.loparkour.leaderboard; +package dev.loki.loparkour.leaderboard.persistence; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.leaderboard.core.LeaderboardSorter; +import dev.loki.loparkour.leaderboard.core.Leaderboard; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.storage.Storage; import dev.lolib.scheduler.Scheduler; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/listener/SchematicWandListener.java b/src/main/java/dev/loki/loparkour/listener/SchematicWandListener.java deleted file mode 100644 index 76096e5..0000000 --- a/src/main/java/dev/loki/loparkour/listener/SchematicWandListener.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.loki.loparkour.listener; - -import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.command.SchematicCommandHandler; -import dev.loki.loparkour.util.ColorUtil; -import dev.loki.loparkour.util.Locations; -import dev.loki.loparkour.util.ParticleUtil; -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.BoundingBox; - -/** - * Handles LPSchematic wand clicks (left/right click on blocks). - */ -public class SchematicWandListener implements Listener { - - @EventHandler - public void onWandInteract(PlayerInteractEvent event) { - Player player = event.getPlayer(); - ItemStack item = player.getInventory().getItemInMainHand(); - - if (!player.hasPermission("LoParkour.admin")) return; - if (item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("LPSchematic Wand")) return; - if (event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) return; - - Location loc = event.getClickedBlock().getLocation(); - Location[] sel = SchematicCommandHandler.selections.get(player); - event.setCancelled(true); - - if (event.getAction() == Action.LEFT_CLICK_BLOCK) { - Location[] updated = new Location[]{loc, sel != null ? sel[1] : null}; - SchematicCommandHandler.selections.put(player, updated); - send(player, LoParkour.PREFIX + "Position 1 set to " + Locations.toString(loc, true)); - if (updated[1] != null) - ParticleUtil.box(BoundingBox.of(loc, updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2); - - } else if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { - Location[] updated = new Location[]{sel != null ? sel[0] : null, loc}; - SchematicCommandHandler.selections.put(player, updated); - send(player, LoParkour.PREFIX + "Position 2 set to " + Locations.toString(loc, true)); - if (updated[0] != null) - ParticleUtil.box(BoundingBox.of(updated[0], loc), player.getWorld(), Particle.END_ROD, player, 0.2); - } - } - - private void send(CommandSender sender, String msg) { - sender.sendMessage(ColorUtil.color(msg)); - } -} diff --git a/src/main/java/dev/loki/loparkour/listener/ParkourRestrictionListener.java b/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java similarity index 92% rename from src/main/java/dev/loki/loparkour/listener/ParkourRestrictionListener.java rename to src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java index 8b215c7..639c5e8 100644 --- a/src/main/java/dev/loki/loparkour/listener/ParkourRestrictionListener.java +++ b/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java @@ -1,13 +1,13 @@ -package dev.loki.loparkour.listener; +package dev.loki.loparkour.listener.gameplay; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.session.core.Session; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.EventHandler; diff --git a/src/main/java/dev/loki/loparkour/listener/PlayerConnectionListener.java b/src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java similarity index 90% rename from src/main/java/dev/loki/loparkour/listener/PlayerConnectionListener.java rename to src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java index ed5a5b0..fe083b5 100644 --- a/src/main/java/dev/loki/loparkour/listener/PlayerConnectionListener.java +++ b/src/main/java/dev/loki/loparkour/listener/player/PlayerConnectionListener.java @@ -1,10 +1,10 @@ -package dev.loki.loparkour.listener; +package dev.loki.loparkour.listener.player; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.world.World; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.world.core.World; import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java b/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java new file mode 100644 index 0000000..0a4389c --- /dev/null +++ b/src/main/java/dev/loki/loparkour/listener/schematic/SchematicWandListener.java @@ -0,0 +1,60 @@ +package dev.loki.loparkour.listener.schematic; + +import dev.loki.loparkour.command.schematic.SchematicCommandHandler; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.util.particle.ParticleUtil; +import dev.loki.loparkour.util.text.ColorUtil; +import dev.loki.loparkour.util.world.Locations; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.jetbrains.annotations.NotNull; + +/** + * Wand left/right click sets schematic selection corners. + */ +public class SchematicWandListener implements Listener { + + @EventHandler(ignoreCancelled = true) + public void onInteract(@NotNull PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND) { + return; + } + if (!SchematicCommandHandler.isWand(event.getItem())) { + return; + } + + Action action = event.getAction(); + if (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) { + return; + } + if (event.getClickedBlock() == null) { + return; + } + + event.setCancelled(true); + var player = event.getPlayer(); + Location loc = event.getClickedBlock().getLocation(); + int index = action == Action.LEFT_CLICK_BLOCK ? 0 : 1; + + SchematicCommandHandler.setPos(player, loc, index); + String label = index == 0 ? "1" : "2"; + player.sendMessage(ColorUtil.color( + Locales.getString(player, "schematic.pos_set").formatted(label, Locations.toString(loc, true)))); + + Location[] sel = SchematicCommandHandler.getSelection(player); + if (sel[0] != null && sel[1] != null) { + ParticleUtil.box( + org.bukkit.util.BoundingBox.of(sel[0], sel[1]), + player.getWorld(), + Particle.END_ROD, + player, + 0.2 + ); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/menu/community/CommunityMenu.java b/src/main/java/dev/loki/loparkour/menu/community/CommunityMenu.java index 544b95b..01e810e 100644 --- a/src/main/java/dev/loki/loparkour/menu/community/CommunityMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/community/CommunityMenu.java @@ -1,9 +1,9 @@ package dev.loki.loparkour.menu.community; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java b/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java index 024d9a1..5115391 100644 --- a/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java @@ -1,12 +1,12 @@ package dev.loki.loparkour.menu.community; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Mode; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.mode.base.Mode; import dev.lolib.gui.InventoryGUI; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -64,12 +64,12 @@ public class LeaderboardsMenu extends LPMenu { // Add navigation buttons if (currentPage > 0) { // Previous page button - gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1)); + gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1)); } if (currentPage < totalPages - 1) { // Next page button - gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1)); + gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1)); } gui.open(player); diff --git a/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java b/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java index 2cf9082..6b930b9 100644 --- a/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java @@ -1,12 +1,12 @@ package dev.loki.loparkour.menu.community; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.player.core.ParkourUser; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.OfflinePlayer; @@ -89,12 +89,12 @@ public class SingleLeaderboardMenu extends LPMenu { // Add navigation buttons if (currentPage > 0) { // Previous page button - gui = gui.setItem(0, backItem(), e -> open(player, mode, sort, currentPage - 1)); + gui = gui.setItem(0, backItem(player), e -> open(player, mode, sort, currentPage - 1)); } if (currentPage < totalPages - 1) { // Next page button - gui = gui.setItem(8, nextItem(), e -> open(player, mode, sort, currentPage + 1)); + gui = gui.setItem(8, nextItem(player), e -> open(player, mode, sort, currentPage + 1)); } gui.open(player); diff --git a/src/main/java/dev/loki/loparkour/menu/DynamicMenu.java b/src/main/java/dev/loki/loparkour/menu/core/DynamicMenu.java similarity index 90% rename from src/main/java/dev/loki/loparkour/menu/DynamicMenu.java rename to src/main/java/dev/loki/loparkour/menu/core/DynamicMenu.java index 4760aa4..6970e13 100644 --- a/src/main/java/dev/loki/loparkour/menu/DynamicMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/core/DynamicMenu.java @@ -1,6 +1,6 @@ -package dev.loki.loparkour.menu; +package dev.loki.loparkour.menu.core; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.player.core.ParkourUser; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,7 +29,7 @@ public abstract class DynamicMenu { */ // Stub: migrate to LoLib GUI when dynamic rows are wired up. /* - public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.MenuItem> item, Predicate shouldDisplay) { + public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.gui.MenuItem> item, Predicate shouldDisplay) { if (id < 0 || row < 0 || row > 4) { return; } @@ -75,6 +75,6 @@ public abstract class DynamicMenu { /** * Data class for registered items */ - protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.MenuItem> item, + protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.gui.MenuItem> item, Predicate predicate) {} } diff --git a/src/main/java/dev/loki/loparkour/menu/LPMenu.java b/src/main/java/dev/loki/loparkour/menu/core/LPMenu.java similarity index 76% rename from src/main/java/dev/loki/loparkour/menu/LPMenu.java rename to src/main/java/dev/loki/loparkour/menu/core/LPMenu.java index 7aebe49..9a4858c 100644 --- a/src/main/java/dev/loki/loparkour/menu/LPMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/core/LPMenu.java @@ -1,8 +1,8 @@ -package dev.loki.loparkour.menu; +package dev.loki.loparkour.menu.core; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.util.text.ColorUtil; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -25,7 +25,7 @@ public abstract class LPMenu { public abstract void open(@NotNull Player player); protected String locale(@NotNull Player player) { - var user = dev.loki.loparkour.player.ParkourUser.getUser(player); + var user = dev.loki.loparkour.player.core.ParkourUser.getUser(player); String loc = user != null ? user.locale : Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG); return (loc != null && !loc.isBlank()) ? loc : "en"; } @@ -40,24 +40,12 @@ public abstract class LPMenu { return item; } - protected ItemStack backItem() { - ItemStack item = new ItemStack(Material.RED_DYE); - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - meta.setDisplayName(ColorUtil.color("<#DE1F1F>Назад")); - item.setItemMeta(meta); - } - return item; + protected ItemStack backItem(@NotNull Player player) { + return localeItem(player, "other.back"); } - protected ItemStack nextItem() { - ItemStack item = new ItemStack(Material.LIME_DYE); - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - meta.setDisplayName(ColorUtil.color("<#0DCB07>Далее")); - item.setItemMeta(meta); - } - return item; + protected ItemStack nextItem(@NotNull Player player) { + return localeItem(player, "other.next"); } protected ItemStack closeButton(@NotNull Player player) { diff --git a/src/main/java/dev/loki/loparkour/menu/MainMenu.java b/src/main/java/dev/loki/loparkour/menu/core/MainMenu.java similarity index 90% rename from src/main/java/dev/loki/loparkour/menu/MainMenu.java rename to src/main/java/dev/loki/loparkour/menu/core/MainMenu.java index 7f6f601..e5e4d76 100644 --- a/src/main/java/dev/loki/loparkour/menu/MainMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/core/MainMenu.java @@ -1,8 +1,8 @@ -package dev.loki.loparkour.menu; +package dev.loki.loparkour.menu.core; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.player.core.ParkourUser; import dev.lolib.gui.InventoryGUI; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -59,7 +59,7 @@ public class MainMenu extends LPMenu { int count = visibleItems.size(); if (count == 0) { - player.sendMessage(Locales.getString(player, "other.no_menu_items")); + Locales.send(player, "other.no_menu_items"); return; } diff --git a/src/main/java/dev/loki/loparkour/menu/MenuStub.java b/src/main/java/dev/loki/loparkour/menu/core/MenuStub.java similarity index 79% rename from src/main/java/dev/loki/loparkour/menu/MenuStub.java rename to src/main/java/dev/loki/loparkour/menu/core/MenuStub.java index 0cf1a77..a61b8e3 100644 --- a/src/main/java/dev/loki/loparkour/menu/MenuStub.java +++ b/src/main/java/dev/loki/loparkour/menu/core/MenuStub.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.menu; +package dev.loki.loparkour.menu.core; /** * Placeholder during menu migration; real flows use {@link Menus} / LoLib GUI. diff --git a/src/main/java/dev/loki/loparkour/menu/Menus.java b/src/main/java/dev/loki/loparkour/menu/core/Menus.java similarity index 97% rename from src/main/java/dev/loki/loparkour/menu/Menus.java rename to src/main/java/dev/loki/loparkour/menu/core/Menus.java index c439227..795b8cd 100644 --- a/src/main/java/dev/loki/loparkour/menu/Menus.java +++ b/src/main/java/dev/loki/loparkour/menu/core/Menus.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.menu; +package dev.loki.loparkour.menu.core; import dev.loki.loparkour.menu.community.CommunityMenu; import dev.loki.loparkour.menu.community.LeaderboardsMenu; diff --git a/src/main/java/dev/loki/loparkour/menu/ParkourOption.java b/src/main/java/dev/loki/loparkour/menu/core/ParkourOption.java similarity index 95% rename from src/main/java/dev/loki/loparkour/menu/ParkourOption.java rename to src/main/java/dev/loki/loparkour/menu/core/ParkourOption.java index 9683dba..6b0999c 100644 --- a/src/main/java/dev/loki/loparkour/menu/ParkourOption.java +++ b/src/main/java/dev/loki/loparkour/menu/core/ParkourOption.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.menu; +package dev.loki.loparkour.menu.core; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Option; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.options.Option; import org.bukkit.permissions.Permissible; /** diff --git a/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java b/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java index f85e70d..30441a2 100644 --- a/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java @@ -1,12 +1,12 @@ package dev.loki.loparkour.menu.lobby; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.session.core.Session; +import dev.loki.loparkour.util.text.ColorUtil; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java b/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java index 111fc50..42c7071 100644 --- a/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java @@ -1,12 +1,12 @@ package dev.loki.loparkour.menu.lobby; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.util.text.ColorUtil; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -51,10 +51,18 @@ public class PlayerManagementMenu extends LPMenu { boolean muted = session.isMuted(other); ItemMeta displayMeta = skull.getItemMeta(); if (displayMeta != null) { - displayMeta.setDisplayName(ColorUtil.color("" + other.getName() + (muted ? " §c[muted]" : ""))); + String muteSuffix = muted + ? Locales.getString(locale, "lobby.player_management.head.muted_suffix") + : ""; + displayMeta.setDisplayName(ColorUtil.color( + Locales.getString(locale, "lobby.player_management.head.name") + .formatted(other.getName(), muteSuffix))); + String muteLoreKey = muted + ? "lobby.player_management.head.lore_unmute" + : "lobby.player_management.head.lore_mute"; displayMeta.setLore(List.of( - ColorUtil.color("§7Left-click §fto kick"), - ColorUtil.color("§7Right-click §f" + (muted ? "to unmute" : "to mute")) + ColorUtil.color(Locales.getString(locale, "lobby.player_management.head.lore_kick")), + ColorUtil.color(Locales.getString(locale, muteLoreKey)) )); skull.setItemMeta(displayMeta); } diff --git a/src/main/java/dev/loki/loparkour/menu/play/PlayMenu.java b/src/main/java/dev/loki/loparkour/menu/play/PlayMenu.java index dcebafd..5dc4bd2 100644 --- a/src/main/java/dev/loki/loparkour/menu/play/PlayMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/play/PlayMenu.java @@ -1,9 +1,9 @@ package dev.loki.loparkour.menu.play; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java b/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java index f79eee2..3f20895 100644 --- a/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java @@ -1,11 +1,11 @@ package dev.loki.loparkour.menu.play; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.player.core.ParkourUser; import dev.lolib.gui.InventoryGUI; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -81,12 +81,12 @@ public class SingleMenu extends LPMenu { // Add navigation buttons if (currentPage > 0) { // Previous page button - gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1)); + gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1)); } if (currentPage < totalPages - 1) { // Next page button - gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1)); + gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1)); } gui.open(player); diff --git a/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java b/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java index c456e2d..9b5f06b 100644 --- a/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java @@ -1,11 +1,11 @@ package dev.loki.loparkour.menu.play; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.world.Divider; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.session.core.Session; +import dev.loki.loparkour.world.core.Divider; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -68,12 +68,12 @@ public class SpectatorMenu extends LPMenu { // Add navigation buttons if (currentPage > 0) { // Previous page button - gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1)); + gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1)); } if (currentPage < totalPages - 1) { // Next page button - gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1)); + gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1)); } gui.open(player); diff --git a/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java b/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java index b69a60a..56876d1 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java @@ -1,10 +1,10 @@ package dev.loki.loparkour.menu.settings; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.util.text.ColorUtil; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java b/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java index 1567410..eb8160e 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java @@ -1,14 +1,14 @@ package dev.loki.loparkour.menu.settings; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.style.Style; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.style.core.Style; +import dev.loki.loparkour.util.text.ColorUtil; import dev.lolib.gui.InventoryGUI; import org.bukkit.Material; import org.bukkit.entity.Player; 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 3c0c1e2..9c01263 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java @@ -1,11 +1,11 @@ package dev.loki.loparkour.menu.settings; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.menu.LPMenu; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.LPMenu; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; import dev.lolib.gui.InventoryGUI; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/mode/ElytraGenerator.java b/src/main/java/dev/loki/loparkour/mode/ElytraGenerator.java deleted file mode 100644 index 4fed241..0000000 --- a/src/main/java/dev/loki/loparkour/mode/ElytraGenerator.java +++ /dev/null @@ -1,262 +0,0 @@ -package dev.loki.loparkour.mode; - -import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.mode.elytra.*; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.ColorUtil; -import org.bukkit.*; -import org.bukkit.entity.Firework; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.FireworkMeta; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Refactored Elytra mode generator using composition pattern. - * Coordinates ring generation, physics, and rendering components. - */ -public class ElytraGenerator extends ParkourGenerator implements Listener { - - private final ElytraConfig config; - private final ElytraRingGenerator ringGenerator; - private final ElytraPhysics physics; - private final ElytraRenderer renderer; - - private final List rings = new ArrayList<>(); - private final Map playerRingIndex = new HashMap<>(); - - private BukkitTask renderTask; - private BukkitTask physicsTask; - - public ElytraGenerator(@NotNull Session session) { - super(session); - - this.config = new ElytraConfig(); - this.ringGenerator = new ElytraRingGenerator(config); - this.physics = new ElytraPhysics(config); - this.renderer = new ElytraRenderer(); - - Bukkit.getPluginManager().registerEvents(this, LoParkour.getPlugin()); - } - - @Override - public void generateFirst(Location spawn, Location blockSpawn) { - // Generate initial rings starting from spawn position - Vector initialDirection = new Vector(1, 0, 0); // East direction - List initialRings = ringGenerator.generateRings(spawn, initialDirection, config.getRingLead()); - rings.addAll(initialRings); - - // Initialize player data and launch them - for (ParkourPlayer player : getPlayers()) { - playerRingIndex.put(player, 0); - giveElytraAndFireworks(player); - launchPlayer(player, spawn); - } - - startTasks(); - } - - private void launchPlayer(@NotNull ParkourPlayer player, @NotNull Location spawn) { - Player p = player.player; - - // Teleport player above spawn (10 blocks up) - Location launchPos = spawn.clone().add(0, 10, 0); - p.teleport(launchPos); - - // Enable gliding - p.setGliding(true); - - // Give initial forward velocity - Vector velocity = new Vector(1, 0, 0).normalize().multiply(1.5); - p.setVelocity(velocity); - } - - @Override - public void tick() { - super.tick(); - checkRingsAndPhysics(); - generateMoreRings(); - cleanupOldRings(); - } - - @Override - public void reset(boolean regenerate) { - cleanup(); // Call cleanup BEFORE super.reset to cancel tasks first - super.reset(regenerate); - } - - private void checkRingsAndPhysics() { - for (ParkourPlayer player : getPlayers()) { - int currentIndex = playerRingIndex.getOrDefault(player, 0); - - // Check ring crossings - int newIndex = physics.checkRingCrossings(player, rings, currentIndex); - if (newIndex > currentIndex) { - onRingPassed(player, rings.get(newIndex - 1)); - playerRingIndex.put(player, newIndex); - } - - // Check for falls - ElytraPhysics.FallCheckResult fallResult = physics.checkFall(player, rings, currentIndex); - if (fallResult.shouldFall()) { - triggerFall(player, fallResult.getReason()); - } - - updateActionBar(player); - } - } - - private void generateMoreRings() { - if (rings.isEmpty()) { - return; - } - if (rings.size() < config.getRingLead() * 2) { - ElytraRing lastRing = rings.get(rings.size() - 1); - Vector direction = calculateNextDirection(lastRing); - - List newRings = ringGenerator.generateRings( - lastRing.getCenter(), direction, config.getRingLead() - ); - rings.addAll(newRings); - } - } - - private void onRingPassed(@NotNull ParkourPlayer player, @NotNull ElytraRing ring) { - boolean centered = physics.isPlayerCentered(player.player.getLocation(), ring); - int points = centered ? 10 : 5; - - player.session.generator.state.score += points; - - // Play sound and show message - player.player.playSound(player.player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f); - - String message = centered ? "Perfect! +" + points : "Good! +" + points; - player.player.sendMessage(ColorUtil.color("&a" + message)); - } - - private void triggerFall(@NotNull ParkourPlayer player, @NotNull String reason) { - fall(); - player.player.sendMessage(ColorUtil.color("&cFell: " + reason)); - } - - @SuppressWarnings("deprecation") // Spigot Chat API: action bar via TextComponent - private void updateActionBar(@NotNull ParkourPlayer player) { - int currentIndex = playerRingIndex.getOrDefault(player, 0); - int totalRings = rings.size(); - int score = state.score; - - String message = String.format("&eRing: %d/%d &7| &aScore: %d", - currentIndex + 1, totalRings, score); - - player.player.spigot().sendMessage( - net.md_5.bungee.api.ChatMessageType.ACTION_BAR, - net.md_5.bungee.api.chat.TextComponent.fromLegacyText(ColorUtil.color(message)) - ); - } - - @EventHandler(priority = EventPriority.HIGH) - public void onPlayerInteract(@NotNull PlayerInteractEvent event) { - Player player = event.getPlayer(); - ParkourPlayer pp = findParkourPlayer(player); - if (pp == null) return; - - ItemStack item = event.getItem(); - if (item != null && item.getType() == Material.FIREWORK_ROCKET) { - if (physics.applyFireworkBoost(pp)) { - spawnBoostFirework(player); - item.setAmount(item.getAmount() - 1); - } - event.setCancelled(true); - } - } - - private void startTasks() { - renderTask = Bukkit.getScheduler().runTaskTimer(LoParkour.getPlugin(), () -> { - for (ParkourPlayer player : getPlayers()) { - int currentIndex = playerRingIndex.getOrDefault(player, 0); - renderer.renderRings(rings, currentIndex); - } - }, 0L, 2L); // Every 2 ticks - } - - private void cleanup() { - // Cancel and nullify tasks to prevent memory leaks - if (renderTask != null) { - renderTask.cancel(); - renderTask = null; - } - if (physicsTask != null) { - physicsTask.cancel(); - physicsTask = null; - } - - // Unregister event listener to prevent memory leaks - HandlerList.unregisterAll(this); - - // Clear all data structures - rings.forEach(ElytraRing::remove); - rings.clear(); - playerRingIndex.clear(); - - for (ParkourPlayer player : getPlayers()) { - physics.cleanup(player.player.getUniqueId()); - } - } - - private void giveElytraAndFireworks(@NotNull ParkourPlayer player) { - Player p = player.player; - p.getInventory().setChestplate(new ItemStack(Material.ELYTRA)); - p.getInventory().addItem(makeFirework(config.getStartingFireworks())); - } - - private void spawnBoostFirework(@NotNull Player player) { - Firework firework = player.getWorld().spawn(player.getLocation(), Firework.class); - FireworkMeta meta = firework.getFireworkMeta(); - meta.setPower(1); - firework.setFireworkMeta(meta); - firework.detonate(); - } - - @NotNull - private Vector calculateNextDirection(@NotNull ElytraRing lastRing) { - // Simple forward direction for now - return new Vector(1, 0, 0); - } - - private void cleanupOldRings() { - // Remove rings that are far behind all players - // Implementation depends on specific requirements - } - - @NotNull - private ItemStack makeFirework(int amount) { - ItemStack firework = new ItemStack(Material.FIREWORK_ROCKET, amount); - FireworkMeta meta = (FireworkMeta) firework.getItemMeta(); - if (meta != null) { - meta.setPower(1); - firework.setItemMeta(meta); - } - return firework; - } - - private ParkourPlayer findParkourPlayer(@NotNull Player player) { - return getPlayers().stream() - .filter(pp -> pp.player.equals(player)) - .findFirst() - .orElse(null); - } -} \ No newline at end of file diff --git a/src/main/java/dev/loki/loparkour/mode/Mode.java b/src/main/java/dev/loki/loparkour/mode/base/Mode.java similarity index 84% rename from src/main/java/dev/loki/loparkour/mode/Mode.java rename to src/main/java/dev/loki/loparkour/mode/base/Mode.java index 09ca8d6..b609220 100644 --- a/src/main/java/dev/loki/loparkour/mode/Mode.java +++ b/src/main/java/dev/loki/loparkour/mode/base/Mode.java @@ -1,6 +1,6 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.base; -import dev.loki.loparkour.leaderboard.Leaderboard; +import dev.loki.loparkour.leaderboard.core.Leaderboard; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -23,7 +23,7 @@ public interface Mode { * @param locale The locale of the menu, used to adjust the name. * @return The item used in menus to show this mode. If this item is null, the mode won't be displayed. */ - @Nullable dev.loki.loparkour.util.Item getItem(String locale); + @Nullable dev.loki.loparkour.util.item.Item getItem(String locale); /** * @return The {@link Leaderboard} that belongs to this mode diff --git a/src/main/java/dev/loki/loparkour/mode/base/ModeMessages.java b/src/main/java/dev/loki/loparkour/mode/base/ModeMessages.java new file mode 100644 index 0000000..b517a82 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/base/ModeMessages.java @@ -0,0 +1,26 @@ +package dev.loki.loparkour.mode.base; + +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * Shared mode messages from locale files. + */ +public final class ModeMessages { + + private ModeMessages() { + } + + /** + * @return true if joining is allowed; otherwise sends {@code other.joining_disabled} and returns false + */ + public static boolean checkJoiningEnabled(@NotNull Player player) { + if (Config.CONFIG.getBoolean("joining")) { + return true; + } + Locales.send(player, "other.joining_disabled"); + return false; + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/Modes.java b/src/main/java/dev/loki/loparkour/mode/base/Modes.java similarity index 87% rename from src/main/java/dev/loki/loparkour/mode/Modes.java rename to src/main/java/dev/loki/loparkour/mode/base/Modes.java index 43563f5..7890482 100644 --- a/src/main/java/dev/loki/loparkour/mode/Modes.java +++ b/src/main/java/dev/loki/loparkour/mode/base/Modes.java @@ -1,6 +1,7 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.base; -import dev.loki.loparkour.api.Registry; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.mode.impl.*; public class Modes { diff --git a/src/main/java/dev/loki/loparkour/mode/MultiMode.java b/src/main/java/dev/loki/loparkour/mode/base/MultiMode.java similarity index 85% rename from src/main/java/dev/loki/loparkour/mode/MultiMode.java rename to src/main/java/dev/loki/loparkour/mode/base/MultiMode.java index 6dfaf41..1e9dc00 100644 --- a/src/main/java/dev/loki/loparkour/mode/MultiMode.java +++ b/src/main/java/dev/loki/loparkour/mode/base/MultiMode.java @@ -1,6 +1,6 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.base; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.session.core.Session; import org.bukkit.entity.Player; /** diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java index fa34731..7304df4 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.mode.elytra; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; /** * Configuration for Elytra mode loaded from config.yml diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraFireworkListener.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraFireworkListener.java new file mode 100644 index 0000000..165eda4 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraFireworkListener.java @@ -0,0 +1,49 @@ +package dev.loki.loparkour.mode.elytra; + +import dev.loki.loparkour.player.core.ParkourPlayer; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import java.util.function.Function; + +/** + * Handles firework rocket use while gliding in elytra parkour. + */ +public final class ElytraFireworkListener implements Listener { + + @FunctionalInterface + public interface FireworkUseHandler { + /** @return true if the rocket was consumed */ + boolean onFirework(@NotNull ParkourPlayer parkourPlayer, @NotNull Player player); + } + + private final Function playerResolver; + private final FireworkUseHandler handler; + + public ElytraFireworkListener( + @NotNull Function playerResolver, + @NotNull FireworkUseHandler handler) { + this.playerResolver = playerResolver; + this.handler = handler; + } + + @EventHandler(priority = EventPriority.HIGH) + public void onPlayerInteract(@NotNull PlayerInteractEvent event) { + Player player = event.getPlayer(); + ParkourPlayer pp = playerResolver.apply(player); + if (pp == null) { + return; + } + + ItemStack item = event.getItem(); + if (item != null && item.getType() == Material.FIREWORK_ROCKET && handler.onFirework(pp, player)) { + item.setAmount(item.getAmount() - 1); + event.setCancelled(true); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraGenerator.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraGenerator.java new file mode 100644 index 0000000..6a23ca3 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraGenerator.java @@ -0,0 +1,121 @@ +package dev.loki.loparkour.mode.elytra; + +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.mode.elytra.*; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.session.core.Session; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Elytra mode generator — coordinates ring loop, rendering, and player kit. + */ +public class ElytraGenerator extends ParkourGenerator { + + private final ElytraConfig config; + private final ElytraRingGenerator ringGenerator; + private final ElytraPhysics physics; + private final ElytraRenderer renderer; + private final ElytraRingLoop ringLoop; + private final ElytraFireworkListener fireworkListener; + + private final List rings = new ArrayList<>(); + private final Map playerRingIndex = new HashMap<>(); + + private BukkitTask renderTask; + + public ElytraGenerator(@NotNull Session session) { + super(session); + + this.config = new ElytraConfig(); + this.ringGenerator = new ElytraRingGenerator(config); + this.physics = new ElytraPhysics(config); + this.renderer = new ElytraRenderer(); + this.ringLoop = new ElytraRingLoop( + config, ringGenerator, physics, rings, playerRingIndex, + (player, ring) -> ElytraScoring.onRingPassed(this, player, ring, physics), + p -> fall()); + this.fireworkListener = new ElytraFireworkListener( + this::findParkourPlayer, + (pp, p) -> { + if (physics.applyFireworkBoost(pp)) { + ElytraPlayerKit.spawnBoostFirework(p); + return true; + } + return false; + }); + + Bukkit.getPluginManager().registerEvents(fireworkListener, LoParkour.getPlugin()); + } + + @Override + public void generateFirst(Location spawn, Location blockSpawn) { + ElytraRingLoop.spawnInitialRings(spawn, config, ringGenerator, rings); + + for (ParkourPlayer player : getPlayers()) { + playerRingIndex.put(player, 0); + ElytraPlayerKit.giveElytraAndFireworks(player, config); + ElytraPlayerKit.launch(player, spawn); + } + + startRenderTask(); + } + + @Override + public void tick() { + super.tick(); + ringLoop.tickPlayers(getPlayers(), this); + ringLoop.ensureRingBuffer(); + } + + @Override + public void reset(boolean regenerate) { + cleanup(); + super.reset(regenerate); + } + + private void startRenderTask() { + renderTask = Bukkit.getScheduler().runTaskTimer(LoParkour.getPlugin(), () -> { + for (ParkourPlayer player : getPlayers()) { + int currentIndex = playerRingIndex.getOrDefault(player, 0); + renderer.renderRings(rings, currentIndex); + } + }, 0L, 2L); + } + + private void cleanup() { + if (renderTask != null) { + renderTask.cancel(); + renderTask = null; + } + + HandlerList.unregisterAll(fireworkListener); + + rings.forEach(ElytraRing::remove); + rings.clear(); + playerRingIndex.clear(); + + for (ParkourPlayer player : getPlayers()) { + physics.cleanup(player.player.getUniqueId()); + } + } + + @Nullable + private ParkourPlayer findParkourPlayer(@NotNull Player player) { + return getPlayers().stream() + .filter(pp -> pp.player.equals(player)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraHud.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraHud.java new file mode 100644 index 0000000..97ab270 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraHud.java @@ -0,0 +1,29 @@ +package dev.loki.loparkour.mode.elytra; + +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.util.text.ColorUtil; +import org.jetbrains.annotations.NotNull; + +/** + * Elytra mode action bar display. + */ +public final class ElytraHud { + + private ElytraHud() { + } + + @SuppressWarnings("deprecation") // Spigot action bar via TextComponent + public static void sendActionBar( + @NotNull ParkourPlayer player, + int ringIndex, + int totalRings, + int score) { + String message = Locales.getString(player.locale, "modes.elytra.action_bar") + .formatted(ringIndex + 1, totalRings, score); + + player.player.spigot().sendMessage( + net.md_5.bungee.api.ChatMessageType.ACTION_BAR, + net.md_5.bungee.api.chat.TextComponent.fromLegacyText(ColorUtil.color(message))); + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java index 36e5b8b..7529e3a 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java @@ -1,6 +1,6 @@ package dev.loki.loparkour.mode.elytra; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.util.Vector; diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPlayerKit.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPlayerKit.java new file mode 100644 index 0000000..c5f45f4 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPlayerKit.java @@ -0,0 +1,53 @@ +package dev.loki.loparkour.mode.elytra; + +import dev.loki.loparkour.player.core.ParkourPlayer; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +/** + * Elytra loadout and launch helpers. + */ +public final class ElytraPlayerKit { + + private ElytraPlayerKit() { + } + + public static void giveElytraAndFireworks(@NotNull ParkourPlayer player, @NotNull ElytraConfig config) { + Player p = player.player; + p.getInventory().setChestplate(new ItemStack(Material.ELYTRA)); + p.getInventory().addItem(makeFirework(config.getStartingFireworks())); + } + + public static void launch(@NotNull ParkourPlayer player, @NotNull Location spawn) { + Player p = player.player; + Location launchPos = spawn.clone().add(0, 10, 0); + p.teleport(launchPos); + p.setGliding(true); + p.setVelocity(new Vector(1, 0, 0).normalize().multiply(1.5)); + } + + public static void spawnBoostFirework(@NotNull Player player) { + Firework firework = player.getWorld().spawn(player.getLocation(), Firework.class); + FireworkMeta meta = firework.getFireworkMeta(); + meta.setPower(1); + firework.setFireworkMeta(meta); + firework.detonate(); + } + + @NotNull + public static ItemStack makeFirework(int amount) { + ItemStack firework = new ItemStack(Material.FIREWORK_ROCKET, amount); + FireworkMeta meta = (FireworkMeta) firework.getItemMeta(); + if (meta != null) { + meta.setPower(1); + firework.setItemMeta(meta); + } + return firework; + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingLoop.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingLoop.java new file mode 100644 index 0000000..6b11e73 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingLoop.java @@ -0,0 +1,87 @@ +package dev.loki.loparkour.mode.elytra; + +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; +import org.bukkit.Location; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Per-tick ring crossing checks and ahead-of-player ring generation. + */ +public final class ElytraRingLoop { + + private final ElytraConfig config; + private final ElytraRingGenerator ringGenerator; + private final ElytraPhysics physics; + private final List rings; + private final Map playerRingIndex; + private final Consumer onFall; + private final RingPassHandler onRingPassed; + + @FunctionalInterface + public interface RingPassHandler { + void onPassed(@NotNull ParkourPlayer player, @NotNull ElytraRing ring); + } + + public ElytraRingLoop( + @NotNull ElytraConfig config, + @NotNull ElytraRingGenerator ringGenerator, + @NotNull ElytraPhysics physics, + @NotNull List rings, + @NotNull Map playerRingIndex, + @NotNull RingPassHandler onRingPassed, + @NotNull Consumer onFall) { + this.config = config; + this.ringGenerator = ringGenerator; + this.physics = physics; + this.rings = rings; + this.playerRingIndex = playerRingIndex; + this.onRingPassed = onRingPassed; + this.onFall = onFall; + } + + public void tickPlayers(@NotNull Iterable players, @NotNull ParkourGenerator generator) { + for (ParkourPlayer player : players) { + int currentIndex = playerRingIndex.getOrDefault(player, 0); + + int newIndex = physics.checkRingCrossings(player, rings, currentIndex); + if (newIndex > currentIndex) { + onRingPassed.onPassed(player, rings.get(newIndex - 1)); + playerRingIndex.put(player, newIndex); + } + + ElytraPhysics.FallCheckResult fallResult = physics.checkFall(player, rings, currentIndex); + if (fallResult.shouldFall()) { + ElytraScoring.notifyFall(player, fallResult.getReason()); + onFall.accept(player); + } + + int displayIndex = playerRingIndex.getOrDefault(player, 0); + ElytraHud.sendActionBar(player, displayIndex, rings.size(), generator.state.score); + } + } + + public void ensureRingBuffer() { + if (rings.isEmpty()) { + return; + } + if (rings.size() < config.getRingLead() * 2) { + ElytraRing lastRing = rings.get(rings.size() - 1); + Vector direction = new Vector(1, 0, 0); + rings.addAll(ringGenerator.generateRings(lastRing.getCenter(), direction, config.getRingLead())); + } + } + + public static void spawnInitialRings( + @NotNull Location spawn, + @NotNull ElytraConfig config, + @NotNull ElytraRingGenerator ringGenerator, + @NotNull List rings) { + rings.addAll(ringGenerator.generateRings(spawn, new Vector(1, 0, 0), config.getRingLead())); + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraScoring.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraScoring.java new file mode 100644 index 0000000..1768247 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraScoring.java @@ -0,0 +1,35 @@ +package dev.loki.loparkour.mode.elytra; + +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; +import org.bukkit.Sound; +import org.jetbrains.annotations.NotNull; + +/** + * Ring pass feedback and fall messages for elytra mode. + */ +public final class ElytraScoring { + + private ElytraScoring() { + } + + public static void onRingPassed( + @NotNull ParkourGenerator generator, + @NotNull ParkourPlayer player, + @NotNull ElytraRing ring, + @NotNull ElytraPhysics physics) { + boolean centered = physics.isPlayerCentered(player.player.getLocation(), ring); + int points = centered ? 10 : 5; + + generator.state.score += points; + player.player.playSound(player.player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f); + + String key = centered ? "modes.elytra.ring_perfect" : "modes.elytra.ring_good"; + Locales.send(player.player, key, points); + } + + public static void notifyFall(@NotNull ParkourPlayer player, @NotNull String reason) { + Locales.send(player.player, "modes.elytra.fell", reason); + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/CoopMode.java b/src/main/java/dev/loki/loparkour/mode/impl/CoopMode.java similarity index 84% rename from src/main/java/dev/loki/loparkour/mode/CoopMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/CoopMode.java index 7312f8f..678a3b6 100644 --- a/src/main/java/dev/loki/loparkour/mode/CoopMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/CoopMode.java @@ -1,12 +1,18 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.base.MultiMode; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.util.text.ColorUtil; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.session.core.Session; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -53,7 +59,7 @@ public class CoopMode implements MultiMode { */ @Override @Nullable - public dev.loki.loparkour.util.Item getItem(String locale) { + public dev.loki.loparkour.util.item.Item getItem(String locale) { return null; } @@ -70,8 +76,7 @@ public class CoopMode implements MultiMode { */ @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("§cJoining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } // Opening lobby menu lets player invite others — actual session created in join() @@ -84,8 +89,7 @@ public class CoopMode implements MultiMode { */ @Override public void join(Player joiningPlayer, Session session) { - if (!Config.CONFIG.getBoolean("joining")) { - joiningPlayer.sendMessage("§cJoining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(joiningPlayer)) { return; } @@ -146,8 +150,8 @@ public class CoopMode implements MultiMode { if (state.score % 50 == 0) { getPlayers().forEach(pp -> { pp.player.sendTitle( - "§6Team: §e" + state.score, - "§aKeep it up!", + ColorUtil.color(Locales.getString(pp.locale, "modes.coop.milestone_title").formatted(state.score)), + ColorUtil.color(Locales.getString(pp.locale, "modes.coop.milestone_subtitle")), 5, 40, 10); pp.player.playSound(pp.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f); diff --git a/src/main/java/dev/loki/loparkour/mode/DefaultMode.java b/src/main/java/dev/loki/loparkour/mode/impl/DefaultMode.java similarity index 63% rename from src/main/java/dev/loki/loparkour/mode/DefaultMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/DefaultMode.java index 9965860..59df06c 100644 --- a/src/main/java/dev/loki/loparkour/mode/DefaultMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/DefaultMode.java @@ -1,11 +1,13 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; -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 dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.util.item.Item; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.session.core.Session; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -26,7 +28,7 @@ public class DefaultMode implements Mode { @Override @Nullable - public dev.loki.loparkour.util.Item getItem(String locale) { + public dev.loki.loparkour.util.item.Item getItem(String locale) { return Locales.getItem(locale, "play.single.default"); } @@ -38,8 +40,7 @@ public class DefaultMode implements Mode { @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("Joining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } diff --git a/src/main/java/dev/loki/loparkour/mode/ElytraMode.java b/src/main/java/dev/loki/loparkour/mode/impl/ElytraMode.java similarity index 67% rename from src/main/java/dev/loki/loparkour/mode/ElytraMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/ElytraMode.java index 597829a..a84dcea 100644 --- a/src/main/java/dev/loki/loparkour/mode/ElytraMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/ElytraMode.java @@ -1,11 +1,14 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.elytra.ElytraGenerator; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.Item; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +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; @@ -39,8 +42,7 @@ public class ElytraMode implements Mode { @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("§cJoining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } diff --git a/src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java b/src/main/java/dev/loki/loparkour/mode/impl/GravityShiftMode.java similarity index 89% rename from src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/GravityShiftMode.java index 3fd8e49..c066fd1 100644 --- a/src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/GravityShiftMode.java @@ -1,12 +1,15 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; -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 dev.loki.loparkour.util.Item; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +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.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; @@ -42,8 +45,7 @@ public class GravityShiftMode implements Mode { @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("Joining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } diff --git a/src/main/java/dev/loki/loparkour/mode/HardcoreMode.java b/src/main/java/dev/loki/loparkour/mode/impl/HardcoreMode.java similarity index 76% rename from src/main/java/dev/loki/loparkour/mode/HardcoreMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/HardcoreMode.java index 51d51b6..fcfc40f 100644 --- a/src/main/java/dev/loki/loparkour/mode/HardcoreMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/HardcoreMode.java @@ -1,12 +1,15 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; -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 dev.loki.loparkour.util.Item; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +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; @@ -38,8 +41,7 @@ public class HardcoreMode implements Mode { @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("Joining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } diff --git a/src/main/java/dev/loki/loparkour/mode/RaceMode.java b/src/main/java/dev/loki/loparkour/mode/impl/RaceMode.java similarity index 71% rename from src/main/java/dev/loki/loparkour/mode/RaceMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/RaceMode.java index da540c1..b03d89a 100644 --- a/src/main/java/dev/loki/loparkour/mode/RaceMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/RaceMode.java @@ -1,12 +1,16 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; import dev.loki.loparkour.LoParkour; -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 dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.util.text.ColorUtil; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.session.core.Session; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -43,7 +47,7 @@ public class RaceMode implements Mode { @Override @Nullable - public dev.loki.loparkour.util.Item getItem(String locale) { + public dev.loki.loparkour.util.item.Item getItem(String locale) { return Locales.getItem(locale, "modes.race"); } @@ -55,8 +59,7 @@ public class RaceMode implements Mode { @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("§cJoining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } @@ -100,16 +103,21 @@ public class RaceMode implements Mode { @SuppressWarnings("deprecation") // Spigot Chat API: action bar via TextComponent private void updateProgressBar() { int remaining = Math.max(0, targetScore - state.score); - int filled = Math.min(20, state.score * 20 / targetScore); - String bar = "§a" + "█".repeat(filled) + "§7" + "█".repeat(20 - filled); + int filledCount = Math.min(20, state.score * 20 / Math.max(targetScore, 1)); + String filled = ColorUtil.color("7FF55") + "█".repeat(filledCount); + String empty = ColorUtil.color("&#A0A0A0") + "█".repeat(20 - filledCount); - getPlayers().forEach(pp -> + getPlayers().forEach(pp -> { + String suffix = remaining > 0 + ? Locales.getString(pp.locale, "modes.race.action_bar_left").formatted(remaining) + : Locales.getString(pp.locale, "modes.race.action_bar_done"); + String message = ColorUtil.color( + Locales.getString(pp.locale, "modes.race.action_bar") + .formatted(filled + empty, state.score, targetScore, suffix)); pp.player.spigot().sendMessage( - net.md_5.bungee.api.ChatMessageType.ACTION_BAR, - net.md_5.bungee.api.chat.TextComponent.fromLegacyText( - bar + " §e" + state.score + " §7/ §a" + targetScore - + (remaining > 0 ? " §7(§c" + remaining + " §7left)" : " §6§lDONE!") - ))); + net.md_5.bungee.api.ChatMessageType.ACTION_BAR, + net.md_5.bungee.api.chat.TextComponent.fromLegacyText(message)); + }); } private void finishRace() { @@ -118,8 +126,8 @@ public class RaceMode implements Mode { getPlayers().forEach(pp -> { pp.player.sendTitle( - "§6§lRACE COMPLETE!", - "§aFinished in §f" + time, + ColorUtil.color(Locales.getString(pp.locale, "modes.race.title")), + ColorUtil.color(Locales.getString(pp.locale, "modes.race.subtitle").formatted(time)), 10, 80, 20); pp.player.playSound(pp.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); diff --git a/src/main/java/dev/loki/loparkour/mode/SpectatorMode.java b/src/main/java/dev/loki/loparkour/mode/impl/SpectatorMode.java similarity index 65% rename from src/main/java/dev/loki/loparkour/mode/SpectatorMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/SpectatorMode.java index d35a1c6..2361486 100644 --- a/src/main/java/dev/loki/loparkour/mode/SpectatorMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/SpectatorMode.java @@ -1,15 +1,16 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.menu.Menus; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.player.ParkourSpectator; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.menu.core.Menus; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.session.core.Session; -import dev.loki.loparkour.util.ColorUtil; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,7 +24,7 @@ public class SpectatorMode implements Mode { @Override @Nullable - public dev.loki.loparkour.util.Item getItem(String locale) { + public dev.loki.loparkour.util.item.Item getItem(String locale) { return null; } @@ -39,8 +40,7 @@ public class SpectatorMode implements Mode { } public void create(Player player, Session session) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage(ColorUtil.color("Joining is currently disabled.")); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } if (!ParkourOption.SPECTATOR.mayPerform(player)) { diff --git a/src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java b/src/main/java/dev/loki/loparkour/mode/impl/SpeedrunMode.java similarity index 90% rename from src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java rename to src/main/java/dev/loki/loparkour/mode/impl/SpeedrunMode.java index e6d49c9..befe19a 100644 --- a/src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/SpeedrunMode.java @@ -1,15 +1,18 @@ -package dev.loki.loparkour.mode; +package dev.loki.loparkour.mode.impl; +import dev.loki.loparkour.mode.base.ModeMessages; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; -import dev.loki.loparkour.util.Item; +import dev.loki.loparkour.util.item.Item; import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.ScheduledTask; import dev.loki.loparkour.LoParkour; -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 dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.session.core.Session; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.Sound; @@ -59,8 +62,7 @@ public class SpeedrunMode implements Mode { @Override public void create(Player player) { - if (!Config.CONFIG.getBoolean("joining")) { - player.sendMessage("Joining is currently disabled."); + if (!ModeMessages.checkJoiningEnabled(player)) { return; } diff --git a/src/main/java/dev/loki/loparkour/player/ParkourPlayer.java b/src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java similarity index 66% rename from src/main/java/dev/loki/loparkour/player/ParkourPlayer.java rename to src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java index b60dc15..f1071d3 100644 --- a/src/main/java/dev/loki/loparkour/player/ParkourPlayer.java +++ b/src/main/java/dev/loki/loparkour/player/core/ParkourPlayer.java @@ -1,18 +1,20 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.core; import com.google.gson.annotations.Expose; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.generator.Profile; -import dev.loki.loparkour.menu.ParkourOption; -import dev.loki.loparkour.mode.MultiMode; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.generator.core.model.Profile; +import dev.loki.loparkour.menu.core.ParkourOption; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.mode.base.MultiMode; import dev.loki.loparkour.player.data.PreviousData; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.player.service.ParkourHotbar; +import dev.loki.loparkour.player.service.PlayerSettingsManager; +import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.storage.Storage; -import dev.loki.loparkour.world.Divider; +import dev.loki.loparkour.world.core.Divider; import dev.lolib.scheduler.Scheduler; import org.bukkit.GameMode; import org.bukkit.Location; @@ -152,8 +154,8 @@ public class ParkourPlayer extends ParkourUser { .set("selectedTime", selectedTime.toString()) .set("style", style); - // Use profileManager instead of calling overrideProfile directly on generator generator.profileManager.overrideProfile(); + generator.profileManager.calculateChances(); } /** @@ -177,28 +179,7 @@ public class ParkourPlayer extends ParkourUser { player.setGameMode(GameMode.ADVENTURE); if (Config.CONFIG.getBoolean("options.inventory-handling")) { - Scheduler.get(LoParkour.getPlugin()).runLater(() -> { - player.getInventory().clear(); - - // Load hotbar slots from config - // Don't show Play button when already in parkour - if (ParkourOption.COMMUNITY.mayPerform(player)) { - int slot = Config.CONFIG.getInt("options.hotbar-slots.community"); - player.getInventory().setItem(slot, Locales.getItem(locale, "community.item").build()); - } - if (ParkourOption.SETTINGS.mayPerform(player)) { - int slot = Config.CONFIG.getInt("options.hotbar-slots.settings"); - 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); + Scheduler.get(LoParkour.getPlugin()).runLater(() -> ParkourHotbar.apply(this), 5); } else { sendTranslated("other.customize"); } @@ -208,27 +189,7 @@ public class ParkourPlayer extends ParkourUser { * Updates hotbar items with current language */ public void updateHotbar() { - if (!Config.CONFIG.getBoolean("options.inventory-handling")) return; - - player.getInventory().clear(); - - // Don't show Play button when already in parkour - if (ParkourOption.COMMUNITY.mayPerform(player)) { - int slot = Config.CONFIG.getInt("options.hotbar-slots.community"); - player.getInventory().setItem(slot, Locales.getItem(locale, "community.item").build()); - } - if (ParkourOption.SETTINGS.mayPerform(player)) { - int slot = Config.CONFIG.getInt("options.hotbar-slots.settings"); - 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()); - } + ParkourHotbar.apply(this); } public record OptionContainer(ParkourOption option, BiConsumer consumer) { diff --git a/src/main/java/dev/loki/loparkour/player/ParkourUser.java b/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java similarity index 86% rename from src/main/java/dev/loki/loparkour/player/ParkourUser.java rename to src/main/java/dev/loki/loparkour/player/core/ParkourUser.java index d1d725e..ed22da4 100644 --- a/src/main/java/dev/loki/loparkour/player/ParkourUser.java +++ b/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java @@ -1,13 +1,16 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.core; +import dev.loki.loparkour.player.service.UserRegistry; +import dev.loki.loparkour.player.service.ScoreboardManager; +import dev.loki.loparkour.player.core.ParkourPlayer; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.hook.FloodgateHook; -import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.hook.floodgate.FloodgateHook; +import dev.loki.loparkour.menu.core.ParkourOption; import dev.loki.loparkour.player.data.PreviousData; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.session.core.Session; +import dev.loki.loparkour.util.text.ColorUtil; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -86,7 +89,7 @@ public abstract class ParkourUser { } } - protected abstract void unregister(); + public abstract void unregister(); public void teleport(@NotNull Location to) { player.teleport(to); diff --git a/src/main/java/dev/loki/loparkour/player/data/InventoryData.java b/src/main/java/dev/loki/loparkour/player/data/InventoryData.java index d14462c..e694ec0 100644 --- a/src/main/java/dev/loki/loparkour/player/data/InventoryData.java +++ b/src/main/java/dev/loki/loparkour/player/data/InventoryData.java @@ -1,7 +1,7 @@ package dev.loki.loparkour.player.data; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; import dev.lolib.scheduler.Scheduler; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/dev/loki/loparkour/player/data/PreviousData.java b/src/main/java/dev/loki/loparkour/player/data/PreviousData.java index ed925e4..8c87c94 100644 --- a/src/main/java/dev/loki/loparkour/player/data/PreviousData.java +++ b/src/main/java/dev/loki/loparkour/player/data/PreviousData.java @@ -1,9 +1,9 @@ package dev.loki.loparkour.player.data; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.reward.Reward; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.reward.core.Reward; import io.papermc.lib.PaperLib; import org.bukkit.GameMode; import org.bukkit.Location; diff --git a/src/main/java/dev/loki/loparkour/player/BungeeUtil.java b/src/main/java/dev/loki/loparkour/player/service/BungeeUtil.java similarity index 82% rename from src/main/java/dev/loki/loparkour/player/BungeeUtil.java rename to src/main/java/dev/loki/loparkour/player/service/BungeeUtil.java index f553945..aad1d0a 100644 --- a/src/main/java/dev/loki/loparkour/player/BungeeUtil.java +++ b/src/main/java/dev/loki/loparkour/player/service/BungeeUtil.java @@ -1,8 +1,9 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.service; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.config.locale.Locales; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.ChannelNotRegisteredException; @@ -23,7 +24,7 @@ class BungeeUtil { } catch (ChannelNotRegisteredException ex) { LoParkour.getPlugin().getLogger().severe( "Error while trying to send %s to server %s. - %s".formatted(player.getName(), server, ex.getMessage())); - player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server)); + player.kickPlayer(Locales.getString(player, "other.bungee_kick").formatted(server)); } } } diff --git a/src/main/java/dev/loki/loparkour/player/service/ParkourHotbar.java b/src/main/java/dev/loki/loparkour/player/service/ParkourHotbar.java new file mode 100644 index 0000000..bf1715a --- /dev/null +++ b/src/main/java/dev/loki/loparkour/player/service/ParkourHotbar.java @@ -0,0 +1,40 @@ +package dev.loki.loparkour.player.service; +import dev.loki.loparkour.player.core.ParkourPlayer; + +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.menu.core.ParkourOption; +import org.jetbrains.annotations.NotNull; + +/** + * In-session hotbar items (community, settings, lobby, quit). + */ +public final class ParkourHotbar { + + private ParkourHotbar() { + } + + public static void apply(@NotNull ParkourPlayer player) { + if (!Config.CONFIG.getBoolean("options.inventory-handling")) { + return; + } + + player.player.getInventory().clear(); + fillSlot(player, ParkourOption.COMMUNITY, "options.hotbar-slots.community", "community.item"); + fillSlot(player, ParkourOption.SETTINGS, "options.hotbar-slots.settings", "settings.item"); + fillSlot(player, ParkourOption.LOBBY, "options.hotbar-slots.lobby", "lobby.item"); + fillSlot(player, ParkourOption.QUIT, "options.hotbar-slots.quit", "other.quit"); + } + + private static void fillSlot( + @NotNull ParkourPlayer player, + @NotNull ParkourOption option, + @NotNull String slotPath, + @NotNull String itemPath) { + if (!option.mayPerform(player.player)) { + return; + } + int slot = Config.CONFIG.getInt(slotPath); + player.player.getInventory().setItem(slot, Locales.getItem(player.locale, itemPath).build()); + } +} diff --git a/src/main/java/dev/loki/loparkour/player/PlayerSettingsManager.java b/src/main/java/dev/loki/loparkour/player/service/PlayerSettingsManager.java similarity index 96% rename from src/main/java/dev/loki/loparkour/player/PlayerSettingsManager.java rename to src/main/java/dev/loki/loparkour/player/service/PlayerSettingsManager.java index 7b581cd..be5eb03 100644 --- a/src/main/java/dev/loki/loparkour/player/PlayerSettingsManager.java +++ b/src/main/java/dev/loki/loparkour/player/service/PlayerSettingsManager.java @@ -1,7 +1,8 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.service; +import dev.loki.loparkour.player.core.ParkourPlayer; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.menu.ParkourOption; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.menu.core.ParkourOption; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/dev/loki/loparkour/player/ScoreboardManager.java b/src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java similarity index 92% rename from src/main/java/dev/loki/loparkour/player/ScoreboardManager.java rename to src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java index b799474..219e730 100644 --- a/src/main/java/dev/loki/loparkour/player/ScoreboardManager.java +++ b/src/main/java/dev/loki/loparkour/player/service/ScoreboardManager.java @@ -1,10 +1,10 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.service; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.leaderboard.Leaderboard; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.leaderboard.core.Leaderboard; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.util.text.ColorUtil; import me.clip.placeholderapi.PlaceholderAPI; import org.bukkit.entity.Player; import org.bukkit.scoreboard.DisplaySlot; @@ -20,7 +20,7 @@ import java.util.UUID; * * @since 5.0.0 */ -class ScoreboardManager { +public class ScoreboardManager { private static final String[] COLOR_CODES = { "0", "1", "2", "3", "4", "5", "6", "7", diff --git a/src/main/java/dev/loki/loparkour/player/UserRegistry.java b/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java similarity index 87% rename from src/main/java/dev/loki/loparkour/player/UserRegistry.java rename to src/main/java/dev/loki/loparkour/player/service/UserRegistry.java index 97fa70c..943e41b 100644 --- a/src/main/java/dev/loki/loparkour/player/UserRegistry.java +++ b/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java @@ -1,14 +1,17 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.service; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.api.event.ParkourJoinEvent; import dev.loki.loparkour.api.event.ParkourLeaveEvent; -import dev.loki.loparkour.config.Config; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; import dev.loki.loparkour.player.data.PreviousData; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.storage.Storage; -import dev.loki.loparkour.world.Divider; +import dev.loki.loparkour.world.core.Divider; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -55,6 +58,7 @@ public class UserRegistry { } public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) { + dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.unloadPlayer(user.getUUID()); new ParkourLeaveEvent(user).call(); try { @@ -107,7 +111,7 @@ public class UserRegistry { private static void handleUnregisterError(ParkourUser user, Exception ex) { user.player.getServer().getLogger().log(java.util.logging.Level.SEVERE, "Error while trying to make player " + user.getName() + " leave", ex); - user.send("There was an error while trying to handle leaving."); + user.sendTranslated("other.leave_error"); } private static boolean shouldKickToBungee(boolean kickIfBungee) { diff --git a/src/main/java/dev/loki/loparkour/player/ParkourSpectator.java b/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java similarity index 92% rename from src/main/java/dev/loki/loparkour/player/ParkourSpectator.java rename to src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java index 794975b..0d5dc78 100644 --- a/src/main/java/dev/loki/loparkour/player/ParkourSpectator.java +++ b/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java @@ -1,11 +1,13 @@ -package dev.loki.loparkour.player; +package dev.loki.loparkour.player.spectator; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.api.event.ParkourSpectateEvent; -import dev.loki.loparkour.config.Locales; +import dev.loki.loparkour.config.locale.Locales; import dev.loki.loparkour.player.data.PreviousData; -import dev.loki.loparkour.session.Session; -import dev.loki.loparkour.util.ColorUtil; +import dev.loki.loparkour.session.core.Session; +import dev.loki.loparkour.util.text.ColorUtil; import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.ScheduledTask; import net.md_5.bungee.api.ChatMessageType; diff --git a/src/main/java/dev/loki/loparkour/reward/Reward.java b/src/main/java/dev/loki/loparkour/reward/core/Reward.java similarity index 88% rename from src/main/java/dev/loki/loparkour/reward/Reward.java rename to src/main/java/dev/loki/loparkour/reward/core/Reward.java index a6f7162..14c4824 100644 --- a/src/main/java/dev/loki/loparkour/reward/Reward.java +++ b/src/main/java/dev/loki/loparkour/reward/core/Reward.java @@ -1,11 +1,11 @@ -package dev.loki.loparkour.reward; +package dev.loki.loparkour.reward.core; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.api.Registry; -import dev.loki.loparkour.hook.VaultHook; -import dev.loki.loparkour.mode.Mode; -import dev.loki.loparkour.mode.Modes; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.api.core.Registry; +import dev.loki.loparkour.hook.vault.VaultHook; +import dev.loki.loparkour.mode.base.Mode; +import dev.loki.loparkour.mode.base.Modes; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/reward/Rewards.java b/src/main/java/dev/loki/loparkour/reward/core/Rewards.java similarity index 97% rename from src/main/java/dev/loki/loparkour/reward/Rewards.java rename to src/main/java/dev/loki/loparkour/reward/core/Rewards.java index b95815b..d3b0649 100644 --- a/src/main/java/dev/loki/loparkour/reward/Rewards.java +++ b/src/main/java/dev/loki/loparkour/reward/core/Rewards.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.reward; +package dev.loki.loparkour.reward.core; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; import java.util.HashMap; import java.util.List; diff --git a/src/main/java/dev/loki/loparkour/schematic/config/SchematicDifficultyConfig.java b/src/main/java/dev/loki/loparkour/schematic/config/SchematicDifficultyConfig.java new file mode 100644 index 0000000..0ef3700 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/config/SchematicDifficultyConfig.java @@ -0,0 +1,31 @@ +package dev.loki.loparkour.schematic.config; + +import dev.loki.loparkour.config.core.Config; +import dev.loki.loparkour.schematic.core.SchematicManager; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * Writes entries to {@code schematics/schematics.yml} and reloads {@link Config#SCHEMATICS}. + */ +public final class SchematicDifficultyConfig { + + private SchematicDifficultyConfig() { + } + + public static void setDifficulty(@NotNull String fileStem, double difficulty) throws IOException { + if (Config.SCHEMATICS.path == null) { + throw new IOException("Schematics config path is not initialized"); + } + + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(Config.SCHEMATICS.path); + String key = SchematicManager.configKey(fileStem); + yaml.set("difficulty." + key, difficulty); + yaml.save(Config.SCHEMATICS.path); + + Config.SCHEMATICS.fileConfiguration = YamlConfiguration.loadConfiguration(Config.SCHEMATICS.path); + Config.SCHEMATICS.load(); + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/convert/LpschemConverter.java b/src/main/java/dev/loki/loparkour/schematic/convert/LpschemConverter.java new file mode 100644 index 0000000..e38d75a --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/convert/LpschemConverter.java @@ -0,0 +1,115 @@ +package dev.loki.loparkour.schematic.convert; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.schematic.config.SchematicDifficultyConfig; +import dev.loki.loparkour.schematic.legacy.lpschem.LPSchematicLegacy; +import dev.loki.loparkour.schematic.legacy.lpschem.SchematicMetadata; +import dev.loki.loparkour.schematic.schem.SchematicClipboardBuilder; +import dev.loki.loparkour.schematic.schem.SchemWriter; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Converts legacy {@code .lpschem} files to {@code .schem} in the schematics folder. + */ +public final class LpschemConverter { + + private LpschemConverter() { + } + + public record ConvertResult(int converted, int failed, @NotNull List messages) { + } + + @NotNull + public static ConvertResult convertAll() { + List sources = new ArrayList<>(); + collect(LoParkour.getInFolder("schematics"), sources); + collect(LoParkour.getInFolder("schematics-new"), sources); + + File outDir = LoParkour.getInFolder("schematics"); + if (!outDir.exists()) { + outDir.mkdirs(); + } + + int ok = 0; + int failed = 0; + List messages = new ArrayList<>(); + + for (File file : sources) { + try { + String msg = convertFile(file, outDir); + messages.add(msg); + ok++; + } catch (IOException e) { + failed++; + messages.add(file.getName() + ": " + e.getMessage()); + LoParkour.getPlugin().getLogger().warning("lpschem convert failed: " + file.getName() + " — " + e.getMessage()); + } + } + + if (sources.isEmpty()) { + messages.add("No .lpschem files found in schematics/ or schematics-new/"); + } + + return new ConvertResult(ok, failed, messages); + } + + @NotNull + private static String convertFile(@NotNull File lpschemFile, @NotNull File outDir) throws IOException { + LPSchematicLegacy legacy = LPSchematicLegacy.load(lpschemFile); + SchematicMetadata meta = legacy.getMetadata(); + + String stem = resolveStem(meta, lpschemFile); + File outFile = new File(outDir, stem + ".schem"); + double difficulty = meta.getDifficulty() > 0 ? meta.getDifficulty() : 0.5; + + boolean wroteFile = false; + if (!outFile.exists()) { + Clipboard clipboard = SchematicClipboardBuilder.fromLegacy(legacy); + SchemWriter.save(clipboard, outFile); + wroteFile = true; + } + + SchematicDifficultyConfig.setDifficulty(stem, difficulty); + + return wroteFile + ? stem + ".schem (" + difficulty + ")" + : stem + " (yaml updated, .schem exists)"; + } + + @NotNull + private static String resolveStem(@NotNull SchematicMetadata meta, @NotNull File file) { + String name = meta.getName(); + if (name != null && !name.isBlank() && name.matches("^[a-zA-Z0-9_-]+$")) { + return name.toLowerCase(Locale.ROOT).startsWith("parkour-") + ? name.toLowerCase(Locale.ROOT) + : "parkour-" + name.toLowerCase(Locale.ROOT); + } + String base = file.getName(); + if (base.toLowerCase(Locale.ROOT).endsWith(".lpschem")) { + base = base.substring(0, base.length() - 7); + } + if (!base.toLowerCase(Locale.ROOT).startsWith("parkour-")) { + base = "parkour-" + base; + } + return base.toLowerCase(Locale.ROOT); + } + + private static void collect(@NotNull File dir, @NotNull List out) { + if (!dir.isDirectory()) { + return; + } + File[] files = dir.listFiles((d, name) -> name.toLowerCase(Locale.ROOT).endsWith(".lpschem")); + if (files != null) { + for (File f : files) { + out.add(f); + } + } + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java b/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java new file mode 100644 index 0000000..2bbbc65 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java @@ -0,0 +1,76 @@ +package dev.loki.loparkour.schematic.core; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import dev.loki.loparkour.schematic.nbt.StructurePaster; +import dev.loki.loparkour.schematic.schem.SchemPaster; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.structure.Structure; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * A parkour jump structure from {@code .nbt} (vanilla) or {@code .schem} / {@code .schematic} (WorldEdit). + */ +public final class ParkourSchematic { + + private final String id; + private final double difficulty; + private final SchematicFormat format; + @Nullable + private final Structure nbtStructure; + @Nullable + private final Clipboard weClipboard; + + private ParkourSchematic( + @NotNull String id, + double difficulty, + @NotNull SchematicFormat format, + @Nullable Structure nbtStructure, + @Nullable Clipboard weClipboard + ) { + this.id = id; + this.difficulty = difficulty; + this.format = format; + this.nbtStructure = nbtStructure; + this.weClipboard = weClipboard; + } + + @NotNull + public static ParkourSchematic fromNbt(@NotNull String id, double difficulty, @NotNull Structure structure) { + return new ParkourSchematic(id, difficulty, SchematicFormat.NBT, structure, null); + } + + @NotNull + public static ParkourSchematic fromSchem(@NotNull String id, double difficulty, @NotNull Clipboard clipboard) { + return new ParkourSchematic(id, difficulty, SchematicFormat.SCHEM, null, clipboard); + } + + @NotNull + public String getId() { + return id; + } + + public double getDifficulty() { + return difficulty; + } + + @NotNull + public SchematicFormat getFormat() { + return format; + } + + @NotNull + public List paste(@NotNull Location origin, @NotNull World world) { + if (format == SchematicFormat.NBT && nbtStructure != null) { + return StructurePaster.paste(nbtStructure, origin, world); + } + if (format == SchematicFormat.SCHEM && weClipboard != null) { + return SchemPaster.paste(weClipboard, origin, world); + } + return List.of(); + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/core/SchematicFormat.java b/src/main/java/dev/loki/loparkour/schematic/core/SchematicFormat.java new file mode 100644 index 0000000..ae5e2db --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/core/SchematicFormat.java @@ -0,0 +1,9 @@ +package dev.loki.loparkour.schematic.core; + +/** + * On-disk schematic format. + */ +public enum SchematicFormat { + NBT, + SCHEM +} diff --git a/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java b/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java new file mode 100644 index 0000000..46d1857 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java @@ -0,0 +1,177 @@ +package dev.loki.loparkour.schematic.core; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.schematic.schem.SchemLoader; +import dev.loki.loparkour.config.core.Config; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.structure.Structure; +import org.bukkit.structure.StructureManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Loads {@code .nbt}, {@code .schem}, and {@code .schematic} files from {@code plugins/LoParkour/schematics/}. + * Difficulty per structure is configured in {@code schematics/schematics.yml}. + */ +public final class SchematicManager { + + private static final String PARKOUR_PREFIX = "parkour-"; + + private final File folder; + private final Map loaded = new HashMap<>(); + + public SchematicManager() { + this.folder = LoParkour.getInFolder("schematics"); + if (!folder.exists()) { + folder.mkdirs(); + } + } + + public void loadAll() { + loaded.clear(); + + int nbtCount = loadNbtFiles(); + int schemCount = loadSchemFiles(); + + if (nbtCount + schemCount == 0) { + LoParkour.log("No .nbt / .schem files in " + folder.getAbsolutePath()); + return; + } + + LoParkour.log("Loaded " + loaded.size() + " schematic(s) (" + nbtCount + " nbt, " + schemCount + " schem)"); + } + + public void reload() { + loadAll(); + } + + @Nullable + public ParkourSchematic get(@NotNull String id) { + return loaded.get(id.toLowerCase(Locale.ROOT)); + } + + @NotNull + public Map getAll() { + return new HashMap<>(loaded); + } + + /** + * Picks a random structure with difficulty at most {@code maxDifficulty} + * (player schematic setting: higher value allows harder structures). + */ + @Nullable + public ParkourSchematic pick(double maxDifficulty) { + List eligible = new ArrayList<>(); + for (ParkourSchematic schematic : loaded.values()) { + if (schematic.getDifficulty() <= maxDifficulty + 1e-9) { + eligible.add(schematic); + } + } + if (eligible.isEmpty()) { + return null; + } + return eligible.get(ThreadLocalRandom.current().nextInt(eligible.size())); + } + + private int loadNbtFiles() { + File[] files = folder.listFiles((dir, name) -> name.endsWith(".nbt")); + if (files == null || files.length == 0) { + return 0; + } + + StructureManager structureManager = Bukkit.getStructureManager(); + int ok = 0; + for (File file : files) { + String stem = stem(file, ".nbt"); + try { + Structure structure = structureManager.loadStructure(file); + register(stem, ParkourSchematic.fromNbt(stem, resolveDifficulty(stem), structure)); + ok++; + } catch (IOException e) { + LoParkour.getPlugin().getLogger().severe("Failed to load structure: " + file.getName()); + e.printStackTrace(); + } + } + return ok; + } + + private int loadSchemFiles() { + File[] files = folder.listFiles((dir, name) -> { + String lower = name.toLowerCase(Locale.ROOT); + return lower.endsWith(".schem") || lower.endsWith(".schematic"); + }); + if (files == null || files.length == 0) { + return 0; + } + + int ok = 0; + for (File file : files) { + String stem = stem(file, file.getName().toLowerCase(Locale.ROOT).endsWith(".schematic") + ? ".schematic" + : ".schem"); + try { + Clipboard clipboard = SchemLoader.load(file); + register(stem, ParkourSchematic.fromSchem(stem, resolveDifficulty(stem), clipboard)); + ok++; + } catch (IOException e) { + LoParkour.getPlugin().getLogger().severe("Failed to load schematic: " + file.getName()); + e.printStackTrace(); + } + } + return ok; + } + + private void register(@NotNull String stem, @NotNull ParkourSchematic schematic) { + String key = stem.toLowerCase(Locale.ROOT); + if (loaded.containsKey(key)) { + LoParkour.getPlugin().getLogger().warning( + "Duplicate schematic id '" + stem + "' — keeping " + loaded.get(key).getFormat() + + ", skipping " + schematic.getFormat()); + return; + } + loaded.put(key, schematic); + } + + @NotNull + private static String stem(@NotNull File file, @NotNull String suffix) { + String name = file.getName(); + return name.substring(0, name.length() - suffix.length()); + } + + private static double resolveDifficulty(@NotNull String fileStem) { + ConfigurationSection section = Config.SCHEMATICS.fileConfiguration == null + ? null + : Config.SCHEMATICS.fileConfiguration.getConfigurationSection("difficulty"); + if (section == null) { + return 0.5; + } + + String key = configKey(fileStem); + if (section.contains(key)) { + return section.getDouble(key); + } + if (section.contains(fileStem)) { + return section.getDouble(fileStem); + } + return 0.5; + } + + @NotNull + public static String configKey(@NotNull String fileStem) { + if (fileStem.regionMatches(true, 0, PARKOUR_PREFIX, 0, PARKOUR_PREFIX.length())) { + return fileStem.substring(PARKOUR_PREFIX.length()); + } + return fileStem; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/create/SchematicCreator.java b/src/main/java/dev/loki/loparkour/schematic/create/SchematicCreator.java new file mode 100644 index 0000000..30280f3 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/create/SchematicCreator.java @@ -0,0 +1,71 @@ +package dev.loki.loparkour.schematic.create; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.schematic.config.SchematicDifficultyConfig; +import dev.loki.loparkour.schematic.schem.SchematicClipboardBuilder; +import dev.loki.loparkour.schematic.schem.SchemWriter; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +/** + * Saves a block selection as {@code .schem} and registers difficulty in YAML. + */ +public final class SchematicCreator { + + private SchematicCreator() { + } + + public record CreateResult(@NotNull String stem, double difficulty, @NotNull File file) { + } + + @NotNull + public static CreateResult create( + @NotNull Location pos1, + @NotNull Location pos2, + @Nullable String optionalStem, + double difficulty + ) throws IOException { + File folder = LoParkour.getInFolder("schematics"); + if (!folder.exists()) { + folder.mkdirs(); + } + + String stem = resolveStem(optionalStem, pos1, pos2); + File outFile = new File(folder, stem + ".schem"); + if (outFile.exists()) { + throw new IOException("Schematic already exists: " + outFile.getName()); + } + + Clipboard clipboard = SchematicClipboardBuilder.fromSelection(pos1, pos2); + SchemWriter.save(clipboard, outFile); + SchematicDifficultyConfig.setDifficulty(stem, difficulty); + + return new CreateResult(stem, difficulty, outFile); + } + + @NotNull + private static String resolveStem(@Nullable String optionalStem, @NotNull Location pos1, @NotNull Location pos2) { + if (optionalStem != null && !optionalStem.isBlank()) { + String cleaned = optionalStem.toLowerCase(Locale.ROOT); + if (!cleaned.matches("^[a-z0-9_-]+$")) { + throw new IllegalArgumentException("Invalid schematic name"); + } + return cleaned.startsWith("parkour-") ? cleaned : "parkour-" + cleaned; + } + return "parkour-" + SchematicIdGenerator.fromSelection(pos1, pos2); + } + + public static double parseDifficulty(@NotNull String raw) { + double value = Double.parseDouble(raw.replace(',', '.')); + if (value < 0 || value > 1) { + throw new IllegalArgumentException("Difficulty must be between 0.0 and 1.0"); + } + return value; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/create/SchematicIdGenerator.java b/src/main/java/dev/loki/loparkour/schematic/create/SchematicIdGenerator.java new file mode 100644 index 0000000..374d4b0 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/create/SchematicIdGenerator.java @@ -0,0 +1,34 @@ +package dev.loki.loparkour.schematic.create; + +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +final class SchematicIdGenerator { + + private SchematicIdGenerator() { + } + + @NotNull + static String fromSelection(@NotNull Location pos1, @NotNull Location pos2) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + if (pos1.getWorld() != null) { + md.update(pos1.getWorld().getUID().toString().getBytes(StandardCharsets.UTF_8)); + } + md.update((byte) Math.min(pos1.getBlockX(), pos2.getBlockX())); + md.update((byte) Math.min(pos1.getBlockY(), pos2.getBlockY())); + md.update((byte) Math.min(pos1.getBlockZ(), pos2.getBlockZ())); + md.update((byte) Math.max(pos1.getBlockX(), pos2.getBlockX())); + md.update((byte) Math.max(pos1.getBlockY(), pos2.getBlockY())); + md.update((byte) Math.max(pos1.getBlockZ(), pos2.getBlockZ())); + return HexFormat.of().formatHex(md.digest()).substring(0, 8); + } catch (NoSuchAlgorithmException e) { + return Long.toHexString(System.nanoTime()).substring(0, 8); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/LPSchematicLegacy.java b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/LPSchematicLegacy.java new file mode 100644 index 0000000..814b9f2 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/LPSchematicLegacy.java @@ -0,0 +1,89 @@ +package dev.loki.loparkour.schematic.legacy.lpschem; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.zip.GZIPInputStream; + +/** + * Read-only loader for deprecated gzip+JSON {@code .lpschem} files (format v2). + */ +public final class LPSchematicLegacy { + + private static final Gson GSON = new GsonBuilder().create(); + private static final int FORMAT_VERSION = 2; + + private final SchematicMetadata metadata; + private final SchematicDimensions dimensions; + private final List palette; + private final int[] blocks; + + private LPSchematicLegacy( + SchematicMetadata metadata, + SchematicDimensions dimensions, + List palette, + int[] blocks + ) { + this.metadata = metadata; + this.dimensions = dimensions; + this.palette = palette; + this.blocks = blocks; + } + + @NotNull + public static LPSchematicLegacy load(@NotNull File file) throws IOException { + StringBuilder json = new StringBuilder(); + try (GZIPInputStream gzip = new GZIPInputStream(new FileInputStream(file)); + BufferedReader reader = new BufferedReader(new InputStreamReader(gzip, java.nio.charset.StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + json.append(line); + } + } + + SchematicData data = GSON.fromJson(json.toString(), SchematicData.class); + if (data == null || data.format_version != FORMAT_VERSION) { + throw new IOException("Unsupported .lpschem format in " + file.getName()); + } + + return new LPSchematicLegacy(data.metadata, data.dimensions, data.palette, data.blocks); + } + + @NotNull + public SchematicMetadata getMetadata() { + return metadata; + } + + @NotNull + public SchematicDimensions getDimensions() { + return dimensions; + } + + @NotNull + public List getPalette() { + return palette; + } + + @NotNull + public int[] getBlockArray() { + return blocks; + } + + private static class SchematicData { + int format_version; + SchematicMetadata metadata; + SchematicDimensions dimensions; + List palette; + int[] blocks; + SchematicMarkers markers; + SchematicVisuals visuals; + SchematicLogic logic; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicDimensions.java b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicDimensions.java new file mode 100644 index 0000000..b44a65f --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicDimensions.java @@ -0,0 +1,9 @@ +package dev.loki.loparkour.schematic.legacy.lpschem; + +/** Gson POJO for legacy {@code .lpschem}. */ +public class SchematicDimensions { + + public int width; + public int height; + public int length; +} diff --git a/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicLogic.java b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicLogic.java new file mode 100644 index 0000000..4ab590f --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicLogic.java @@ -0,0 +1,5 @@ +package dev.loki.loparkour.schematic.legacy.lpschem; + +/** Gson POJO for legacy {@code .lpschem} (optional section). */ +public class SchematicLogic { +} diff --git a/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMarkers.java b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMarkers.java new file mode 100644 index 0000000..e4fceac --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMarkers.java @@ -0,0 +1,17 @@ +package dev.loki.loparkour.schematic.legacy.lpschem; + +import java.util.List; + +/** Gson POJO for legacy {@code .lpschem}. */ +public class SchematicMarkers { + + public Vector3i start; + public Vector3i end; + public List checkpoints; + + public static class Vector3i { + public int x; + public int y; + public int z; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMetadata.java b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMetadata.java new file mode 100644 index 0000000..0d7c64e --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicMetadata.java @@ -0,0 +1,21 @@ +package dev.loki.loparkour.schematic.legacy.lpschem; + +/** Gson POJO for legacy {@code .lpschem}. */ +public class SchematicMetadata { + + private String name; + private String author; + private double difficulty; + + public String getName() { + return name; + } + + public String getAuthor() { + return author; + } + + public double getDifficulty() { + return difficulty; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicVisuals.java b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicVisuals.java new file mode 100644 index 0000000..d1f9edb --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/legacy/lpschem/SchematicVisuals.java @@ -0,0 +1,5 @@ +package dev.loki.loparkour.schematic.legacy.lpschem; + +/** Gson POJO for legacy {@code .lpschem} (optional section). */ +public class SchematicVisuals { +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java deleted file mode 100644 index cbe7c99..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java +++ /dev/null @@ -1,189 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.jetbrains.annotations.NotNull; - -import java.io.*; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -public class LPSchematic { - - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private static final int FORMAT_VERSION = 2; - - private final SchematicMetadata metadata; - private final SchematicDimensions dimensions; - private final List palette; - private final int[] blocks; - private final SchematicMarkers markers; - private SchematicVisuals visuals; - private SchematicLogic logic; - - public LPSchematic(@NotNull SchematicMetadata metadata, - @NotNull SchematicDimensions dimensions, - @NotNull List palette, - @NotNull int[] blocks, - @NotNull SchematicMarkers markers) { - this.metadata = metadata; - this.dimensions = dimensions; - this.palette = palette; - this.blocks = blocks; - this.markers = markers; - } - - public void save(@NotNull File file) throws IOException { - SchematicData data = new SchematicData(); - data.format_version = FORMAT_VERSION; - data.metadata = metadata; - data.dimensions = dimensions; - data.palette = palette; - data.blocks = blocks; - data.markers = markers; - data.visuals = visuals; - data.logic = logic; - - String json = GSON.toJson(data); - - try (GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream(file))) { - gzip.write(json.getBytes("UTF-8")); - } - } - - @NotNull - public static LPSchematic load(@NotNull File file) throws IOException { - StringBuilder json = new StringBuilder(); - - try (GZIPInputStream gzip = new GZIPInputStream(new FileInputStream(file)); - BufferedReader reader = new BufferedReader(new InputStreamReader(gzip, "UTF-8"))) { - String line; - while ((line = reader.readLine()) != null) { - json.append(line); - } - } - - SchematicData data = GSON.fromJson(json.toString(), SchematicData.class); - - if (data.format_version != FORMAT_VERSION) { - throw new IOException("Unsupported format version: " + data.format_version); - } - - LPSchematic schematic = new LPSchematic( - data.metadata, - data.dimensions, - data.palette, - data.blocks, - data.markers - ); - - schematic.visuals = data.visuals; - schematic.logic = data.logic; - - return schematic; - } - - @NotNull - public List paste(@NotNull Location origin, @NotNull World world) { - List placedBlocks = new ArrayList<>(); - - int width = dimensions.width; - int height = dimensions.height; - int length = dimensions.length; - - for (int y = 0; y < height; y++) { - for (int z = 0; z < length; z++) { - for (int x = 0; x < width; x++) { - int index = x + (z * width) + (y * width * length); - int paletteIndex = blocks[index]; - - if (paletteIndex == 0) { - continue; - } - - String blockDataString = palette.get(paletteIndex); - BlockData blockData = parseBlockData(blockDataString); - - if (blockData == null) { - continue; - } - - Block block = world.getBlockAt( - origin.getBlockX() + x, - origin.getBlockY() + y, - origin.getBlockZ() + z - ); - - block.setBlockData(blockData); - placedBlocks.add(block); - } - } - } - - return placedBlocks; - } - - private BlockData parseBlockData(@NotNull String blockDataString) { - try { - return org.bukkit.Bukkit.createBlockData(blockDataString); - } catch (IllegalArgumentException e) { - return Material.MAGENTA_GLAZED_TERRACOTTA.createBlockData(); - } - } - - public SchematicMetadata getMetadata() { - return metadata; - } - - public SchematicDimensions getDimensions() { - return dimensions; - } - - public SchematicMarkers getMarkers() { - return markers; - } - - public SchematicVisuals getVisuals() { - return visuals; - } - - public void setVisuals(SchematicVisuals visuals) { - this.visuals = visuals; - } - - public SchematicLogic getLogic() { - return logic; - } - - public void setLogic(SchematicLogic logic) { - this.logic = logic; - } - - /** Raw block palette-index array. Do not modify directly. */ - public int[] getBlockArray() { - return blocks; - } - - /** The palette of block data strings. Do not modify directly. */ - public List getPalette() { - return palette; - } - - private static class SchematicData { - int format_version; - SchematicMetadata metadata; - SchematicDimensions dimensions; - List palette; - int[] blocks; - SchematicMarkers markers; - SchematicVisuals visuals; - SchematicLogic logic; - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java deleted file mode 100644 index 2cd7d18..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java +++ /dev/null @@ -1,124 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import java.util.ArrayList; - -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -public class LPSchematicBuilder { - - private final SchematicMetadata metadata; - private final SchematicDimensions dimensions; - private final Map paletteMap; - private final List palette; - private final int[] blocks; - private SchematicMarkers.Vector3i start; - private SchematicMarkers.Vector3i end; - private final List checkpoints; - - public LPSchematicBuilder(@NotNull String name, @NotNull String author, double difficulty, - int width, int height, int length) { - this.metadata = new SchematicMetadata(name, author, difficulty); - this.dimensions = new SchematicDimensions(width, height, length); - this.paletteMap = new HashMap<>(); - this.palette = new ArrayList<>(); - this.blocks = new int[width * height * length]; - this.checkpoints = new ArrayList<>(); - - palette.add("minecraft:air"); - paletteMap.put("minecraft:air", 0); - } - - public LPSchematicBuilder setBlock(int x, int y, int z, @NotNull BlockData blockData) { - String blockString = blockData.getAsString(); - int paletteIndex = getOrAddToPalette(blockString); - int index = x + (z * dimensions.width) + (y * dimensions.width * dimensions.length); - blocks[index] = paletteIndex; - return this; - } - - public LPSchematicBuilder setBlock(int x, int y, int z, @NotNull Material material) { - return setBlock(x, y, z, material.createBlockData()); - } - - public LPSchematicBuilder setStart(int x, int y, int z) { - this.start = new SchematicMarkers.Vector3i(x, y, z); - return this; - } - - public LPSchematicBuilder setEnd(int x, int y, int z) { - this.end = new SchematicMarkers.Vector3i(x, y, z); - return this; - } - - public LPSchematicBuilder addCheckpoint(int x, int y, int z) { - checkpoints.add(new SchematicMarkers.Vector3i(x, y, z)); - return this; - } - - public LPSchematicBuilder addTag(@NotNull String tag) { - metadata.addTag(tag); - return this; - } - - public LPSchematicBuilder fromWorld(@NotNull World world, @NotNull Location corner1, @NotNull Location corner2) { - int minX = Math.min(corner1.getBlockX(), corner2.getBlockX()); - int minY = Math.min(corner1.getBlockY(), corner2.getBlockY()); - int minZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ()); - - int maxX = Math.max(corner1.getBlockX(), corner2.getBlockX()); - int maxY = Math.max(corner1.getBlockY(), corner2.getBlockY()); - int maxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ()); - - for (int y = minY; y <= maxY; y++) { - for (int z = minZ; z <= maxZ; z++) { - for (int x = minX; x <= maxX; x++) { - Block block = world.getBlockAt(x, y, z); - if (block.getType() != Material.AIR) { - int relX = x - minX; - int relY = y - minY; - int relZ = z - minZ; - setBlock(relX, relY, relZ, block.getBlockData()); - - if (block.getType() == Material.LIME_WOOL) { - setStart(relX, relY, relZ); - } else if (block.getType() == Material.RED_WOOL) { - setEnd(relX, relY, relZ); - } - } - } - } - } - - return this; - } - - @NotNull - public LPSchematic build() { - if (start == null || end == null) { - throw new IllegalStateException("Start and end markers must be set"); - } - - SchematicMarkers markers = new SchematicMarkers(start, end); - checkpoints.forEach(markers::addCheckpoint); - - return new LPSchematic(metadata, dimensions, palette, blocks, markers); - } - - private int getOrAddToPalette(@NotNull String blockString) { - if (paletteMap.containsKey(blockString)) { - return paletteMap.get(blockString); - } - - int index = palette.size(); - palette.add(blockString); - paletteMap.put(blockString, index); - return index; - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java deleted file mode 100644 index 445f88e..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java +++ /dev/null @@ -1,67 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import dev.loki.loparkour.LoParkour; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class LPSchematicManager { - - private static final File SCHEMATIC_FOLDER = LoParkour.getInFolder("schematics-new"); - private final Map loadedSchematics = new HashMap<>(); - - public LPSchematicManager() { - if (!SCHEMATIC_FOLDER.exists()) { - SCHEMATIC_FOLDER.mkdirs(); - } - } - - public void loadAll() { - File[] files = SCHEMATIC_FOLDER.listFiles((dir, name) -> name.endsWith(".lpschem")); - - if (files == null || files.length == 0) { - LoParkour.log("No .lpschem files found"); - return; - } - - for (File file : files) { - try { - LPSchematic schematic = LPSchematic.load(file); - String name = schematic.getMetadata().getName(); - loadedSchematics.put(name, schematic); - LoParkour.log("Loaded schematic: " + name); - } catch (IOException e) { - LoParkour.getPlugin().getLogger().severe("Failed to load schematic: " + file.getName()); - e.printStackTrace(); - } - } - - LoParkour.log("Loaded " + loadedSchematics.size() + " schematics"); - } - - @Nullable - public LPSchematic getSchematic(@NotNull String name) { - return loadedSchematics.get(name); - } - - public void saveSchematic(@NotNull LPSchematic schematic) throws IOException { - String fileName = schematic.getMetadata().getName().replaceAll("[^a-zA-Z0-9-_]", "_") + ".lpschem"; - File file = new File(SCHEMATIC_FOLDER, fileName); - schematic.save(file); - loadedSchematics.put(schematic.getMetadata().getName(), schematic); - LoParkour.log("Saved schematic: " + schematic.getMetadata().getName()); - } - - public Map getAllSchematics() { - return new HashMap<>(loadedSchematics); - } - - public void reload() { - loadedSchematics.clear(); - loadAll(); - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java deleted file mode 100644 index cf3d03b..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -public class SchematicDimensions { - - public int width; - public int height; - public int length; - - public SchematicDimensions(int width, int height, int length) { - this.width = width; - this.height = height; - this.length = length; - } - - public int getTotalBlocks() { - return width * height * length; - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java deleted file mode 100644 index 798df46..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import java.util.ArrayList; -import java.util.List; - -public class SchematicLogic { - - private List ghost_path; - - public SchematicLogic() { - this.ghost_path = new ArrayList<>(); - } - - public List getGhostPath() { - return ghost_path; - } - - public void setGhostPath(List ghost_path) { - this.ghost_path = ghost_path; - } - - public void addGhostPoint(int x, int y, int z) { - ghost_path.add(new int[]{x, y, z}); - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java deleted file mode 100644 index 5f9ec05..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; - -public class SchematicMarkers { - - private Vector3i start; - private Vector3i end; - private List checkpoints; - - public SchematicMarkers(@NotNull Vector3i start, @NotNull Vector3i end) { - this.start = start; - this.end = end; - this.checkpoints = new ArrayList<>(); - } - - public Vector3i getStart() { - return start; - } - - public void setStart(Vector3i start) { - this.start = start; - } - - public Vector3i getEnd() { - return end; - } - - public void setEnd(Vector3i end) { - this.end = end; - } - - public List getCheckpoints() { - return checkpoints; - } - - public void addCheckpoint(Vector3i checkpoint) { - checkpoints.add(checkpoint); - } - - public static class Vector3i { - public int x; - public int y; - public int z; - - public Vector3i(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java deleted file mode 100644 index 2a15485..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.List; - -public class SchematicMetadata { - private String name; - private String author; - private double difficulty; - private List tags; - - public SchematicMetadata(@NotNull String name, @NotNull String author, double difficulty) { - this.name = name; - this.author = author; - this.difficulty = difficulty; - this.tags = new ArrayList<>(); - } - - public String getName() { - return name; - } - - public String getAuthor() { - return author; - } - - public double getDifficulty() { - return difficulty; - } - - public List getTags() { - return tags; - } - - public void addTag(@NotNull String tag) { - tags.add(tag); - } -} \ No newline at end of file diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicTransformer.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicTransformer.java deleted file mode 100644 index 354d011..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicTransformer.java +++ /dev/null @@ -1,145 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import org.bukkit.Bukkit; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Directional; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; - -/** - * Rotates a {@link LPSchematic} by 90/180/270 degrees around the Y axis. - * Preserves block facing where possible via {@link Directional} BlockData. - */ -public class SchematicTransformer { - - private final LPSchematic source; - - public SchematicTransformer(@NotNull LPSchematic source) { - this.source = source; - } - - /** - * Returns a new rotated schematic. The original is not modified. - * - * @param degrees 90, 180, or 270 (clockwise from above). - */ - @NotNull - public LPSchematic rotate(int degrees) { - degrees = ((degrees % 360) + 360) % 360; - if (degrees == 0) return source; - - SchematicDimensions dim = source.getDimensions(); - int origW = dim.width; - int origH = dim.height; - int origL = dim.length; - - // New dimensions after rotation - int newW = (degrees == 180) ? origW : origL; - int newL = (degrees == 180) ? origL : origW; - - int[] origBlocks = getBlocks(); - List palette = getPalette(); - - int[] rotated = new int[origW * origH * origL]; - - for (int y = 0; y < origH; y++) { - for (int z = 0; z < origL; z++) { - for (int x = 0; x < origW; x++) { - int origIdx = x + (z * origW) + (y * origW * origL); - int paletteIdx = origBlocks[origIdx]; - - int nx, nz; - switch (degrees) { - case 90 -> { nx = origL - 1 - z; nz = x; } - case 180 -> { nx = origW - 1 - x; nz = origL - 1 - z; } - case 270 -> { nx = z; nz = origW - 1 - x; } - default -> { nx = x; nz = z; } - } - - int newIdx = nx + (nz * newW) + (y * newW * newL); - rotated[newIdx] = paletteIdx; - } - } - } - - // Rotate palette entries (block facing) - List rotatedPalette = rotatePalette(palette, degrees); - - SchematicDimensions newDim = new SchematicDimensions(newW, origH, newL); - - // Rotate markers - SchematicMarkers origMarkers = source.getMarkers(); - SchematicMarkers newMarkers = rotateMarkers(origMarkers, origW, origL, newW, newL, degrees); - - return new LPSchematic(source.getMetadata(), newDim, rotatedPalette, rotated, newMarkers); - } - - // ── Internals ──────────────────────────────────────────────────────────── - - private int[] getBlocks() { - return source.getBlockArray(); - } - - private List getPalette() { - return new ArrayList<>(source.getPalette()); - } - - private List rotatePalette(List palette, int degrees) { - List result = new ArrayList<>(palette.size()); - for (String blockStr : palette) { - result.add(rotateBlockData(blockStr, degrees)); - } - return result; - } - - private String rotateBlockData(String blockDataStr, int degrees) { - try { - BlockData bd = Bukkit.createBlockData(blockDataStr); - if (bd instanceof Directional dir) { - for (int i = 0; i < degrees / 90; i++) { - dir.setFacing(rotateDirection(dir.getFacing())); - } - return dir.getAsString(); - } - return blockDataStr; - } catch (Exception e) { - return blockDataStr; - } - } - - private org.bukkit.block.BlockFace rotateDirection(org.bukkit.block.BlockFace face) { - return switch (face) { - case NORTH -> org.bukkit.block.BlockFace.EAST; - case EAST -> org.bukkit.block.BlockFace.SOUTH; - case SOUTH -> org.bukkit.block.BlockFace.WEST; - case WEST -> org.bukkit.block.BlockFace.NORTH; - default -> face; - }; - } - - private SchematicMarkers rotateMarkers(SchematicMarkers m, - int origW, int origL, - int newW, int newL, - int degrees) { - var start = rotateVec(m.getStart(), origW, origL, degrees); - var end = rotateVec(m.getEnd(), origW, origL, degrees); - - SchematicMarkers result = new SchematicMarkers(start, end); - for (var cp : m.getCheckpoints()) { - result.addCheckpoint(rotateVec(cp, origW, origL, degrees)); - } - return result; - } - - private SchematicMarkers.Vector3i rotateVec(SchematicMarkers.Vector3i v, - int origW, int origL, int degrees) { - return switch (degrees) { - case 90 -> new SchematicMarkers.Vector3i(origL - 1 - v.z, v.y, v.x); - case 180 -> new SchematicMarkers.Vector3i(origW - 1 - v.x, v.y, origL - 1 - v.z); - case 270 -> new SchematicMarkers.Vector3i(v.z, v.y, origW - 1 - v.x); - default -> new SchematicMarkers.Vector3i(v.x, v.y, v.z); - }; - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java deleted file mode 100644 index 063ee0e..0000000 --- a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java +++ /dev/null @@ -1,72 +0,0 @@ -package dev.loki.loparkour.schematic.lpschem; - -import java.util.ArrayList; -import java.util.List; - -public class SchematicVisuals { - - private List displays; - private List particles; - - public SchematicVisuals() { - this.displays = new ArrayList<>(); - this.particles = new ArrayList<>(); - } - - public List getDisplays() { - return displays; - } - - public void addDisplay(BlockDisplay display) { - displays.add(display); - } - - public List getParticles() { - return particles; - } - - public void addParticle(ParticleEffect particle) { - particles.add(particle); - } - - public static class BlockDisplay { - public String type; - public String block; - public float[] pos; - public float[] scale; - public Animation animation; - - public BlockDisplay(String block, float[] pos, float[] scale) { - this.type = "block"; - this.block = block; - this.pos = pos; - this.scale = scale; - } - } - - public static class Animation { - public String type; - public String axis; - public float speed; - - public Animation(String type, String axis, float speed) { - this.type = type; - this.axis = axis; - this.speed = speed; - } - } - - public static class ParticleEffect { - public String type; - public float[] pos; - public int amount; - public float spread; - - public ParticleEffect(String type, float[] pos, int amount, float spread) { - this.type = type; - this.pos = pos; - this.amount = amount; - this.spread = spread; - } - } -} diff --git a/src/main/java/dev/loki/loparkour/schematic/nbt/StructurePaster.java b/src/main/java/dev/loki/loparkour/schematic/nbt/StructurePaster.java new file mode 100644 index 0000000..10d189a --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/nbt/StructurePaster.java @@ -0,0 +1,57 @@ +package dev.loki.loparkour.schematic.nbt; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.structure.Mirror; +import org.bukkit.block.structure.StructureRotation; +import org.bukkit.structure.Structure; +import org.bukkit.util.BlockVector; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public final class StructurePaster { + + private StructurePaster() { + } + + @NotNull + public static List paste(@NotNull Structure structure, @NotNull Location origin, @NotNull World world) { + structure.place( + origin, + false, + StructureRotation.NONE, + Mirror.NONE, + -1, + 1.0f, + new Random() + ); + + BlockVector size = structure.getSize(); + int width = size.getBlockX(); + int height = size.getBlockY(); + int length = size.getBlockZ(); + + int baseX = origin.getBlockX(); + int baseY = origin.getBlockY(); + int baseZ = origin.getBlockZ(); + + List placed = new ArrayList<>(); + for (int y = 0; y < height; y++) { + for (int z = 0; z < length; z++) { + for (int x = 0; x < width; x++) { + Block block = world.getBlockAt(baseX + x, baseY + y, baseZ + z); + BlockData data = block.getBlockData(); + if (!data.getMaterial().isAir()) { + placed.add(block); + } + } + } + } + return placed; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/Schematics.java b/src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java similarity index 50% rename from src/main/java/dev/loki/loparkour/schematic/Schematics.java rename to src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java index 3c3f8d5..c18e3d7 100644 --- a/src/main/java/dev/loki/loparkour/schematic/Schematics.java +++ b/src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java @@ -1,12 +1,13 @@ -package dev.loki.loparkour.schematic; +package dev.loki.loparkour.schematic.registry; import dev.loki.loparkour.LoParkour; +import dev.loki.loparkour.schematic.core.SchematicManager; import dev.lolib.scheduler.Scheduler; import java.io.File; /** - * Schematic folder discovery (files are loaded on demand elsewhere). + * Ensures the schematics folder exists; structures are loaded by {@link SchematicManager}. */ public class Schematics { @@ -18,14 +19,17 @@ public class Schematics { FOLDER.mkdirs(); } - File[] files = FOLDER.listFiles((dir, name) -> name.endsWith(".lpschem")); + File[] files = FOLDER.listFiles((dir, name) -> { + String lower = name.toLowerCase(); + return lower.endsWith(".nbt") || lower.endsWith(".schem") || lower.endsWith(".schematic"); + }); if (files == null || files.length == 0) { - LoParkour.log("No schematics found in " + FOLDER.getAbsolutePath()); + LoParkour.log("No .nbt / .schem files in " + FOLDER.getAbsolutePath()); return; } - LoParkour.log("Found " + files.length + " schematic files"); + LoParkour.log("Found " + files.length + " schematic file(s) in schematics/"); }); } } diff --git a/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java b/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java new file mode 100644 index 0000000..6278472 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java @@ -0,0 +1,29 @@ +package dev.loki.loparkour.schematic.schem; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public final class SchemLoader { + + private SchemLoader() { + } + + @NotNull + public static Clipboard load(@NotNull File file) throws IOException { + ClipboardFormat format = ClipboardFormats.findByFile(file); + if (format == null) { + throw new IOException("Unsupported schematic file: " + file.getName()); + } + try (FileInputStream in = new FileInputStream(file); + ClipboardReader reader = format.getReader(in)) { + return reader.read(); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/schem/SchemPaster.java b/src/main/java/dev/loki/loparkour/schematic/schem/SchemPaster.java new file mode 100644 index 0000000..2be1bdf --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/schem/SchemPaster.java @@ -0,0 +1,47 @@ +package dev.loki.loparkour.schematic.schem; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.block.BlockState; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public final class SchemPaster { + + private SchemPaster() { + } + + @NotNull + public static List paste(@NotNull Clipboard clipboard, @NotNull Location origin, @NotNull World world) { + BlockVector3 pasteAt = BlockVector3.at( + origin.getBlockX(), + origin.getBlockY(), + origin.getBlockZ() + ); + BlockVector3 min = clipboard.getMinimumPoint(); + + List placed = new ArrayList<>(); + for (BlockVector3 pos : clipboard.getRegion()) { + BlockState weBlock = clipboard.getBlock(pos); + if (weBlock.getBlockType().getMaterial().isAir()) { + continue; + } + + BlockVector3 dest = pasteAt.add(pos.subtract(min)); + Block block = world.getBlockAt(dest.x(), dest.y(), dest.z()); + try { + block.setBlockData(Bukkit.createBlockData(weBlock.getAsString()), false); + placed.add(block); + } catch (IllegalArgumentException ignored) { + // Block state not valid on this server version + } + } + return placed; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/schem/SchemWriter.java b/src/main/java/dev/loki/loparkour/schematic/schem/SchemWriter.java new file mode 100644 index 0000000..6102cf3 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/schem/SchemWriter.java @@ -0,0 +1,31 @@ +package dev.loki.loparkour.schematic.schem; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +public final class SchemWriter { + + private SchemWriter() { + } + + public static void save(@NotNull Clipboard clipboard, @NotNull File file) throws IOException { + ClipboardFormat format = ClipboardFormats.findByFile(file); + if (format == null) { + format = ClipboardFormats.findByAlias("sponge"); + } + if (format == null) { + throw new IOException("No clipboard format for " + file.getName()); + } + try (FileOutputStream out = new FileOutputStream(file); + ClipboardWriter writer = format.getWriter(out)) { + writer.write(clipboard); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/schem/SchematicClipboardBuilder.java b/src/main/java/dev/loki/loparkour/schematic/schem/SchematicClipboardBuilder.java new file mode 100644 index 0000000..f8c3546 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/schem/SchematicClipboardBuilder.java @@ -0,0 +1,104 @@ +package dev.loki.loparkour.schematic.schem; + +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.world.block.BlockState; +import dev.loki.loparkour.schematic.legacy.lpschem.LPSchematicLegacy; +import dev.loki.loparkour.schematic.legacy.lpschem.SchematicDimensions; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public final class SchematicClipboardBuilder { + + private SchematicClipboardBuilder() { + } + + @NotNull + public static Clipboard fromSelection(@NotNull Location pos1, @NotNull Location pos2) { + if (pos1.getWorld() == null || pos2.getWorld() == null || !pos1.getWorld().equals(pos2.getWorld())) { + throw new IllegalArgumentException("Selection must be in one world"); + } + + int minX = Math.min(pos1.getBlockX(), pos2.getBlockX()); + int minY = Math.min(pos1.getBlockY(), pos2.getBlockY()); + int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ()); + int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX()); + int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY()); + int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ()); + + BlockVector3 min = BlockVector3.at(minX, minY, minZ); + BlockVector3 max = BlockVector3.at(maxX, maxY, maxZ); + CuboidRegion region = new CuboidRegion(min, max); + BlockArrayClipboard clipboard = new BlockArrayClipboard(region); + + World world = pos1.getWorld(); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + Block block = world.getBlockAt(x, y, z); + if (block.getType().isAir()) { + continue; + } + BlockVector3 rel = BlockVector3.at(x, y, z).subtract(min); + try { + BlockData data = block.getBlockData(); + BlockState state = BukkitAdapter.adapt(data); + clipboard.setBlock(rel, state); + } catch (IllegalArgumentException ignored) { + // Unknown block on this version + } + } + } + } + return clipboard; + } + + @NotNull + public static Clipboard fromLegacy(@NotNull LPSchematicLegacy legacy) { + SchematicDimensions dim = legacy.getDimensions(); + BlockVector3 min = BlockVector3.at(0, 0, 0); + BlockVector3 max = BlockVector3.at(dim.width - 1, dim.height - 1, dim.length - 1); + CuboidRegion region = new CuboidRegion(min, max); + BlockArrayClipboard clipboard = new BlockArrayClipboard(region); + + List palette = legacy.getPalette(); + int[] blocks = legacy.getBlockArray(); + int width = dim.width; + int length = dim.length; + + for (int y = 0; y < dim.height; y++) { + for (int z = 0; z < length; z++) { + for (int x = 0; x < width; x++) { + int index = x + (z * width) + (y * width * length); + int paletteIndex = blocks[index]; + if (paletteIndex <= 0 || paletteIndex >= palette.size()) { + continue; + } + String blockData = palette.get(paletteIndex); + if (blockData == null || blockData.isBlank()) { + continue; + } + try { + BlockData data = org.bukkit.Bukkit.createBlockData(blockData); + BlockState state = BukkitAdapter.adapt(data); + if (state.getBlockType().getMaterial().isAir()) { + continue; + } + clipboard.setBlock(BlockVector3.at(x, y, z), state); + } catch (IllegalArgumentException ignored) { + // Skip invalid states + } + } + } + } + return clipboard; + } +} diff --git a/src/main/java/dev/loki/loparkour/session/Session.java b/src/main/java/dev/loki/loparkour/session/core/Session.java similarity index 89% rename from src/main/java/dev/loki/loparkour/session/Session.java rename to src/main/java/dev/loki/loparkour/session/core/Session.java index b6b65e1..5cbbf5d 100644 --- a/src/main/java/dev/loki/loparkour/session/Session.java +++ b/src/main/java/dev/loki/loparkour/session/core/Session.java @@ -1,9 +1,11 @@ -package dev.loki.loparkour.session; +package dev.loki.loparkour.session.core; +import dev.loki.loparkour.session.manager.SessionStateManager; +import dev.loki.loparkour.session.manager.SessionPlayerManager; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourSpectator; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.player.core.ParkourUser; import org.bukkit.Location; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -139,7 +141,7 @@ public class Session { stateManager.cleanup(); } - void setGenerator(@NotNull ParkourGenerator generator) { + public void setGenerator(@NotNull ParkourGenerator generator) { this.generator = generator; } @@ -151,4 +153,4 @@ public class Session { public enum ChatType { LOBBY_ONLY, PLAYERS_ONLY, PUBLIC } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/session/SessionPlayerManager.java b/src/main/java/dev/loki/loparkour/session/manager/SessionPlayerManager.java similarity index 93% rename from src/main/java/dev/loki/loparkour/session/SessionPlayerManager.java rename to src/main/java/dev/loki/loparkour/session/manager/SessionPlayerManager.java index de9fa33..6c51d09 100644 --- a/src/main/java/dev/loki/loparkour/session/SessionPlayerManager.java +++ b/src/main/java/dev/loki/loparkour/session/manager/SessionPlayerManager.java @@ -1,8 +1,9 @@ -package dev.loki.loparkour.session; +package dev.loki.loparkour.session.manager; +import dev.loki.loparkour.session.core.Session; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourSpectator; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.player.core.ParkourUser; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -144,4 +145,4 @@ public class SessionPlayerManager { } } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/session/SessionStateManager.java b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java similarity index 89% rename from src/main/java/dev/loki/loparkour/session/SessionStateManager.java rename to src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java index 1d75a9c..1cae06f 100644 --- a/src/main/java/dev/loki/loparkour/session/SessionStateManager.java +++ b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java @@ -1,9 +1,11 @@ -package dev.loki.loparkour.session; +package dev.loki.loparkour.session.manager; +import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.generator.ParkourGenerator; -import dev.loki.loparkour.player.ParkourUser; -import dev.loki.loparkour.world.Divider; +import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices; +import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; +import dev.loki.loparkour.player.core.ParkourUser; +import dev.loki.loparkour.world.core.Divider; import dev.lolib.scheduler.Scheduler; import dev.lolib.scheduler.ScheduledTask; import org.bukkit.Location; @@ -39,6 +41,7 @@ public class SessionStateManager { // Create generator ParkourGenerator generator = generatorFunction.apply(session); session.setGenerator(generator); + AdaptiveServices.attachToGenerator(generator); // Build island at spawn location generator.island.build(spawnLocation); @@ -137,4 +140,4 @@ public class SessionStateManager { public void clearMutedUsers() { mutedUsers.clear(); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/session/SessionUserManager.java b/src/main/java/dev/loki/loparkour/session/manager/SessionUserManager.java similarity index 91% rename from src/main/java/dev/loki/loparkour/session/SessionUserManager.java rename to src/main/java/dev/loki/loparkour/session/manager/SessionUserManager.java index 028e1d7..41e05df 100644 --- a/src/main/java/dev/loki/loparkour/session/SessionUserManager.java +++ b/src/main/java/dev/loki/loparkour/session/manager/SessionUserManager.java @@ -1,9 +1,10 @@ -package dev.loki.loparkour.session; +package dev.loki.loparkour.session.manager; +import dev.loki.loparkour.session.core.Session; -import dev.loki.loparkour.config.Locales; -import dev.loki.loparkour.player.ParkourPlayer; -import dev.loki.loparkour.player.ParkourSpectator; -import dev.loki.loparkour.player.ParkourUser; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.player.core.ParkourPlayer; +import dev.loki.loparkour.player.spectator.ParkourSpectator; +import dev.loki.loparkour.player.core.ParkourUser; import java.util.*; diff --git a/src/main/java/dev/loki/loparkour/storage/Storage.java b/src/main/java/dev/loki/loparkour/storage/Storage.java index 97b0cd3..d358035 100644 --- a/src/main/java/dev/loki/loparkour/storage/Storage.java +++ b/src/main/java/dev/loki/loparkour/storage/Storage.java @@ -1,8 +1,10 @@ package dev.loki.loparkour.storage; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.storage.disk.StorageDisk; +import dev.loki.loparkour.storage.sql.StorageSQL; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.jetbrains.annotations.NotNull; import java.util.Map; diff --git a/src/main/java/dev/loki/loparkour/storage/StorageDisk.java b/src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java similarity index 96% rename from src/main/java/dev/loki/loparkour/storage/StorageDisk.java rename to src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java index c569fb1..a4b075a 100644 --- a/src/main/java/dev/loki/loparkour/storage/StorageDisk.java +++ b/src/main/java/dev/loki/loparkour/storage/disk/StorageDisk.java @@ -1,9 +1,9 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.disk; import com.google.gson.annotations.Expose; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -20,7 +20,7 @@ import java.util.UUID; * * @since 5.0.0 */ -class StorageDisk { +public class StorageDisk { public static @NotNull Map readScores(@NotNull String mode) { File file = getLeaderboardFile(mode); diff --git a/src/main/java/dev/loki/loparkour/storage/SQLConnectionManager.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java similarity index 96% rename from src/main/java/dev/loki/loparkour/storage/SQLConnectionManager.java rename to src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java index 9d94fc4..51435ac 100644 --- a/src/main/java/dev/loki/loparkour/storage/SQLConnectionManager.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java @@ -1,9 +1,9 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.sql; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Option; +import dev.loki.loparkour.config.options.Option; import java.sql.Connection; import java.sql.PreparedStatement; @@ -14,7 +14,7 @@ import java.util.concurrent.CopyOnWriteArrayList; /** * MySQL access via HikariCP pool. */ -class SQLConnectionManager { +public class SQLConnectionManager { private HikariDataSource dataSource; private volatile boolean connected = false; diff --git a/src/main/java/dev/loki/loparkour/storage/SQLDataMapper.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java similarity index 96% rename from src/main/java/dev/loki/loparkour/storage/SQLDataMapper.java rename to src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java index 04e58f2..bac9888 100644 --- a/src/main/java/dev/loki/loparkour/storage/SQLDataMapper.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.sql; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -122,4 +122,4 @@ public class SQLDataMapper { String settings, long lastSeen ) {} -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/storage/SQLMigrationManager.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLMigrationManager.java similarity index 73% rename from src/main/java/dev/loki/loparkour/storage/SQLMigrationManager.java rename to src/main/java/dev/loki/loparkour/storage/sql/SQLMigrationManager.java index 1e15489..b36e73e 100644 --- a/src/main/java/dev/loki/loparkour/storage/SQLMigrationManager.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLMigrationManager.java @@ -1,6 +1,6 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.sql; -import dev.loki.loparkour.config.Option; +import dev.loki.loparkour.config.options.Option; /** * Manages database schema migrations. @@ -50,5 +50,22 @@ class SQLMigrationManager { // Fix legacy Boolean locale values (true/false) to proper locale codes queryExecutor.executeStaticUpdateSuppressed( "UPDATE `%soptions` SET `locale` = 'en' WHERE `locale` IN ('true', 'false', '1', '0');".formatted(prefix)); + + createAdaptiveStatsTable(); + } + + private void createAdaptiveStatsTable() { + queryExecutor.executeStaticUpdate( + ("CREATE TABLE IF NOT EXISTS loparkour_player_stats (" + + "player_uuid VARCHAR(36) PRIMARY KEY, " + + "skill_rating DOUBLE DEFAULT 1.0, " + + "sessions_count INT DEFAULT 0, " + + "total_jumps INT DEFAULT 0, " + + "total_falls INT DEFAULT 0, " + + "longest_streak INT DEFAULT 0, " + + "last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + + "INDEX idx_skill_rating (skill_rating), " + + "INDEX idx_last_updated (last_updated)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4")); } } diff --git a/src/main/java/dev/loki/loparkour/storage/SQLQueryBuilder.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLQueryBuilder.java similarity index 98% rename from src/main/java/dev/loki/loparkour/storage/SQLQueryBuilder.java rename to src/main/java/dev/loki/loparkour/storage/sql/SQLQueryBuilder.java index 18d0cd1..eaffbd7 100644 --- a/src/main/java/dev/loki/loparkour/storage/SQLQueryBuilder.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLQueryBuilder.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.sql; import org.jetbrains.annotations.NotNull; @@ -101,4 +101,4 @@ public class SQLQueryBuilder { public static String getTableName(@NotNull String mode) { return "scores_" + mode.toLowerCase().replace(" ", "_"); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/storage/SQLQueryExecutor.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLQueryExecutor.java similarity index 98% rename from src/main/java/dev/loki/loparkour/storage/SQLQueryExecutor.java rename to src/main/java/dev/loki/loparkour/storage/sql/SQLQueryExecutor.java index 4eb3b44..2f83f90 100644 --- a/src/main/java/dev/loki/loparkour/storage/SQLQueryExecutor.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLQueryExecutor.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.sql; import dev.loki.loparkour.LoParkour; diff --git a/src/main/java/dev/loki/loparkour/storage/StorageSQL.java b/src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java similarity index 82% rename from src/main/java/dev/loki/loparkour/storage/StorageSQL.java rename to src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java index 7d452a9..644e61b 100644 --- a/src/main/java/dev/loki/loparkour/storage/StorageSQL.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.storage; +package dev.loki.loparkour.storage.sql; -import dev.loki.loparkour.leaderboard.Score; -import dev.loki.loparkour.player.ParkourPlayer; +import dev.loki.loparkour.leaderboard.model.Score; +import dev.loki.loparkour.player.core.ParkourPlayer; import org.jetbrains.annotations.NotNull; import java.sql.Connection; @@ -27,6 +27,14 @@ public class StorageSQL { public static boolean isConnected() { return connectionManager != null && connectionManager.isConnected(); } + + @NotNull + public static SQLConnectionManager getConnectionManager() { + if (connectionManager == null) { + throw new IllegalStateException("SQL connection manager is not initialized"); + } + return connectionManager; + } /** * Run callback when database is connected. @@ -177,6 +185,22 @@ public class StorageSQL { try (PreparedStatement stmt = conn.prepareStatement(SQLQueryBuilder.createPlayersTable())) { stmt.executeUpdate(); } + + // Adaptive player stats + try (PreparedStatement stmt = conn.prepareStatement( + "CREATE TABLE IF NOT EXISTS loparkour_player_stats (" + + "player_uuid VARCHAR(36) PRIMARY KEY, " + + "skill_rating DOUBLE DEFAULT 1.0, " + + "sessions_count INT DEFAULT 0, " + + "total_jumps INT DEFAULT 0, " + + "total_falls INT DEFAULT 0, " + + "longest_streak INT DEFAULT 0, " + + "last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + + "INDEX idx_skill_rating (skill_rating), " + + "INDEX idx_last_updated (last_updated)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4")) { + stmt.executeUpdate(); + } } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/style/RandomStyle.java b/src/main/java/dev/loki/loparkour/style/core/RandomStyle.java similarity index 94% rename from src/main/java/dev/loki/loparkour/style/RandomStyle.java rename to src/main/java/dev/loki/loparkour/style/core/RandomStyle.java index 365d671..ef7a13a 100644 --- a/src/main/java/dev/loki/loparkour/style/RandomStyle.java +++ b/src/main/java/dev/loki/loparkour/style/core/RandomStyle.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.style; +package dev.loki.loparkour.style.core; import org.bukkit.Material; @@ -26,4 +26,4 @@ public final class RandomStyle implements Style { return name; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/style/Style.java b/src/main/java/dev/loki/loparkour/style/core/Style.java similarity index 88% rename from src/main/java/dev/loki/loparkour/style/Style.java rename to src/main/java/dev/loki/loparkour/style/core/Style.java index 82ab9db..5f547a3 100644 --- a/src/main/java/dev/loki/loparkour/style/Style.java +++ b/src/main/java/dev/loki/loparkour/style/core/Style.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.style; +package dev.loki.loparkour.style.core; import org.bukkit.Material; diff --git a/src/main/java/dev/loki/loparkour/util/Menu.java b/src/main/java/dev/loki/loparkour/util/gui/Menu.java similarity index 59% rename from src/main/java/dev/loki/loparkour/util/Menu.java rename to src/main/java/dev/loki/loparkour/util/gui/Menu.java index 589fbce..a442dbd 100644 --- a/src/main/java/dev/loki/loparkour/util/Menu.java +++ b/src/main/java/dev/loki/loparkour/util/gui/Menu.java @@ -1,28 +1,30 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.gui; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.util.item.Item; import org.bukkit.entity.Player; /** - * Temporary stub for Menu during migration + * Legacy menu stub — real menus use LoLib {@link dev.lolib.gui.InventoryGUI}. */ public class Menu { private final int rows; private final String title; - + public Menu(int rows, String title) { this.rows = rows; this.title = title; } - + public Menu item(int slot, Item item) { return this; } - + public Menu distributeRowsEvenly() { return this; } - + public void open(Player player) { - player.sendMessage("§c" + title + " (" + rows + " рядов) — меню временно недоступно (миграция GUI)."); + Locales.send(player, "other.menu_unavailable", title, rows); } } diff --git a/src/main/java/dev/loki/loparkour/util/MenuItem.java b/src/main/java/dev/loki/loparkour/util/gui/MenuItem.java similarity index 90% rename from src/main/java/dev/loki/loparkour/util/MenuItem.java rename to src/main/java/dev/loki/loparkour/util/gui/MenuItem.java index 83f49e9..4b9ff5a 100644 --- a/src/main/java/dev/loki/loparkour/util/MenuItem.java +++ b/src/main/java/dev/loki/loparkour/util/gui/MenuItem.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.gui; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/util/PagedMenu.java b/src/main/java/dev/loki/loparkour/util/gui/PagedMenu.java similarity index 55% rename from src/main/java/dev/loki/loparkour/util/PagedMenu.java rename to src/main/java/dev/loki/loparkour/util/gui/PagedMenu.java index 34cbec2..726c641 100644 --- a/src/main/java/dev/loki/loparkour/util/PagedMenu.java +++ b/src/main/java/dev/loki/loparkour/util/gui/PagedMenu.java @@ -1,46 +1,42 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.gui; +import dev.loki.loparkour.config.locale.Locales; +import dev.loki.loparkour.util.item.Item; import org.bukkit.entity.Player; -import java.util.ArrayList; + import java.util.List; /** - * Temporary stub for PagedMenu during migration + * Legacy paged menu stub — real menus use LoLib GUI. */ public class PagedMenu { private final int rows; private final String title; - private final List items = new ArrayList<>(); - + private List items = List.of(); + public PagedMenu(int rows, String title) { this.rows = rows; this.title = title; } - + public PagedMenu addToDisplay(List items) { + this.items = items; return this; } - + public PagedMenu item(int slot, Item item) { return this; } - + public PagedMenu nextPage(int slot, Item item) { return this; } - + public PagedMenu prevPage(int slot, Item item) { return this; } - - public void page(int direction) { - } - - public List getTotalToDisplay() { - return new ArrayList<>(); - } - + public void open(Player player) { - player.sendMessage("§c" + title + " (" + rows + " рядов, " + items.size() + " пунктов) — меню временно недоступно."); + Locales.send(player, "other.paged_menu_unavailable", title, rows, items.size()); } } diff --git a/src/main/java/dev/loki/loparkour/util/SliderItem.java b/src/main/java/dev/loki/loparkour/util/gui/SliderItem.java similarity index 82% rename from src/main/java/dev/loki/loparkour/util/SliderItem.java rename to src/main/java/dev/loki/loparkour/util/gui/SliderItem.java index bf212a2..6555837 100644 --- a/src/main/java/dev/loki/loparkour/util/SliderItem.java +++ b/src/main/java/dev/loki/loparkour/util/gui/SliderItem.java @@ -1,5 +1,6 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.gui; +import dev.loki.loparkour.util.item.Item; import org.bukkit.event.inventory.InventoryClickEvent; import java.util.function.Consumer; diff --git a/src/main/java/dev/loki/loparkour/util/Item.java b/src/main/java/dev/loki/loparkour/util/item/Item.java similarity index 97% rename from src/main/java/dev/loki/loparkour/util/Item.java rename to src/main/java/dev/loki/loparkour/util/item/Item.java index 961ee43..2c874c6 100644 --- a/src/main/java/dev/loki/loparkour/util/Item.java +++ b/src/main/java/dev/loki/loparkour/util/item/Item.java @@ -1,5 +1,6 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.item; +import dev.loki.loparkour.util.text.ColorUtil; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; diff --git a/src/main/java/dev/loki/loparkour/util/Materials.java b/src/main/java/dev/loki/loparkour/util/item/Materials.java similarity index 98% rename from src/main/java/dev/loki/loparkour/util/Materials.java rename to src/main/java/dev/loki/loparkour/util/item/Materials.java index f1aada8..0b57e05 100644 --- a/src/main/java/dev/loki/loparkour/util/Materials.java +++ b/src/main/java/dev/loki/loparkour/util/item/Materials.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.item; import org.bukkit.Material; import org.bukkit.NamespacedKey; diff --git a/src/main/java/dev/loki/loparkour/util/Probs.java b/src/main/java/dev/loki/loparkour/util/misc/Probs.java similarity index 98% rename from src/main/java/dev/loki/loparkour/util/Probs.java rename to src/main/java/dev/loki/loparkour/util/misc/Probs.java index 5ca2835..db1122f 100644 --- a/src/main/java/dev/loki/loparkour/util/Probs.java +++ b/src/main/java/dev/loki/loparkour/util/misc/Probs.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.misc; import java.util.Map; import java.util.Random; diff --git a/src/main/java/dev/loki/loparkour/util/SkullSetter.java b/src/main/java/dev/loki/loparkour/util/misc/SkullSetter.java similarity index 89% rename from src/main/java/dev/loki/loparkour/util/SkullSetter.java rename to src/main/java/dev/loki/loparkour/util/misc/SkullSetter.java index 07498fe..a002c19 100644 --- a/src/main/java/dev/loki/loparkour/util/SkullSetter.java +++ b/src/main/java/dev/loki/loparkour/util/misc/SkullSetter.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.misc; import org.bukkit.OfflinePlayer; import org.bukkit.inventory.meta.SkullMeta; diff --git a/src/main/java/dev/loki/loparkour/util/ParticleData.java b/src/main/java/dev/loki/loparkour/util/particle/ParticleData.java similarity index 94% rename from src/main/java/dev/loki/loparkour/util/ParticleData.java rename to src/main/java/dev/loki/loparkour/util/particle/ParticleData.java index b9fe441..d8c47fd 100644 --- a/src/main/java/dev/loki/loparkour/util/ParticleData.java +++ b/src/main/java/dev/loki/loparkour/util/particle/ParticleData.java @@ -1,6 +1,6 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.particle; -import dev.loki.loparkour.util.ParticleData; +import dev.loki.loparkour.util.particle.ParticleData; import org.bukkit.Particle; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/dev/loki/loparkour/util/ParticleUtil.java b/src/main/java/dev/loki/loparkour/util/particle/ParticleUtil.java similarity index 98% rename from src/main/java/dev/loki/loparkour/util/ParticleUtil.java rename to src/main/java/dev/loki/loparkour/util/particle/ParticleUtil.java index 9f8f2fb..914b78e 100644 --- a/src/main/java/dev/loki/loparkour/util/ParticleUtil.java +++ b/src/main/java/dev/loki/loparkour/util/particle/ParticleUtil.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.particle; import org.bukkit.Location; import org.bukkit.Particle; diff --git a/src/main/java/dev/loki/loparkour/util/ColorUtil.java b/src/main/java/dev/loki/loparkour/util/text/ColorUtil.java similarity index 84% rename from src/main/java/dev/loki/loparkour/util/ColorUtil.java rename to src/main/java/dev/loki/loparkour/util/text/ColorUtil.java index 4a3b259..6a3039e 100644 --- a/src/main/java/dev/loki/loparkour/util/ColorUtil.java +++ b/src/main/java/dev/loki/loparkour/util/text/ColorUtil.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.text; import net.md_5.bungee.api.ChatColor; @@ -6,17 +6,19 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Utility class for color formatting - * Replaces vilib ColorUtil.color() + * Converts LoParkour text colors to Minecraft chat format. + *

    Project standard: {@code &#RRGGBB} and {@code &l} etc. — see {@code .cursor/rules/locale-and-colors.mdc}. + * MiniMessage tags are legacy-only; do not use in new locale or Java strings. */ public class ColorUtil { private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})"); + /** Legacy fallback for old strings; do not use in new code. */ private static final Pattern MINIMESSAGE_HEX_PATTERN = Pattern.compile("<#([A-Fa-f0-9]{6})>"); /** - * Converts color codes to Minecraft format - * Supports legacy (&), hex (&#RRGGBB), and MiniMessage (<#RRGGBB>) colors + * Converts color codes to Minecraft format. + * Primary: {@code &#RRGGBB} and legacy {@code &} codes. * * @param text The text to color * @return Colored text diff --git a/src/main/java/dev/loki/loparkour/util/Locations.java b/src/main/java/dev/loki/loparkour/util/world/Locations.java similarity index 97% rename from src/main/java/dev/loki/loparkour/util/Locations.java rename to src/main/java/dev/loki/loparkour/util/world/Locations.java index 9566b9a..a03815c 100644 --- a/src/main/java/dev/loki/loparkour/util/Locations.java +++ b/src/main/java/dev/loki/loparkour/util/world/Locations.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.util; +package dev.loki.loparkour.util.world; import org.bukkit.Location; diff --git a/src/main/java/dev/loki/loparkour/world/Divider.java b/src/main/java/dev/loki/loparkour/world/core/Divider.java similarity index 95% rename from src/main/java/dev/loki/loparkour/world/Divider.java rename to src/main/java/dev/loki/loparkour/world/core/Divider.java index bc66fce..4bcbd5d 100644 --- a/src/main/java/dev/loki/loparkour/world/Divider.java +++ b/src/main/java/dev/loki/loparkour/world/core/Divider.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.world; +package dev.loki.loparkour.world.core; -import dev.loki.loparkour.config.Option; -import dev.loki.loparkour.session.Session; +import dev.loki.loparkour.config.options.Option; +import dev.loki.loparkour.session.core.Session; import org.bukkit.Location; import java.util.HashMap; diff --git a/src/main/java/dev/loki/loparkour/world/World.java b/src/main/java/dev/loki/loparkour/world/core/World.java similarity index 97% rename from src/main/java/dev/loki/loparkour/world/World.java rename to src/main/java/dev/loki/loparkour/world/core/World.java index b0bad02..917f5d7 100644 --- a/src/main/java/dev/loki/loparkour/world/World.java +++ b/src/main/java/dev/loki/loparkour/world/core/World.java @@ -1,7 +1,7 @@ -package dev.loki.loparkour.world; +package dev.loki.loparkour.world.core; import dev.loki.loparkour.LoParkour; -import dev.loki.loparkour.config.Config; +import dev.loki.loparkour.config.core.Config; import dev.lolib.worlds.VoidGenerator; import org.bukkit.*; @@ -136,7 +136,7 @@ public class World { /** * Prevents path traversal and accidental deletion outside the server world container. */ - static boolean isValidWorldFolderName(String n) { + public static boolean isValidWorldFolderName(String n) { if (n == null || n.isBlank() || n.length() > 64) { return false; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e848bfe..bc179ff 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -33,6 +33,15 @@ debug: false # -= Enable joining =- joining: true +# -= Adaptive difficulty =- +adaptive: + enabled: true + min-sessions: 3 + update-interval: 300 + rating-weight: 0.15 + confidence-threshold: 0.3 + near-miss-distance: 0.3 + # -= Proxy options =- bungeecord: enabled: false diff --git a/src/main/resources/docs/SCHEMATICS.md b/src/main/resources/docs/SCHEMATICS.md new file mode 100644 index 0000000..a4daaa8 --- /dev/null +++ b/src/main/resources/docs/SCHEMATICS.md @@ -0,0 +1,79 @@ +# Parkour schematics (LoParkour) + +Jump structures are loaded from `plugins/LoParkour/schematics/` and matched to a **difficulty** in `plugins/LoParkour/schematics/schematics.yml`. + +## Supported file types + +| Extension | Source | Notes | +|-----------|--------|--------| +| `.nbt` | Vanilla structure block, `/structure save` | Pasted with Bukkit `StructureManager` | +| `.schem`, `.schematic` | WorldEdit (`//copy`, `//schem save`) | Read via embedded WorldEdit clipboard IO | +| `.lpschem` | **Legacy LoParkour format** | Not used in-game; convert with `/lp schematic convert` | + +During parkour generation, when the profile rolls **schematic** type, the plugin picks a random file whose `difficulty` in YAML is **≤** the player's schematic setting (0.0–1.0). + +## Difficulty keys (`schematics.yml`) + +```yaml +difficulty: + d0e227d2: 0.25 # short id (recommended) +``` + +For file `parkour-d0e227d2.schem` you can use key `d0e227d2` or the full stem `parkour-d0e227d2`. + +Player setting **0.25 (easy)** → only structures with difficulty ≤ 0.25. +**1.0** → any structure. + +## Creating schematics in-game (admin) + +1. Get the wand: `/lp schematic wand` +2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`) +3. Save and register: + - `/lp create ` — auto name `parkour-.schem` + YAML entry + - `/lp schematic create ` — same + - `/lp schematic create ` — custom name (letters, numbers, `-`, `_`) + +Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1). + +4. `/lp schematic reload` — reload files without restart + +## Converting old `.lpschem` files + +Place `.lpschem` files in `plugins/LoParkour/schematics/` or legacy `schematics-new/`, then: + +``` +/lp schematic convert +``` + +Each file becomes `parkour-.schem` in `schematics/` and gets a `difficulty:` entry (from the old metadata, or `0.5` if missing). Original `.lpschem` files are kept; delete them manually after checking. + +## Admin commands + +| Command | Description | +|---------|-------------| +| `/lp schematic wand` | Selection wand | +| `/lp schematic pos1` / `pos2` | Set corners at your feet | +| `/lp create ` | Save selection → `.schem` + YAML | +| `/lp schematic create ` | Save with custom name | +| `/lp schematic convert` | Convert all `.lpschem` in schematics folders | +| `/lp schematic list` | List loaded structures | +| `/lp schematic paste ` | Test-paste at your location | +| `/lp schematic reload` | Reload disk files | + +## Flow (runtime) + +```mermaid +flowchart LR + A[BlockPlacer SCHEMATIC roll] --> B[SchematicManager.pick] + B --> C{Format} + C -->|nbt| D[Structure.place] + C -->|schem| E[Clipboard paste] + D --> F[Track blocks / score] + E --> F +``` + +## Folders + +- `plugins/LoParkour/schematics/` — `.nbt`, `.schem`, `.schematic` (active) +- `plugins/LoParkour/schematics/schematics.yml` — difficulty map +- `plugins/LoParkour/schematics-new/` — legacy; scanned only for `.lpschem` convert diff --git a/src/main/resources/locales/en.yml b/src/main/resources/locales/en.yml index 0e44910..9f80e23 100644 --- a/src/main/resources/locales/en.yml +++ b/src/main/resources/locales/en.yml @@ -2,7 +2,7 @@ # By loki # Copy this file and rename it to add your own language! # Materials have to be from: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html -# Learn more about colouring: https://docs.adventure.kyori.net/minimessage/format.html#format +# Colors: hex &#RRGGBB and legacy & codes only (see .cursor/rules/locale-and-colors.mdc) name: "English" @@ -236,10 +236,14 @@ lobby: head: material: "" - name: "�D4FF&l%s" + name: "&#FFFF55%s%s" + muted_suffix: " &#FF4444[mute]" lore: "" top: "񢩈Muted in lobby||" bottom: "�D4FFLeft click񢩈 to kick from lobby||�D4FFRight click񢩈 to (un)mute in lobby" + lore_kick: "&#A0A0A0Left-click &#FFFFFFto kick" + lore_mute: "&#A0A0A0Right-click &#FFFFFFto mute" + lore_unmute: "&#A0A0A0Right-click &#FFFFFFto unmute" scoreboard: @@ -256,8 +260,24 @@ scoreboard: other: no_do: "&#A0A0A0You can't do this right now." + no_menu_items: "&#FF4444&lNo menu items are available for you right now." + joining_disabled: "&#FF4444&lJoining is currently disabled." no_teleporting: "&#FFFFFFYou can't enter the parkour world by teleporting!" customize: "&#A0A0A0Customize your parkour experience by using \"/parkour\"" + leave_error: "&#FF4444&lThere was an error while trying to leave." + bungee_kick: "Couldn't move you to %s. Please rejoin." + menu_unavailable: "&#FF4444%s &#A0A0A0(%d rows) — menu unavailable." + paged_menu_unavailable: "&#FF4444%s &#A0A0A0(%d rows, %d items) — menu unavailable." + + back: + material: "red_dye" + name: "&#DE1F1F&lBack" + lore: "" + + next: + material: "lime_dye" + name: "�DCB07&lNext" + lore: "" quit: material: "barrier" @@ -280,19 +300,95 @@ modes: name: "�D4FF&lElytra" lore: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost" start_hint: "&#A0A0A0Fly through the rings! Use fireworks to boost." + ring_perfect: "7FF55Perfect! +%d" + ring_good: "7FF55Good! +%d" + fell: "&#FF4444Fell: %s" + action_bar: "&#FFFF55Ring: %d/%d &#A0A0A0| 7FF55Score: &#FFFFFF%d" race: material: "golden_boots" name: "&#FFD700&lRace" lore: "&#A0A0A0First to reach target score!||&#A0A0A0Compete for fastest time" completed: "&#FFD700Finished! Score: &#FFFFFF%s &#A0A0Ain &#FFFFFF%s" + title: "&#FFD700&lRACE COMPLETE!" + subtitle: "7FF55Finished in &#FFFFFF%s" + action_bar: "%s &#FFFF55%d &#A0A0A0/ 7FF55%d%s" + action_bar_left: " &#A0A0A0(&#FF4444%d &#A0A0A0left)" + action_bar_done: " &#FFD700&lDONE!" coop: material: "player_head" name: "�FF00&lCo-op" lore: "&#A0A0A0Team up with friends!||&#A0A0A0Shared score and progress" - player_joined: "�FF00%s &#A0A0Ajoined the co-op session!" - stats: "&#A0A0ATeam score: &#FFFFFF%s &#A0A0A| Your blocks: &#FFFFFF%s &#A0A0A(&#FFFFFF%s%%&#A0A0A)" + player_joined: "�FF00%s &#A0A0A0joined the co-op session!" + stats: "&#A0A0A0Team score: &#FFFFFF%s &#A0A0A0| Your blocks: &#FFFFFF%s &#A0A0A0(&#FFFFFF%s%%&#A0A0A0)" + milestone_title: "&#FFD700&lTeam: &#FFFFFF%d" + milestone_subtitle: "7FF55Keep it up!" admin: - cooldown: "Please wait before using this again." + cooldown: "&#A0A0A0Please wait before using this again." + force_join_everyone: "7FF55Force joined everyone!" + force_join_player: "7FF55Force joined %s!" + force_leave_everyone: "7FF55Force kicked everyone!" + player_not_online: "&#FF4444Player not online!" + player_not_playing: "&#FF4444That player isn't playing parkour!" + reset_all: "7FF55Reset all high scores." + reset_player: "7FF55Reset high score of %s." + recover_inventory: "7FF55Recovered inventory of %s." + recover_inventory_missing: "&#FF4444No saved inventory found for %s." + +commands: + too_many_args: "&#A0A0A0Too many arguments. Try /parkour help" + error: "&#FF4444&lLoParkour: command error — see server console." + unknown_player: "&#FF4444Unknown player." + not_playing: "&#FF4444That player isn't playing." + reload: "&#A0A0A0Reloading config files... (locales load asynchronously)" + help: + header: "񢩈&m--------------- &#FF6464&lLoParkour 񢩈&m---------------" + line_parkour: "&#A0A0A0/parkour 񢩈- Main command" + line_join: "&#A0A0A0/parkour join [mode/player] 񢩈- Join a mode" + line_leave: "&#A0A0A0/parkour leave 񢩈- Leave parkour" + line_menu: "&#A0A0A0/parkour menu 񢩈- Open the menu" + line_play: "&#A0A0A0/parkour play 񢩈- Mode selection menu" + line_leaderboard: "&#A0A0A0/parkour leaderboard [type] 񢩈- Leaderboard" + line_schematic: "&#A0A0A0/LoParkour schematic 񢩈- Schematic tools" + line_reload: "&#A0A0A0/LoParkour reload 񢩈- Reload configs" + line_reset: "&#A0A0A0/LoParkour reset 񢩈- Reset scores" + line_force: "&#A0A0A0/LoParkour forcejoin/forceleave 񢩈- Force join/leave" + line_recover: "&#A0A0A0/LoParkour recoverinventory 񢩈- Recover inventory" + +schematic: + wand: + material: "golden_axe" + name: "&#FF4444&lSchematic Wand" + lore: "&#A0A0A0Left click: position 1||&#A0A0A0Right click: position 2" + help: + header: "B0000&lSchematics 񢩈-----------" + line_wand: "&#FF4444/LoParkour schematic wand 񢩈- &#A0A0A0Selection wand" + line_pos1: "&#FF4444/LoParkour schematic pos1 񢩈- &#A0A0A0Set position 1" + line_pos2: "&#FF4444/LoParkour schematic pos2 񢩈- &#A0A0A0Set position 2" + line_create: "&#FF4444/LoParkour create 񢩈- &#A0A0A0Save selection to .schem + yml" + line_create_named: "&#FF4444/LoParkour schematic create 񢩈- &#A0A0A0Named save" + line_convert: "&#FF4444/LoParkour schematic convert 񢩈- &#A0A0A0Convert .lpschem to .schem" + line_paste: "&#FF4444/LoParkour schematic paste 񢩈- &#A0A0A0Test-paste" + line_list: "&#FF4444/LoParkour schematic list 񢩈- &#A0A0A0List loaded" + line_reload: "&#FF4444/LoParkour schematic reload 񢩈- &#A0A0A0Reload from disk" + line_folder: "&#A0A0A0Folder: plugins/LoParkour/schematics/ (.nbt, .schem, .schematic)" + line_docs: "&#A0A0A0Docs: plugins/LoParkour/docs/SCHEMATICS.md (also in jar /docs)" + wand_given: "&#A0A0A0Wand given. Left = pos1, Right = pos2." + list_header: "&#A0A0A0Loaded schematics:" + list_entry: "&#A0A0A0- &#FF4444%s 񢩈(%s, %s)" + reloaded: "&#A0A0A0Reloaded." + pos_set: "&#A0A0A0Position %s set to %s" + invalid_name: "&#FF4444Invalid name. Use only letters, numbers, hyphens and underscores." + invalid_difficulty: "&#FF4444Difficulty must be a number from 0.0 to 1.0 (e.g. 0.25, 0.5, 1.0)." + selection_incomplete: "&#FF4444Selection incomplete. Use wand or pos1/pos2 first." + created: "&#A0A0A0Added &#FF4444%s 񢩈(diff %s) as &#FF4444%s" + create_failed: "&#FF4444Create failed: %s" + convert_done: "&#A0A0A0Converted %s file(s), %s failed." + convert_truncated: "&#A0A0A0...and %s more (see console)." + not_found: "&#FF4444Schematic '%s' not found." + pasted: "&#A0A0A0Pasted &#FF4444%s&#A0A0A0." + +ghost: + saved: "�D4FFYour ghost was saved with a score of %s!" diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 4a66ac1..0b12185 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -1,7 +1,7 @@ # Русский (Russian, ru) # By loki # Материалы должны быть из: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html -# Узнайте больше о цветах: https://docs.adventure.kyori.net/minimessage/format.html#format +# Цвета: hex &#RRGGBB и legacy & (см. .cursor/rules/locale-and-colors.mdc) name: "Русский" @@ -235,10 +235,14 @@ lobby: head: material: "" - name: "�D4FF&l%s" + name: "&#FFFF55%s%s" + muted_suffix: " &#FF4444[заглушён]" lore: "" top: "񢩈Заглушён в лобби||" bottom: "�D4FFЛевый клик񢩈 чтобы исключить из лобби||�D4FFПравый клик񢩈 чтобы заглушить/разблокировать в лобби" + lore_kick: "&#A0A0A0ЛКМ &#FFFFFF— исключить" + lore_mute: "&#A0A0A0ПКМ &#FFFFFF— заглушить" + lore_unmute: "&#A0A0A0ПКМ &#FFFFFF— разглушить" scoreboard: @@ -255,8 +259,24 @@ scoreboard: other: no_do: "&#A0A0A0Вы не можете сделать это сейчас." + no_menu_items: "&#FF4444&lСейчас нет доступных пунктов меню." + joining_disabled: "&#FF4444&lПрисоединение временно отключено." no_teleporting: "&#FFFFFFВы не можете войти в мир паркура телепортацией!" customize: "&#A0A0A0Настройте свой паркур, используя \"/parkour\"" + leave_error: "&#FF4444&lОшибка при выходе из паркура." + bungee_kick: "Не удалось перенести вас на %s. Перезайдите." + menu_unavailable: "&#FF4444%s &#A0A0A0(%d рядов) — меню недоступно." + paged_menu_unavailable: "&#FF4444%s &#A0A0A0(%d рядов, %d пунктов) — меню недоступно." + + back: + material: "red_dye" + name: "&#DE1F1F&lНазад" + lore: "" + + next: + material: "lime_dye" + name: "�DCB07&lДалее" + lore: "" quit: material: "barrier" @@ -279,22 +299,95 @@ modes: name: "�D4FF&lЭлитры" lore: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения" start_hint: "&#A0A0A0Летите через кольца! Используйте фейерверки для ускорения." + ring_perfect: "7FF55Идеально! +%d" + ring_good: "7FF55Хорошо! +%d" + fell: "&#FF4444Падение: %s" + action_bar: "&#FFFF55Кольцо: %d/%d &#A0A0A0| 7FF55Счёт: &#FFFFFF%d" race: material: "golden_boots" name: "&#FFD700&lГонка" lore: "&#A0A0A0Первым достигните целевого счёта!||&#A0A0A0Соревнуйтесь за лучшее время" - completed: "&#FFD700Финиш! Счёт: &#FFFFFF%s &#A0A0Aза &#FFFFFF%s" + completed: "&#FFD700Финиш! Счёт: &#FFFFFF%s &#A0A0A0за &#FFFFFF%s" + title: "&#FFD700&lГОНКА ЗАВЕРШЕНА!" + subtitle: "7FF55Время: &#FFFFFF%s" + action_bar: "%s &#FFFF55%d &#A0A0A0/ 7FF55%d%s" + action_bar_left: " &#A0A0A0(&#FF4444%d &#A0A0A0осталось)" + action_bar_done: " &#FFD700&lГОТОВО!" coop: material: "player_head" name: "�FF00&lКооператив" lore: "&#A0A0A0Объединитесь с друзьями!||&#A0A0A0Общий счёт и прогресс" - player_joined: "�FF00%s &#A0A0Aприсоединился к кооперативной сессии!" - stats: "&#A0A0AКомандный счёт: &#FFFFFF%s &#A0A0A| Ваши блоки: &#FFFFFF%s &#A0A0A(&#FFFFFF%s%%&#A0A0A)" + player_joined: "�FF00%s &#A0A0A0присоединился к кооперативной сессии!" + stats: "&#A0A0A0Командный счёт: &#FFFFFF%s &#A0A0A0| Ваши блоки: &#FFFFFF%s &#A0A0A0(&#FFFFFF%s%%&#A0A0A0)" + milestone_title: "&#FFD700&lКоманда: &#FFFFFF%d" + milestone_subtitle: "7FF55Так держать!" admin: - cooldown: "Подождите немного перед повторным использованием." + cooldown: "&#A0A0A0Подождите перед повторным использованием." + force_join_everyone: "7FF55Все игроки принудительно вошли!" + force_join_player: "7FF55Игрок %s принудительно вошёл!" + force_leave_everyone: "7FF55Все игроки принудительно выгнаны!" + player_not_online: "&#FF4444Игрок не в сети!" + player_not_playing: "&#FF4444Игрок не в паркуре!" + reset_all: "7FF55Все рекорды сброшены." + reset_player: "7FF55Рекорд игрока %s сброшен." + recover_inventory: "7FF55Инвентарь %s восстановлен." + recover_inventory_missing: "&#FF4444Сохранённый инвентарь для %s не найден." + +commands: + too_many_args: "&#A0A0A0Слишком много аргументов. Попробуйте /parkour help" + error: "&#FF4444&lLoParkour: ошибка команды — см. консоль сервера." + unknown_player: "&#FF4444Неизвестный игрок." + not_playing: "&#FF4444Этот игрок не в паркуре." + reload: "&#A0A0A0Перезагрузка конфигов... (локали грузятся асинхронно)" + help: + header: "񢩈&m--------------- &#FF6464&lLoParkour 񢩈&m---------------" + line_parkour: "&#A0A0A0/parkour 񢩈- Главная команда" + line_join: "&#A0A0A0/parkour join [режим/игрок] 񢩈- Войти в режим" + line_leave: "&#A0A0A0/parkour leave 񢩈- Выйти" + line_menu: "&#A0A0A0/parkour menu 񢩈- Открыть меню" + line_play: "&#A0A0A0/parkour play 񢩈- Выбор режима" + line_leaderboard: "&#A0A0A0/parkour leaderboard [тип] 񢩈- Таблица лидеров" + line_schematic: "&#A0A0A0/LoParkour schematic 񢩈- Схематики" + line_reload: "&#A0A0A0/LoParkour reload 񢩈- Перезагрузка конфигов" + line_reset: "&#A0A0A0/LoParkour reset <все/игрок> 񢩈- Сброс рекордов" + line_force: "&#A0A0A0/LoParkour forcejoin/forceleave <цель> 񢩈- Принудительный вход/выход" + line_recover: "&#A0A0A0/LoParkour recoverinventory <игрок> 񢩈- Восстановить инвентарь" + +schematic: + wand: + material: "golden_axe" + name: "&#FF4444&lПалочка схем" + lore: "&#A0A0A0ЛКМ: позиция 1||&#A0A0A0ПКМ: позиция 2" + help: + header: "B0000&lСхематики 񢩈-----------" + line_wand: "&#FF4444/LoParkour schematic wand 񢩈- &#A0A0A0Палочка выделения" + line_pos1: "&#FF4444/LoParkour schematic pos1 񢩈- &#A0A0A0Позиция 1" + line_pos2: "&#FF4444/LoParkour schematic pos2 񢩈- &#A0A0A0Позиция 2" + line_create: "&#FF4444/LoParkour create <сложность> 񢩈- &#A0A0A0Сохранить в .schem + yml" + line_create_named: "&#FF4444/LoParkour schematic create <имя> <сложность> 񢩈- &#A0A0A0С именем" + line_convert: "&#FF4444/LoParkour schematic convert 񢩈- &#A0A0A0.lpschem → .schem" + line_paste: "&#FF4444/LoParkour schematic paste <имя> 񢩈- &#A0A0A0Тестовая вставка" + line_list: "&#FF4444/LoParkour schematic list 񢩈- &#A0A0A0Список" + line_reload: "&#FF4444/LoParkour schematic reload 񢩈- &#A0A0A0Перезагрузка" + line_folder: "&#A0A0A0Папка: plugins/LoParkour/schematics/ (.nbt, .schem)" + line_docs: "&#A0A0A0Документация: plugins/LoParkour/docs/SCHEMATICS.md" + wand_given: "&#A0A0A0Палочка выдана. ЛКМ = pos1, ПКМ = pos2." + list_header: "&#A0A0A0Загруженные схемы:" + list_entry: "&#A0A0A0- &#FF4444%s 񢩈(%s, %s)" + reloaded: "&#A0A0A0Перезагружено." + pos_set: "&#A0A0A0Позиция %s: %s" + invalid_name: "&#FF4444Недопустимое имя. Только буквы, цифры, дефис и подчёркивание." + invalid_difficulty: "&#FF4444Сложность: число от 0.0 до 1.0 (напр. 0.25, 0.5, 1.0)." + selection_incomplete: "&#FF4444Сначала выделите область (палочка или pos1/pos2)." + created: "&#A0A0A0Добавлено &#FF4444%s 񢩈(сложн. %s) → &#FF4444%s" + create_failed: "&#FF4444Ошибка: %s" + convert_done: "&#A0A0A0Конвертировано: %s, ошибок: %s." + convert_truncated: "&#A0A0A0...ещё %s (см. консоль)." + not_found: "&#FF4444Схема '%s' не найдена." + pasted: "&#A0A0A0Вставлено: &#FF4444%s&#A0A0A0." ghost: saved: "�D4FFВаш призрак сохранён с результатом %s очков!" diff --git a/src/main/resources/schematics/schematics.yml b/src/main/resources/schematics/schematics.yml index e097357..8326be1 100644 --- a/src/main/resources/schematics/schematics.yml +++ b/src/main/resources/schematics/schematics.yml @@ -1,7 +1,10 @@ -# -------- Add your schematics here -------- -# Add your schematic random code or custom name here, followed by the difficulty. -# If your file is named parkour-d0f227d2, then you can either rename the file to parkour-4 ('4: 0.5') or -# just add 'd0f227d2: 0.5'. Any questions? Join the Discord. +# -------- Structure difficulty -------- +# plugins/LoParkour/schematics/ +# .nbt — structure block export +# .schem / .schematic — WorldEdit or /lp create +# .lpschem — legacy; run /lp schematic convert +# In-game: /lp schematic wand → select → /lp create +# Docs: plugins/LoParkour/docs/SCHEMATICS.md # -------- The difficulty of schematics -------- # These are represented by a number below or equal to 1.0. diff --git a/src/main/resources/sql/migration_adaptive.sql b/src/main/resources/sql/migration_adaptive.sql new file mode 100644 index 0000000..12410ca --- /dev/null +++ b/src/main/resources/sql/migration_adaptive.sql @@ -0,0 +1,19 @@ +-- Создание таблицы для адаптивных метрик +CREATE TABLE IF NOT EXISTS loparkour_player_stats ( + player_uuid VARCHAR(36) PRIMARY KEY, + skill_rating DOUBLE DEFAULT 1.0, + sessions_count INT DEFAULT 0, + total_jumps INT DEFAULT 0, + total_falls INT DEFAULT 0, + longest_streak INT DEFAULT 0, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_skill_rating (skill_rating), + INDEX idx_last_updated (last_updated) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Миграция существующих данных (если есть старая таблица) +INSERT INTO loparkour_player_stats (player_uuid, sessions_count, last_updated) +SELECT player_uuid, COUNT(*) as sessions, MAX(timestamp) as last_updated +FROM loparkour_scores +GROUP BY player_uuid +ON DUPLICATE KEY UPDATE sessions_count = VALUES(sessions_count); diff --git a/src/test/java/dev/loki/loparkour/generator/jump/JumpValidatorTest.java b/src/test/java/dev/loki/loparkour/generator/jump/JumpValidatorTest.java index e281ca9..cc5e2ef 100644 --- a/src/test/java/dev/loki/loparkour/generator/jump/JumpValidatorTest.java +++ b/src/test/java/dev/loki/loparkour/generator/jump/JumpValidatorTest.java @@ -1,4 +1,4 @@ -package dev.loki.loparkour.generator.jump; +package dev.loki.loparkour.generator.jump.calculation; import org.bukkit.Location; import org.bukkit.World; diff --git a/src/test/java/dev/loki/loparkour/ghost/GhostDataTest.java b/src/test/java/dev/loki/loparkour/ghost/GhostDataTest.java index bc63576..d93ffda 100644 --- a/src/test/java/dev/loki/loparkour/ghost/GhostDataTest.java +++ b/src/test/java/dev/loki/loparkour/ghost/GhostDataTest.java @@ -1,5 +1,7 @@ package dev.loki.loparkour.ghost; +import dev.loki.loparkour.ghost.model.GhostData; +import dev.loki.loparkour.ghost.model.GhostFrame; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; diff --git a/src/test/java/dev/loki/loparkour/style/RandomStyleTest.java b/src/test/java/dev/loki/loparkour/style/RandomStyleTest.java index 8d1ab36..a4664ec 100644 --- a/src/test/java/dev/loki/loparkour/style/RandomStyleTest.java +++ b/src/test/java/dev/loki/loparkour/style/RandomStyleTest.java @@ -1,5 +1,6 @@ package dev.loki.loparkour.style; +import dev.loki.loparkour.style.core.RandomStyle; import org.bukkit.Material; import org.junit.jupiter.api.Test; diff --git a/src/test/java/dev/loki/loparkour/world/WorldNameValidationTest.java b/src/test/java/dev/loki/loparkour/world/WorldNameValidationTest.java index d04030e..5c76ce9 100644 --- a/src/test/java/dev/loki/loparkour/world/WorldNameValidationTest.java +++ b/src/test/java/dev/loki/loparkour/world/WorldNameValidationTest.java @@ -1,5 +1,6 @@ package dev.loki.loparkour.world; +import dev.loki.loparkour.world.core.World; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse;