diff --git a/README.md b/README.md index e64036f..eff4176 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,28 @@ -# JouTak Mod Template (Java Edition) +# RouteRecorder +Сделан на основе JouTak Mod Template (Java Edition) в качестве [тестового задания](https://github.com/JouTak/TemplateMod-Java/issues/4). -Заготовка для мода на JouTak (ДжоуТек) ([joutak.ru](https://joutak.ru)) +**Minecraft 26.1.2**, **Fabric** и **Java 25**. -Шаблон рассчитан на **Minecraft 26.1.2**, **Fabric** и **Java 25**. +Клиентский мод: записывает маршрут и создаёт вейпоинты в виде стрелочек в месте, где прошёл игрок. +По умолчанию запись включается и выключается на кнопку C, запись можно поставить на паузу с помощью кнопки V. +После завершения записи нажми R, чтобы включить возврат по маршруту. Повторное нажатие R останавливает возврат. +Иди по стрелке на HUD: она показывает направление к текущей точке, расстояние и количество оставшихся точек. +При приближении на 1,5 блока цель переключается на предыдущую точку записи. Высота тоже учитывается. +У начала маршрута возврат завершается, HUD скрывается, а запись сохраняется для повторного использования. -# Требования +P выводит список записанных точек в чат. Delete полностью очищает маршрут и останавливает запись и возврат. +Все клавиши можно переназначить в настройках управления. +Новая запись заменяет старую. При смерти, выходе из мира или смене измерения маршрут очищается. -- **JDK 25** для Gradle JVM и компиляции мода -- **IntelliJ IDEA 2025.3** или новее +Интервал записи можно поменять в `config/PathTrackMod-config.json`, параметр `recordingIntervalTicks`. +По умолчанию точка сохраняется каждые 40 тиков, если игрок сдвинулся с предыдущей точки. Минимальный интервал — 1 тик. +После изменения конфига нужно перезапустить игру. -После открытия проекта выберите JDK 25 в `Settings` → `Build, Execution, Deployment` → `Build Tools` → `Gradle` → `Gradle JVM`. Без Java 25 Gradle и конфигурация `[Dev] Run Client` не запустятся. +Для установки нужны Fabric Loader и Fabric API. Mod Menu, Cloth Config и LambDynamicLights ставятся по желанию: -Gradle 9.5.1 уже включён через Gradle Wrapper, поэтому устанавливать Gradle отдельно не нужно. Для Minecraft 26.1.2 шаблон использует non-remapping Loom: зависимости Fabric подключаются через `implementation`, а конфигурация `mappings` отсутствует. +- [Mod Menu](https://modrinth.com/mod/modmenu) вместе с [Cloth Config](https://modrinth.com/mod/cloth-config) добавляют экран настройки интервала записи. +- [LambDynamicLights](https://modrinth.com/mod/lambdynamiclights) добавляет свет от точек маршрута. - -# Перед использованием - -1. Отредактируйте: - - `gradle.properties` — укажите **версию** и **название** мода, а также **ссылку на репозиторий** с вашим модом - - `src/main/java/ru/joutak/` — переименуйте **основной класс** под название вашего мода - - `src/main/resources/fabric.mod.json` — укажите **правильный путь** до основного класса мода и укажите себя как **автора** - - `gradle/libs.versions.toml` — измените **версии** **JDK**, **Minecraft**, **Fabric** и **других библиотек** по необходимости; при переходе на Minecraft `1.21.11` или старше также потребуется вернуть remapping-конфигурацию Loom - - Укажите **полный путь до папки `mods/`** тестового клиента Minecraft в переменной окружения системы `MODS_PATH` - -> [!TIP] -> Указав путь до папки `mods/` тестового клиента, можно собирать мод с помощью конфигурации `[Snapshot] Build`, тогда jar-архив с вашим модом сразу будет создаваться в нужной папке автоматически. - - -# Сборка мода - -1. Открыть проект в **IntelliJ IDEA 2025.3** или новее и дождаться завершения импорта Gradle -2. Нажать кнопку `Run` справа сверху для сборки: - - `[Dev] Run Client` — запускает Minecraft-клиент прямо из IDE с загруженным модом (для разработки) - - `[Snapshot]` — создает архив с модом в папке `mods/` тестового клиента (путь до которого нужно указать в переменной окружения `MODS_PATH`) - - `[Release]` — создает архив с модом в папке `build/` проекта - - -# Перед началом выполнения тестового задания - -> [!NOTE] -> Данный параграф предназначен для тех, кто захотел попробовать себя в роли разработчика модов для Minecraft в составе команды JouTak. - -Базовое описание, что нужно сделать: -1) Сделать собственный **fork** данного репозитория -2) Склонировать созданный репозиторий себе по **SSH** (`git clone`) -3) Открыть проект в любой удобной для вас **IDE** - -> [!TIP] -> Рекомендуем использовать **IntelliJ IDEA**, так как в ней есть полноценная поддержка **Java** со всеми фишками и плюшками, но выбор за вами - -4) Разобраться с расположением папочек и синтаксисом **Java**, затем прописать базовую логику, которая была описана в вашем задании в issues (Любая креативность поощряется) -5) Собрать мод с помощью **Gradle** - -> [!TIP] -> Конфиги уже заранее прописаны (один для разработки, один для тестовых целей и один для полноценного релиза), вам самим ничего прописывать не надо. Для разработки лучше использовать `[Dev] Run Client`, а для тестов на реальном клиенте — `[Snapshot]` версию, чтобы каждый раз не перекидывать вручную мод в папку клиента, главное не забудьте создать переменную окружения `MODS_PATH`! - -6) Запустить свой мод через конфигурацию `[Dev] Run Client` или подключить к своему локальному Minecraft-клиенту -7) Проверить, что мод работает исправно -8) Отправить **pull request** на **GitHub** и дождаться ответа - -> [!TIP] -> Если долго никто не отвечает, то можно пингануть кого-нибудь, чтобы реквест отсмотрели и приняли :) - -Если мод написан без базовых ошибок (синтаксических и т. п.), то на **GitHub**'е автоматически соберется такой же jar-архив, который будет так же работать на твоем локальном Minecraft-клиенте. - -Поначалу всё можно писать в основном классе, но потом все вещи по типу команд, обработчиков событий и различной логики нужно выделить в отдельные классы и пакеты. - -> [!IMPORTANT] -> Главное, если есть какие-то вопросы — не стесняйся спрашивать! +Для сборки нужен JDK 25. В корне проекта выполни `./gradlew build` (на Windows — `.\gradlew.bat build`). +Готовый JAR появится в `build/libs/`. diff --git a/build.gradle.kts b/build.gradle.kts index f931969..edda13b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,12 +17,42 @@ val javaVersion = libs.versions.jdk.get().toInt() repositories { mavenCentral() maven("https://maven.fabricmc.net/") { name = "Fabric" } + maven("https://maven.terraformersmc.com/releases/") { + name = "TerraformersMC" + } + + maven("https://maven.shedaniel.me/") { + name = "Shedaniel" + } + maven("https://maven.gegy.dev") { + name = "Gegy" + } } dependencies { minecraft("com.mojang:minecraft:${libs.versions.minecraft.get()}") implementation("net.fabricmc:fabric-loader:${libs.versions.fabric.loader.get()}") implementation(libs.fabric.api) + compileOnly(libs.modmenu) { + exclude(group = "net.fabricmc.fabric-api") + } + + compileOnly(libs.cloth.config) { + exclude(group = "net.fabricmc.fabric-api") + } + + localRuntime(libs.modmenu) { + exclude(group = "net.fabricmc.fabric-api") + } + + localRuntime(libs.cloth.config) { + exclude(group = "net.fabricmc.fabric-api") + } + compileOnly(libs.lambdynamiclights.api) + + localRuntime(libs.lambdynamiclights.runtime) { + exclude(group = "net.fabricmc.fabric-api") + } } java { diff --git a/gradle.properties b/gradle.properties index 75dbe5b..8e45e23 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,10 @@ # Mod properties -repo=https://github.com/JouTak/TemplateMod-Java +repo=https://github.com/UrSick/TemplateMod-Java-WeNeedFood group=ru.joutak mod_name=TemplateMod-Java mod_id=templatemod -version=1.0.0 +version=1.5 # Gradle properties org.gradle.parallel=true +org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fca8bf0..aa8a229 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,9 +4,16 @@ minecraft = "26.1.2" fabric-loader = "0.19.5" fabric-api = "0.155.2+26.1.2" loom = "1.17.20" +modmenu = "18.0.0" +cloth-config = "26.1.154" +lambdynamiclights = "4.11.2+26.1.2" [libraries] fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } +modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" } +cloth-config = { module = "me.shedaniel.cloth:cloth-config-fabric", version.ref = "cloth-config" } +lambdynamiclights-api = { module = "dev.lambdaurora.lambdynamiclights:lambdynamiclights-api", version.ref = "lambdynamiclights" } +lambdynamiclights-runtime = { module = "dev.lambdaurora.lambdynamiclights:lambdynamiclights-runtime", version.ref = "lambdynamiclights" } [plugins] fabric-loom = { id = "net.fabricmc.fabric-loom", version.ref = "loom" } diff --git a/src/main/java/ru/joutak/templatemod/TemplateMod.java b/src/main/java/ru/joutak/templatemod/TemplateMod.java index 670de0e..8a919c4 100644 --- a/src/main/java/ru/joutak/templatemod/TemplateMod.java +++ b/src/main/java/ru/joutak/templatemod/TemplateMod.java @@ -18,6 +18,6 @@ public static TemplateMod getInstance() { @Override public void onInitialize() { instance = this; - LOGGER.info("Mod {} initialized!", MOD_ID); + LOGGER.info("Мод {} загружен", MOD_ID); } } diff --git a/src/main/java/ru/joutak/templatemod/client/RecordingHud.java b/src/main/java/ru/joutak/templatemod/client/RecordingHud.java new file mode 100644 index 0000000..28f1865 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/client/RecordingHud.java @@ -0,0 +1,41 @@ +package ru.joutak.templatemod.client; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.resources.Identifier; +import ru.joutak.templatemod.TemplateMod; +import ru.joutak.templatemod.client.RoutRecorder.RecordingState; +public class RecordingHud { + private static final Identifier RECORDING_TEXTURE = + Identifier.fromNamespaceAndPath( + TemplateMod.MOD_ID, + "textures/recording.png" + ); + private static final Identifier PAUSED_TEXTURE = + Identifier.fromNamespaceAndPath( + TemplateMod.MOD_ID, + "textures/pause.png" + ); + public static void draw(GuiGraphicsExtractor graphics, RecordingState state) { + if (state == RecordingState.STOPPED) { + return; + } + + Identifier texture; + + if (state == RecordingState.RECORDING) { + texture = RECORDING_TEXTURE; + } else { + texture = PAUSED_TEXTURE; + } + + graphics.blit( + RenderPipelines.GUI_TEXTURED, + texture, + 8, graphics.guiHeight() - 24, + 0f, 0f, + 16, 16, + 32, 32, + 32, 32 + ); + } +} diff --git a/src/main/java/ru/joutak/templatemod/client/ReturnHUD.java b/src/main/java/ru/joutak/templatemod/client/ReturnHUD.java new file mode 100644 index 0000000..bb393db --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/client/ReturnHUD.java @@ -0,0 +1,49 @@ +package ru.joutak.templatemod.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import ru.joutak.templatemod.TemplateMod; + +import java.util.Locale; + +public class ReturnHUD { + private static final Identifier ARROW_TEXTURE = Identifier.fromNamespaceAndPath( + TemplateMod.MOD_ID, "textures/waypoint.png" + ); + + public static void draw(GuiGraphicsExtractor graphics, float angleDegrees, double distance, int remainingPoints) { + var matrices = graphics.pose(); + + matrices.pushMatrix(); + matrices.translate(40f, graphics.guiHeight() - 56f); + matrices.rotate((float) Math.toRadians(angleDegrees + 180f)); + + graphics.blit( + RenderPipelines.GUI_TEXTURED, + ARROW_TEXTURE, + -16, -16, + 0f, 0f, + 32, 32, + 32, 32, + 32, 32 + ); + + matrices.popMatrix(); + + graphics.text( + Minecraft.getInstance().font, + Component.translatable("hud.templatemod.return_distance", String.format(Locale.ROOT, "%.1f", distance)), + 8, graphics.guiHeight() - 36, + 0xFFFFFFFF + ); + graphics.text( + Minecraft.getInstance().font, + Component.translatable("hud.templatemod.return_progress", remainingPoints), + 8, graphics.guiHeight() - 24, + 0xFFFFFFFF + ); + } +} diff --git a/src/main/java/ru/joutak/templatemod/client/RoutLighting.java b/src/main/java/ru/joutak/templatemod/client/RoutLighting.java new file mode 100644 index 0000000..4837923 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/client/RoutLighting.java @@ -0,0 +1,16 @@ +package ru.joutak.templatemod.client; +import net.minecraft.world.phys.Vec3; +import java.util.List; +import java.util.function.Consumer; + +public class RoutLighting { + private static Consumer> updater = points -> {}; + + public static void setUpdater(Consumer> newUpdater) { + updater = newUpdater; + } + + public static void update(List points) { + updater.accept(points); + } +} diff --git a/src/main/java/ru/joutak/templatemod/client/RoutRecorder.java b/src/main/java/ru/joutak/templatemod/client/RoutRecorder.java new file mode 100644 index 0000000..58b171e --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/client/RoutRecorder.java @@ -0,0 +1,66 @@ +package ru.joutak.templatemod.client; +import net.minecraft.world.phys.Vec3; +import java.util.ArrayList; +import java.util.List; + +public class RoutRecorder { + private final int recordingIntervalTicks; + public RoutRecorder(int recordingIntervalTicks) { + this.recordingIntervalTicks = recordingIntervalTicks; + } + public enum RecordingState { + STOPPED, + RECORDING, + PAUSED + } + public void start(Vec3 position) { + points.clear(); + points.add(position); + this.state = RecordingState.RECORDING; + this.ticksSinceLastPoint = 0; + } + + public void tick(Vec3 position) { + if (this.state != RecordingState.RECORDING) { + return; + } + this.ticksSinceLastPoint ++; + if (this.ticksSinceLastPoint >= this.recordingIntervalTicks) { + if (!points.getLast().equals(position)) { + points.add(position); + } + this.ticksSinceLastPoint = 0; + } + } + public void toggleRecording(Vec3 position) { + if (this.state == RecordingState.STOPPED) { + start(position); + } + else if (this.state == RecordingState.RECORDING || this.state == RecordingState.PAUSED) { + this.state = RecordingState.STOPPED; + this.ticksSinceLastPoint = 0; + } + } + public void togglePause() { + if (this.state == RecordingState.RECORDING) { + this.state = RecordingState.PAUSED; + } else if (this.state == RecordingState.PAUSED) { + this.state = RecordingState.RECORDING; + } + } + public void clear() { + this.points.clear(); + this.state = RecordingState.STOPPED; + this.ticksSinceLastPoint = 0; + } + public List getPoints() { + return List.copyOf(points); + } + public RecordingState getState() { + return state; + } + private RecordingState state = RecordingState.STOPPED; + private int ticksSinceLastPoint = 0; + private ArrayList points = new ArrayList<>(); + +} diff --git a/src/main/java/ru/joutak/templatemod/client/RouteMarkers.java b/src/main/java/ru/joutak/templatemod/client/RouteMarkers.java new file mode 100644 index 0000000..98b6358 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/client/RouteMarkers.java @@ -0,0 +1,64 @@ +package ru.joutak.templatemod.client; +import java.util.List; +import net.minecraft.resources.Identifier; +import org.joml.Vector3f; +import ru.joutak.templatemod.TemplateMod; +import net.minecraft.world.phys.Vec3; +import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.util.LightCoordsUtil; +import net.minecraft.util.Util; + +public class RouteMarkers { + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath( + TemplateMod.MOD_ID, "textures/waypoint.png" + ); + private static List renderPoints = List.of(); + private static double bobOffset = 0.0; + public static void extract(List points) { + renderPoints = List.copyOf(points); + double seconds = Util.getMillis() / 1000.0; + double period = 4.0; + double amplitude = 0.1; + bobOffset = Math.sin(seconds * 2 * Math.PI / period) * amplitude; + } + public static void draw(LevelRenderContext context) { + Vec3 camera = context.levelState().cameraRenderState.pos; + PoseStack matrices = context.poseStack(); + for (Vec3 point : renderPoints) { + double x = point.x - camera.x; + double y = point.y - camera.y; + double z = point.z - camera.z; + matrices.pushPose(); + matrices.translate(x, y + 0.5 + bobOffset, z); + matrices.mulPose(context.levelState().cameraRenderState.orientation); + + context.submitNodeCollector().submitCustomGeometry( + matrices, + RenderTypes.entityTranslucentEmissive(TEXTURE), + (pose, buffer) -> { + vertex(buffer, pose, -0.25f, -0.25f, 0f, 1f); + vertex(buffer, pose, 0.25f, -0.25f, 1f, 1f); + vertex(buffer, pose, 0.25f, 0.25f, 1f, 0f); + vertex(buffer, pose, -0.25f, 0.25f, 0f, 0f); + + } + ); + matrices.popPose(); + } + } + private static void vertex( + VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float u, float v + ) { + buffer.addVertex(pose,x,y,0f) + .setColor(255,255,255,255) + .setUv(u,v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(LightCoordsUtil.FULL_BRIGHT) + .setNormal(pose,0.0F,0.0F, 1f); + } +} diff --git a/src/main/java/ru/joutak/templatemod/client/RouteNavigator.java b/src/main/java/ru/joutak/templatemod/client/RouteNavigator.java new file mode 100644 index 0000000..8be0195 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/client/RouteNavigator.java @@ -0,0 +1,49 @@ +package ru.joutak.templatemod.client; + +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +public class RouteNavigator { + private static final double REACH_RADIUS = 1.5; + + private List points = List.of(); + private int targetIndex = -1; + + public boolean start(List route) { + this.points = List.copyOf(route); + this.targetIndex = this.points.size() - 1; + return isActive(); + } + + public boolean tick(Vec3 position) { + if (!isActive()) { + return false; + } + + while (isActive() + && position.distanceToSqr(this.points.get(this.targetIndex)) <= REACH_RADIUS * REACH_RADIUS) { + this.targetIndex--; + } + + // Сообщаем о завершении только в тот тик, когда достигнута первая точка. + return !isActive(); + } + + public void stop() { + this.points = List.of(); + this.targetIndex = -1; + } + + public boolean isActive() { + return this.targetIndex >= 0; + } + + public Vec3 getTarget() { + return isActive() ? this.points.get(this.targetIndex) : null; + } + + public int getRemainingPointCount() { + return this.targetIndex + 1; + } +} diff --git a/src/main/java/ru/joutak/templatemod/client/TemplateModClient.java b/src/main/java/ru/joutak/templatemod/client/TemplateModClient.java index 0a91541..9a17817 100644 --- a/src/main/java/ru/joutak/templatemod/client/TemplateModClient.java +++ b/src/main/java/ru/joutak/templatemod/client/TemplateModClient.java @@ -1,12 +1,176 @@ package ru.joutak.templatemod.client; - +import ru.joutak.templatemod.config.ConfigLoader; +import ru.joutak.templatemod.config.ModConfig; import net.fabricmc.api.ClientModInitializer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.Vec3; import ru.joutak.templatemod.TemplateMod; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.resources.Identifier; +import com.mojang.blaze3d.platform.InputConstants; +import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.network.chat.Component; +import ru.joutak.templatemod.client.RoutRecorder.RecordingState; +import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; +import net.minecraft.client.multiplayer.ClientLevel; public class TemplateModClient implements ClientModInitializer { @Override public void onInitializeClient() { - TemplateMod.LOGGER.info("Client side of mod {} initialized!", TemplateMod.MOD_ID); + ModConfig config = ConfigLoader.load(); + + this.recorder = new RoutRecorder( + config.getRecordingIntervalTicks() + ); + TemplateMod.LOGGER.info("Клиентская часть мода {} запущена", TemplateMod.MOD_ID); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (this.routeLevel != client.level) { + this.recorder.clear(); + this.navigator.stop(); + RoutLighting.update(this.recorder.getPoints()); + this.routeLevel = client.level; + } + if (client.player == null || client.isPaused()) { + return; + } + if (!client.player.isAlive()) { + recorder.clear(); + navigator.stop(); + RoutLighting.update(recorder.getPoints()); + return; + } + while (this.StartRecording.consumeClick()) { + navigator.stop(); + if (recorder.getState() == RecordingState.STOPPED) { + client.player.sendSystemMessage(Component.translatable("message.templatemod.recording_started")); + } else { + client.player.sendSystemMessage(Component.translatable("message.templatemod.recording_stopped")); + if (this.startReturn.isUnbound()) { + client.player.sendSystemMessage(Component.translatable("message.templatemod.bind_return_key")); + } else { + client.player.sendSystemMessage(Component.translatable( + "message.templatemod.return_hint", + this.startReturn.getTranslatedKeyMessage() + )); + } + } + recorder.toggleRecording(client.player.position()); + } + while (this.ShowList.consumeClick()) { + if (recorder.getPoints().isEmpty()){ + client.player.sendSystemMessage(Component.translatable("message.templatemod.points_empty")); + } + else { + for (Vec3 pos : recorder.getPoints()){ + client.player.sendSystemMessage(Component.literal(pos.toString())); + } + } + } + while (this.pauseRecording.consumeClick()) { + recorder.togglePause(); + } + while (this.startReturn.consumeClick()) { + if (navigator.isActive()) { + navigator.stop(); + client.player.sendSystemMessage(Component.translatable("message.templatemod.return_stopped")); + } else if (recorder.getState() != RecordingState.STOPPED) { + client.player.sendSystemMessage(Component.translatable("message.templatemod.finish_recording")); + } else if (navigator.start(recorder.getPoints())) { + client.player.sendSystemMessage(Component.translatable("message.templatemod.return_started")); + } else { + client.player.sendSystemMessage(Component.translatable("message.templatemod.no_route")); + } + } + while (this.clearRoute.consumeClick()) { + recorder.clear(); + navigator.stop(); + client.player.sendSystemMessage(Component.translatable("message.templatemod.route_cleared")); + } + recorder.tick(client.player.position()); + if (navigator.tick(client.player.position())) { + client.player.sendSystemMessage(Component.translatable("message.templatemod.return_finished")); + } + RoutLighting.update(recorder.getPoints()); + }); + LevelRenderEvents.END_EXTRACTION.register(context -> { + RouteMarkers.extract(recorder.getPoints()); + }); + LevelRenderEvents.COLLECT_SUBMITS.register(RouteMarkers::draw); + HudElementRegistry.attachElementBefore( + VanillaHudElements.CHAT, + Identifier.fromNamespaceAndPath(TemplateMod.MOD_ID, "recording_hud"), + (graphics, deltaTracker) -> + RecordingHud.draw(graphics, recorder.getState()) + ); + HudElementRegistry.attachElementBefore( + VanillaHudElements.CHAT, + Identifier.fromNamespaceAndPath(TemplateMod.MOD_ID, "return_hud"), + (graphics, deltaTracker) -> { + var client = Minecraft.getInstance(); + if (client.player == null || !navigator.isActive()) { + return; + } + Vec3 target = navigator.getTarget(); + Vec3 position = client.player.position(); + double dx = target.x - position.x; + double dz = target.z - position.z; + float angleDegrees = 0f; + if (dx * dx + dz * dz > 1.0E-6) { + float targetYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + angleDegrees = Mth.wrapDegrees(targetYaw - client.player.getYRot()); + } + + ReturnHUD.draw(graphics, angleDegrees, position.distanceTo(target), navigator.getRemainingPointCount()); + } + ); } + private RoutRecorder recorder; + private final RouteNavigator navigator = new RouteNavigator(); + private ClientLevel routeLevel; + KeyMapping.Category Pathtrack = KeyMapping.Category.register( + Identifier.fromNamespaceAndPath(TemplateMod.MOD_ID, "controls") + ); + KeyMapping StartRecording = KeyMappingHelper.registerKeyMapping( + new KeyMapping( + "key.PathtrackMod.start-recording", + InputConstants.Type.KEYSYM, + InputConstants.KEY_C, + this.Pathtrack + )); + KeyMapping pauseRecording = KeyMappingHelper.registerKeyMapping( + new KeyMapping( + "key.templatemod.pause_recording", + InputConstants.Type.KEYSYM, + InputConstants.KEY_V, + this.Pathtrack + ) + ); + KeyMapping ShowList = KeyMappingHelper.registerKeyMapping( + new KeyMapping("key.templatemod.show_list", + InputConstants.Type.KEYSYM, + InputConstants.KEY_P, + this.Pathtrack + )); + KeyMapping clearRoute = KeyMappingHelper.registerKeyMapping( + new KeyMapping( + "key.templatemod.clear_route", + InputConstants.Type.KEYSYM, + InputConstants.KEY_DELETE, + this.Pathtrack + ) + ); + KeyMapping startReturn = KeyMappingHelper.registerKeyMapping( + new KeyMapping( + "key.templatemod.start_return", + InputConstants.Type.KEYSYM, + InputConstants.KEY_R, + this.Pathtrack + ) + ); } diff --git a/src/main/java/ru/joutak/templatemod/compat/DynamicLightsIntegration.java b/src/main/java/ru/joutak/templatemod/compat/DynamicLightsIntegration.java new file mode 100644 index 0000000..e8cbc41 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/compat/DynamicLightsIntegration.java @@ -0,0 +1,49 @@ +package ru.joutak.templatemod.compat; + +import dev.lambdaurora.lambdynlights.api.DynamicLightsContext; +import dev.lambdaurora.lambdynlights.api.DynamicLightsInitializer; +import ru.joutak.templatemod.TemplateMod; +import dev.lambdaurora.lambdynlights.api.behavior.DynamicLightBehaviorManager; +import net.minecraft.world.phys.Vec3; +import ru.joutak.templatemod.client.RoutLighting; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.HashSet; + +public class DynamicLightsIntegration implements DynamicLightsInitializer { + private final Map activeLights = new HashMap<>(); + @Override + public void onInitializeDynamicLights(DynamicLightsContext context) { + TemplateMod.LOGGER.info("Поддержка LambDynamicLights подключена"); + + var lights = context.dynamicLightBehaviorManager(); + + RoutLighting.setUpdater(points -> updateLights(points, lights)); + } + private void updateLights( + List points, + DynamicLightBehaviorManager lights + ) { + var currentPoints = new HashSet<>(points); + + for (Vec3 oldPoint : List.copyOf(this.activeLights.keySet())) { + if (!currentPoints.contains(oldPoint)) { + WaypointLight light = this.activeLights.remove(oldPoint); + lights.remove(light); + } + } + + for (Vec3 point : currentPoints) { + if (!this.activeLights.containsKey(point)) { + WaypointLight light = new WaypointLight( + point.add(0.0, 0.5, 0.0), + 10 + ); + + lights.add(light); + this.activeLights.put(point, light); + } + } + } +} diff --git a/src/main/java/ru/joutak/templatemod/compat/ModConfigScreen.java b/src/main/java/ru/joutak/templatemod/compat/ModConfigScreen.java new file mode 100644 index 0000000..1e66df8 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/compat/ModConfigScreen.java @@ -0,0 +1,40 @@ +package ru.joutak.templatemod.compat; + +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import ru.joutak.templatemod.config.ConfigLoader; +import ru.joutak.templatemod.config.ModConfig; + +public class ModConfigScreen { + public static Screen create(Screen parent) { + ModConfig config = ConfigLoader.load(); + + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(Component.translatable("config.templatemod.title")); + + var category = builder.getOrCreateCategory( + Component.translatable("config.templatemod.category.recording") + ); + + category.addEntry( + builder.entryBuilder() + .startIntField( + Component.translatable("config.templatemod.recording_interval_ticks"), + config.getRecordingIntervalTicks() + ) + .setMin(1) + .setDefaultValue( + new ModConfig().getRecordingIntervalTicks() + ) + .setSaveConsumer(config::setRecordingIntervalTicks) + .requireRestart() + .build() + ); + + builder.setSavingRunnable(() -> ConfigLoader.save(config)); + + return builder.build(); + } +} diff --git a/src/main/java/ru/joutak/templatemod/compat/ModMenuIntegration.java b/src/main/java/ru/joutak/templatemod/compat/ModMenuIntegration.java new file mode 100644 index 0000000..afcb3ca --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/compat/ModMenuIntegration.java @@ -0,0 +1,16 @@ +package ru.joutak.templatemod.compat; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import net.fabricmc.loader.api.FabricLoader; + +public class ModMenuIntegration implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + if (!FabricLoader.getInstance().isModLoaded("cloth-config")) { + return ModMenuApi.super.getModConfigScreenFactory(); + } + + return parent -> ModConfigScreen.create(parent); + } +} diff --git a/src/main/java/ru/joutak/templatemod/compat/WaypointLight.java b/src/main/java/ru/joutak/templatemod/compat/WaypointLight.java new file mode 100644 index 0000000..f4440ab --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/compat/WaypointLight.java @@ -0,0 +1,52 @@ +package ru.joutak.templatemod.compat; +import dev.lambdaurora.lambdynlights.api.behavior.DynamicLightBehavior; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Range; + +public class WaypointLight implements DynamicLightBehavior{ + private final Vec3 position; + private final int luminance; + private boolean changed = true; + + public WaypointLight(Vec3 position, int luminance) { + if (luminance < 0 || luminance > 15) { + throw new IllegalArgumentException("Яркость должна быть от 0 до 15"); + } + + this.position = position; + this.luminance = luminance; + } + + @Override + public @Range(from = 0L, to = 15L) double lightAtPos(BlockPos pos, double falloffRatio) { + double dx = pos.getX() + 0.5 - this.position.x; + double dy = pos.getY() + 0.5 - this.position.y; + double dz = pos.getZ() + 0.5 - this.position.z; + + double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + return Math.max(this.luminance - distance * falloffRatio, 0.0); + } + + @Override + public BoundingBox getBoundingBox() { + BlockPos block = BlockPos.containing(this.position); + + return new BoundingBox( + block.getX(), + block.getY(), + block.getZ(), + block.getX() + 1, + block.getY() + 1, + block.getZ() + 1 + ); + } + + @Override + public boolean hasChanged() { + boolean result = this.changed; + this.changed = false; + return result; + } +} diff --git a/src/main/java/ru/joutak/templatemod/config/ConfigLoader.java b/src/main/java/ru/joutak/templatemod/config/ConfigLoader.java new file mode 100644 index 0000000..c9a86b9 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/config/ConfigLoader.java @@ -0,0 +1,60 @@ +package ru.joutak.templatemod.config; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import net.fabricmc.loader.api.FabricLoader; +import ru.joutak.templatemod.TemplateMod; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +public class ConfigLoader { + private static final Gson GSON = new Gson(); + + public static ModConfig load() { + Path path = FabricLoader.getInstance() + .getConfigDir() + .resolve("PathTrackMod-config.json"); + + ModConfig defaults = new ModConfig(); + + try { + if (Files.notExists(path)) { + Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(defaults)); + return defaults; + } + + String json = Files.readString(path); + ModConfig config = GSON.fromJson(json, ModConfig.class); + + if (config == null || config.getRecordingIntervalTicks() < 1) { + TemplateMod.LOGGER.warn( + "В конфиге {} неверный интервал записи. Использую {} тиков.", + path, defaults.getRecordingIntervalTicks() + ); + return defaults; + } + + return config; + } catch (IOException | JsonParseException e) { + TemplateMod.LOGGER.warn( + "Не удалось прочитать конфиг {}. Использую стандартные настройки.", path, e + ); + return defaults; + } + } + + public static void save(ModConfig config) { + Path path = FabricLoader.getInstance() + .getConfigDir() + .resolve("PathTrackMod-config.json"); + + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(config)); + } catch (IOException e) { + TemplateMod.LOGGER.error("Не удалось сохранить конфиг {}.", path, e); + } + + } +} diff --git a/src/main/java/ru/joutak/templatemod/config/ModConfig.java b/src/main/java/ru/joutak/templatemod/config/ModConfig.java new file mode 100644 index 0000000..e5606f7 --- /dev/null +++ b/src/main/java/ru/joutak/templatemod/config/ModConfig.java @@ -0,0 +1,16 @@ +package ru.joutak.templatemod.config; + +public class ModConfig { + private int recordingIntervalTicks = 40; + + public void setRecordingIntervalTicks(int value) { + if (value < 1) { + throw new IllegalArgumentException("Интервал записи должен быть не меньше одного тика"); + } + + this.recordingIntervalTicks = value; + } + public int getRecordingIntervalTicks() { + return recordingIntervalTicks; + } +} diff --git a/src/main/resources/assets/templatemod/lang/en_us.json b/src/main/resources/assets/templatemod/lang/en_us.json new file mode 100644 index 0000000..feb176b --- /dev/null +++ b/src/main/resources/assets/templatemod/lang/en_us.json @@ -0,0 +1,25 @@ +{ + "key.category.templatemod.controls": "Route", + "key.PathtrackMod.start-recording": "Start / stop recording", + "key.templatemod.show_list": "Show recorded points", + "key.templatemod.pause_recording": "Pause / resume recording", + "key.templatemod.clear_route": "Clear route", + "key.templatemod.start_return": "Start / stop return guidance", + "message.templatemod.recording_started": "Recording started!", + "message.templatemod.recording_stopped": "Recording finished", + "message.templatemod.return_hint": "Press %s to start heading back", + "message.templatemod.bind_return_key": "Set a return key in the controls settings", + "message.templatemod.points_empty": "No points recorded", + "message.templatemod.route_cleared": "Route cleared", + "message.templatemod.return_started": "Heading back", + "message.templatemod.return_stopped": "Return guidance stopped", + "message.templatemod.no_route": "Record a route first", + "message.templatemod.finish_recording": "Finish recording first", + "message.templatemod.return_finished": "You're back at the start of the route", + "hud.templatemod.return_distance": "%s blocks", + "hud.templatemod.return_progress": "Points left: %s", + "config.templatemod.title": "Route settings", + "config.templatemod.category.recording": "Recording", + "config.templatemod.recording_interval_ticks": "Recording interval (ticks)", + "modmenu.descriptionTranslation.templatemod": "Records your route, marks it with arrows, and helps you find your way back." +} diff --git a/src/main/resources/assets/templatemod/lang/ru_ru.json b/src/main/resources/assets/templatemod/lang/ru_ru.json new file mode 100644 index 0000000..98c1234 --- /dev/null +++ b/src/main/resources/assets/templatemod/lang/ru_ru.json @@ -0,0 +1,25 @@ +{ + "key.category.templatemod.controls": "Маршрут", + "key.PathtrackMod.start-recording": "Начать / закончить запись", + "key.templatemod.show_list": "Показать список точек", + "key.templatemod.pause_recording": "Пауза / продолжить запись", + "key.templatemod.clear_route": "Очистить маршрут", + "key.templatemod.start_return": "Начать / остановить возвращение", + "message.templatemod.recording_started": "Запись начата!", + "message.templatemod.recording_stopped": "Запись завершена", + "message.templatemod.return_hint": "Нажмите %s, чтобы начать возвращение", + "message.templatemod.bind_return_key": "Назначьте кнопку возвращения в настройках управления", + "message.templatemod.points_empty": "Список пуст", + "message.templatemod.route_cleared": "Маршрут очищен", + "message.templatemod.return_started": "Возвращение начато", + "message.templatemod.return_stopped": "Возвращение остановлено", + "message.templatemod.no_route": "Сначала запиши маршрут", + "message.templatemod.finish_recording": "Сначала заверши запись", + "message.templatemod.return_finished": "Ты вернулся к началу маршрута", + "hud.templatemod.return_distance": "%s бл.", + "hud.templatemod.return_progress": "Осталось точек: %s", + "config.templatemod.title": "Настройки маршрута", + "config.templatemod.category.recording": "Запись", + "config.templatemod.recording_interval_ticks": "Интервал записи, тики", + "modmenu.descriptionTranslation.templatemod": "Записывает маршрут, ставит стрелочки и помогает вернуться обратно." +} diff --git a/src/main/resources/assets/templatemod/textures/icon.png b/src/main/resources/assets/templatemod/textures/icon.png new file mode 100644 index 0000000..eb0160e Binary files /dev/null and b/src/main/resources/assets/templatemod/textures/icon.png differ diff --git a/src/main/resources/assets/templatemod/textures/pause.png b/src/main/resources/assets/templatemod/textures/pause.png new file mode 100644 index 0000000..a395f15 Binary files /dev/null and b/src/main/resources/assets/templatemod/textures/pause.png differ diff --git a/src/main/resources/assets/templatemod/textures/recording.png b/src/main/resources/assets/templatemod/textures/recording.png new file mode 100644 index 0000000..e6484a2 Binary files /dev/null and b/src/main/resources/assets/templatemod/textures/recording.png differ diff --git a/src/main/resources/assets/templatemod/textures/waypoint.png b/src/main/resources/assets/templatemod/textures/waypoint.png new file mode 100644 index 0000000..d5fbfd6 Binary files /dev/null and b/src/main/resources/assets/templatemod/textures/waypoint.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 2be069a..33aceec 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,22 +3,29 @@ "id": "${MOD_ID}", "version": "${VERSION}", "name": "${MOD_NAME}", - "description": "Заготовка для мода на JouTak (joutak.ru)", + "icon": "assets/templatemod/textures/icon.png", + "description": "Records your route, marks it with arrows, and helps you find your way back.", "authors": [ - "JouTak" + "WeNeedFood" ], "contact": { "homepage": "https://joutak.ru", "sources": "${WEBSITE}" }, "license": "MIT", - "environment": "*", + "environment": "client", "entrypoints": { "main": [ "ru.joutak.templatemod.TemplateMod" ], "client": [ "ru.joutak.templatemod.client.TemplateModClient" + ], + "modmenu": [ + "ru.joutak.templatemod.compat.ModMenuIntegration" + ], + "lambdynlights:initializer": [ + "ru.joutak.templatemod.compat.DynamicLightsIntegration" ] }, "mixins": [],