diff --git a/TODO.md b/TODO.md
index 2ff03e1..bad0021 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,7 +13,7 @@
## Правила (для мода, продублированы в его TODO)
- KISS / DRY / SOLID
- Макс. 200 строк на файл (фасады-делегаты исключение)
-- Макс. 3 файла на папку
+- Макс. 4 файла на папку
- Никакого статика, никаких this-escape, никаких мёртвых миксинов
- Без пустышек; каждый этап компилируется (`./gradlew build`)
- Тесты обязательны для новых модулей (JUnit)
diff --git a/combatant-client-26.2/TODO.md b/combatant-client-26.2/TODO.md
index 1a7a576..725a713 100644
--- a/combatant-client-26.2/TODO.md
+++ b/combatant-client-26.2/TODO.md
@@ -5,7 +5,7 @@
## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат)
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга
- **Макс. 200 строк на файл** (было >1500) — превышение = разбить на модули
-- **Макс. 3 файла на папку** — больше = новая подпапка по смыслу
+- **Макс. 4 файла на папку** — больше = новая подпапка по смыслу
- **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле
- **Никаких this-escape** — конструктор не публикует `this`
- **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel
@@ -148,4 +148,68 @@
- [ ] Юнит-тесты: EventBus, конфиг, утилиты (игровые классы — mock)
- [ ] По мере переписи модулей — тест на каждый
+## Фаза 7: QoL-модули из Delta (легит) — ПЛАН, обсуждаем
+> Референс: `ref/delta-26.2-main` (чит-клиент, код обфусцирован — берём только идеи,
+> переписываем под наши правила; у Delta 114 модулей, у нас 63).
+> Уже перенесено: SeeInvisibles→InvisESP, HandsShader→Chams/Noise, AtmoDawnFog,
+> ShulkerPreview, Animations, ItemPhysic (из WorldTweaks), HoldMyItems (фикс ordinal-бага,
+> математика в EaseCurves). Реф-исходники: ref/hmi-src, ref/soupvisuals-src.
+
+### Решения по обсуждению (2026-09-05)
+- [x] Wasted — НЕ делаем
+- [x] TapeMouse — делаем (пользователь: AFK-фермы, не чит-софт)
+- [x] DeathCoords — делаем
+- [x] ItemScroller / Arrows — НЕ делаем
+- [x] ColorPlus ≈ наш PostFX — PostFX восстановлен и починен (дефолтный пресет был near-identity → добавлен Cinematic + тесты)
+- [x] Правило папок: максимум 4 файла (было 3)
+
+### Перенос (список пользователя)
+- [ ] **StreamerMode** (misc, ~80 у них) — при записи/стриме скрывает: ники игроков,
+ имена друзей, названия серверов («Анархия-N») в чате/таб-листе/скорборде; скрытие скинов.
+ Интеграция: наши BetterChat + CustomTabList + Scoreboard-виджет + рендер скинов.
+- [x] **LockSlot** (player, ~36) — запрет выброса из выбранных слотов 1–9 (анти-случайный Q),
+ опция «только в PVP», предупреждение в чат. Точка: перехват drop-пакета.
+- [x] **AutoRespawn** (player, ~28) — авто-возрождение после смерти (tick на death screen).
+- [ ] **AutoLeave** (player, ~61) — выход в хаб по триггерам: малое ХП, игроки рядом (N, радиус), задержка.
+- [ ] **AutoAccept** (player, ~67) — авто-принятие запросов: клан/ТП; опция «только от друзей»
+ через наш PlayerRelations. Точка: перехват chat-пакетов.
+- [ ] **SoundReducer** (player, ~42) — приглушение выбранных звуков/категорий (громкость 0..N),
+ в отличие от нашего NoSound (он только вкл/выкл всего). Вопрос: новый модуль или развитие NoSound.
+- [ ] **Wasted** (misc, ~100) — ОТМЕНЕНО (решение пользователя)
+- [ ] **ThirdPerson** (player, ~77) — свободный обзор от 3-го лица без поворота движения
+ (по нажатию/зажатию бинда). Не дублирует Freecam (он про отдельную камеру-сущность).
+ Точка: наш CameraMixin.
+- [ ] **UseTracker** (player, ~86) — уведомления об использованиях: тотем, зелья, предметы.
+ Вывод через наш HudNotifier. Точка: пакеты + инвентарь.
+- [x] **NoServerPack** (misc) — у Delta ПУСТЫШКА (пустой класс). Сделаем по-настоящему:
+ авто-отклонение принудительного серверного ресурспака (спрашивать/отклонять всегда).
+ Точка: наш ClientCommonPacketListenerImplMixin.
+- [x] **NoFriendDamage** (combat, ~19) — отмена атаки, если цель — друг (PlayerRelations).
+ Точка: перехват атаки (AttackEvent у них; у нас миксин на attack/interact-пакет).
+- [x] **TapeMouse** (combat, ~66) — автоклик по таймеру. ⚠️ ПРОТИВОРЕЧИТ легит-политике Фазы 3.5
+ (тот же класс, что удалённые AutoAttack/TriggerBot). По умолчанию НЕ делаем / делаем
+ легит-вариант «напоминание о клике» без автоматического клика — РЕШИТЬ.
+
+### Кандидаты из отчёта (не в списке пользователя — обсудить)
+- [x] **DeathCoords** (~27) — координаты последней смерти в чат. Дёшево, полезно.
+- [ ] **ItemScroller** — перекладывание предметов прокруткой в инвентарях. Крупный QoL (~200+).
+- [ ] **ColorPlus** — ⚠️ это по сути наш удалённый PostFX (экспозиция/гамма/контраст/насыщенность/
+ виньетка/резкость/LUT). Восстанавливать под новым именем или забыть — РЕШИТЬ.
+- [ ] **Arrows / off-screen индикаторы** — дополнение к Pointers (у нас их нет вообще).
+
+### Проверка после каждого модуля: `./gradlew build` ✓, тесты на чистую логику ✓
+
+## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН
+> Правило ≤200 уже есть (Фаза 1–3), но долг: ~240 файлов >200 строк.
+> У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) /
+> inject(accessors, invokers, mixin). Наша пакетная структура сопоставима — проблема в размере файлов.
+
+- [ ] Топ-гигантов к разбивке (после Фазы 3): LoVisualRenderPipelines 1527,
+ DraggableHudElementRegistry 1449, Predictions 1363, DynamicIsland 1254, Module 1181,
+ HudNotifier 1116, AltManager 1115, Triangulator 1102, TargetESP 1061, UiPrimitiveRenderer 1042
+- [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤200, 0 изменений вызывающих,
+ `./gradlew build` после каждого; по мере касания файлов в Фазе 7 — разбивать на ходу
+- [ ] Facades >200 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам)
+- [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша
+
### Проверка: `./gradlew test` зелёный
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java
index 9274a83..752ba72 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java
@@ -195,7 +195,8 @@ public abstract class DraggableHudElement extends BaseHudElement {
);
}
- void markRendered(long order) {
+ /** public: вызывается хелперами реестра в подпакетах registry/render (JLS: package-private недоступен из подпакета). */
+ public void markRendered(long order) {
this.renderOrder = order;
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java
index 9c52001..f38d689 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java
@@ -7,98 +7,44 @@
package dev.loki.lovisual.features.gui.hud.draggable;
-import dev.loki.lovisual.features.theme.Theme;
-import dev.loki.lovisual.features.gui.hud.*;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore;
+import dev.loki.lovisual.features.gui.hud.draggable.render.RegistryRenderPipeline;
+import dev.loki.lovisual.features.gui.hud.draggable.render.RegistryWidgetRenderPath;
import dev.loki.lovisual.features.module.HudPhase;
-import dev.loki.lovisual.render.engine.profiler.ProfilerPhase;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
-import dev.loki.lovisual.util.screen.ClientScreen;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.screens.ChatScreen;
-import org.lwjgl.glfw.GLFW;
-import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
-import dev.loki.lovisual.features.theme.Themes;
-import dev.loki.lovisual.render.engine.color.RenderColor;
-import dev.loki.lovisual.render.engine.math.HudScale;
-import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D;
-import dev.loki.lovisual.render.engine.text.FontInfo;
-import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer;
-import dev.loki.lovisual.Lifecycle;
-import dev.loki.lovisual.render.helpers.SystemCursor;
-import dev.loki.lovisual.util.input.KeyManager;
-import dev.loki.lovisual.util.logging.DebugLog;
-import java.util.*;
+import java.util.List;
/**
* Реестр HUD-виджетов + drag-and-drop при открытом чате.
+ *
+ * Публичный фасад: логика делегирована хелперам
+ * ({@code registry/} — состояние, поиск, якоря и snap;
+ * {@code interaction/} — drag-and-drop; {@code render/} — пайплайн рендера и оверлей).
*/
public enum DraggableHudElementRegistry {
;
- private static final List WIDGETS = new ArrayList<>();
- private static final float SNAP_DIST = 8f;
- private static final float PARENT_SNAP_DIST = 10f;
- private static final float PARENT_ALIGN_SNAP_DIST = 5f;
- private static final float PARENT_GAP = 6f;
- private static final int SNAP_PREVIEW_LINE = 0xFFFFFFFF;
- private static final float DRAG_COORD_TEXT_SCALE = 0.85f;
- private static final float DRAG_COORD_MARGIN = 4f;
- private static final float LINK_WIRE_THICKNESS = 1.05f;
- private static final float LINK_PREVIEW_THICKNESS = 1.55f;
- private static DraggableHudElement dragging;
- private static float dragOffsetX;
- private static float dragOffsetY;
- private static boolean draggingWithLeft;
- private static boolean prevLeft;
- private static boolean prevMiddle;
- private static long renderOrderSeq;
- private static boolean forceVisible;
- private static boolean snapPreviewActive;
- private static boolean snapPreviewHasX;
- private static boolean snapPreviewHasY;
- private static float snapPreviewLineX;
- private static float snapPreviewLineY;
- private static String editorWidgetId;
- private static boolean hudCursorActive;
- private static int lastScreenW = -1;
- private static int lastScreenH = -1;
- private static Themes.Theme cachedLinkTheme;
- private static LinkWireColors cachedLinkColors;
-
public static void register(DraggableHudElement widget) {
- if (!WIDGETS.contains(widget)) {
- widget.postInit();
- WIDGETS.add(widget);
- DebugLog.config("[HUD] Registered widget: %s", widget.getId());
- }
+ RegistryWidgetStore.register(widget);
}
public static List getWidgets() {
- HudElements.ensureLoaded();
- return Collections.unmodifiableList(WIDGETS);
+ return RegistryWidgetStore.getWidgets();
}
public static DraggableHudElement getById(String id) {
- HudElements.ensureLoaded();
- return findById(id);
+ return RegistryWidgetStore.getById(id);
}
public static boolean isEnabled(String id) {
- if (!Lifecycle.canRunHud()) return false;
- DraggableHudElement w = findById(id);
- return w != null && w.isEnabled();
+ return RegistryWidgetStore.isEnabled(id);
}
public static T get(Class type) {
- if (type == null) return null;
- for (DraggableHudElement w : WIDGETS) {
- if (type.isInstance(w)) {
- return type.cast(w);
- }
- }
- return null;
+ return RegistryWidgetStore.get(type);
}
public static T widget(Class type) {
@@ -106,49 +52,31 @@ public enum DraggableHudElementRegistry {
}
public static boolean isEnabled(Class extends DraggableHudElement> type) {
- if (!Lifecycle.canRunHud()) return false;
- DraggableHudElement w = get(type);
- return w != null && w.isEnabled();
+ return RegistryWidgetStore.isEnabled(type);
}
public static void tickAll() {
- for (DraggableHudElement w : WIDGETS) {
- w.onTick();
- }
+ RegistryWidgetStore.tickAll();
}
public static boolean isForceVisible() {
- return forceVisible;
+ return RegistryState.forceVisible;
}
public static void setForceVisible(boolean value) {
- forceVisible = value;
+ RegistryState.forceVisible = value;
}
public static void setEditorWidgetId(String id) {
- editorWidgetId = id;
+ RegistryState.editorWidgetId = id;
}
public static boolean hasEngineWork(HudPhase phase, boolean foreground) {
- if (!Lifecycle.canRunHud()) return false;
- if (foreground && dragging != null) return true;
-
- for (DraggableHudElement w : WIDGETS) {
- if (isRenderableCandidate(w, phase)) return true;
- }
- return false;
+ return RegistryRenderPipeline.hasEngineWork(phase, foreground);
}
public static boolean hasEngineWidgetWork(String id) {
- if (!Lifecycle.canRunHud()) return false;
- if (id == null || id.isBlank()) return false;
- if (editorWidgetId != null && !editorWidgetId.equals(id)) return false;
-
- DraggableHudElement w = findById(id);
- if (w == null) return false;
- if (!w.usesEngineRenderer()) return false;
- boolean bypassEnabled = forceVisible && editorWidgetId != null && editorWidgetId.equals(id);
- return bypassEnabled || w.isEnabled();
+ return RegistryWidgetRenderPath.hasEngineWidgetWork(id);
}
public static void renderAllEngine(HudPhase phase,
@@ -156,67 +84,7 @@ public enum DraggableHudElementRegistry {
TextRenderer textRenderer,
net.minecraft.client.gui.GuiGraphicsExtractor ctx,
float tickDelta) {
- if (!Lifecycle.canRunHud()) {
- cancelDragging();
- return;
- }
- Minecraft mc = Minecraft.getInstance();
- if (mc == null || mc.getWindow() == null) return;
- boolean allowOnlyModuleList = false;
-
- int fbw = mc.getWindow().getWidth();
- int fbh = mc.getWindow().getHeight();
- int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
- int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
- handleScreenResize(screenW, screenH);
-
- List renderList = new ArrayList<>();
- for (DraggableHudElement w : WIDGETS) {
- boolean singleEditorPreview = editorWidgetId != null;
- if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) {
- continue;
- }
- boolean bypassEnabled = singleEditorPreview && forceVisible;
- if (!bypassEnabled && !w.isEnabled()) {
- continue;
- }
- if (!w.usesEngineRenderer()) {
- continue;
- }
- if (w.getHudPhase() != phase) {
- continue;
- }
- if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) {
- continue;
- }
- ensurePositionInitialized(w, screenW, screenH);
- if (allowOnlyModuleList && !"module_list".equals(w.getId())) {
- continue;
- }
- renderList.add(w);
- }
-
- for (DraggableHudElement w : renderList) {
- if (w == dragging) continue;
- if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) {
- applyScreenAnchor(w, screenW, screenH);
- }
- }
- for (DraggableHudElement w : renderList) {
- if (w == dragging) continue;
- if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) {
- applyAnchors(w, screenW, screenH, new HashSet<>());
- }
- }
-
- for (DraggableHudElement w : renderList) {
- noteRendered(w);
- try (ProfilerPhase.Scope widgetScope =
- ProfilerPhase.scope("2d:widget:" + w.getId());
- RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) {
- w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
- }
- }
+ RegistryRenderPipeline.renderAllEngine(phase, renderer, textRenderer, ctx, tickDelta);
}
public static void renderAllEngineForeground(HudPhase phase,
@@ -224,54 +92,7 @@ public enum DraggableHudElementRegistry {
TextRenderer textRenderer,
net.minecraft.client.gui.GuiGraphicsExtractor ctx,
float tickDelta) {
- if (!Lifecycle.canRunHud()) {
- cancelDragging();
- return;
- }
- Minecraft mc = Minecraft.getInstance();
- if (mc == null || mc.getWindow() == null) return;
- boolean allowOnlyModuleList = false;
-
- int fbw = mc.getWindow().getWidth();
- int fbh = mc.getWindow().getHeight();
- int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
- int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
- handleScreenResize(screenW, screenH);
-
- for (DraggableHudElement w : WIDGETS) {
- boolean singleEditorPreview = editorWidgetId != null;
- if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) {
- continue;
- }
- boolean bypassEnabled = singleEditorPreview && forceVisible;
- if (!bypassEnabled && !w.isEnabled()) {
- continue;
- }
- if (!w.usesEngineRenderer()) {
- continue;
- }
- if (w.getHudPhase() != phase) {
- continue;
- }
- if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) {
- continue;
- }
- if (allowOnlyModuleList && !"module_list".equals(w.getId())) {
- continue;
- }
- try (ProfilerPhase.Scope widgetScope =
- ProfilerPhase.scope("2d:widget_fg:" + w.getId());
- RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) {
- w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
- }
- }
-
- renderDragOverlay(renderer, textRenderer, screenW, screenH);
- BetterChatRenderer.renderCapturedTooltip(ctx);
- if (phase == HudPhase.AFTER_SUBTITLES) {
- float uiScale = HudScale.scale(fbw, fbh);
- handleDragging(mc, screenW, screenH, uiScale);
- }
+ RegistryRenderPipeline.renderAllEngineForeground(phase, renderer, textRenderer, ctx, tickDelta);
}
public static void renderEngineWidget(String id,
@@ -279,1171 +100,6 @@ public enum DraggableHudElementRegistry {
TextRenderer textRenderer,
net.minecraft.client.gui.GuiGraphicsExtractor ctx,
float tickDelta) {
- if (!Lifecycle.canRunHud()) {
- cancelDragging();
- return;
- }
- if (editorWidgetId != null && (!editorWidgetId.equals(id))) return;
- Minecraft mc = Minecraft.getInstance();
- if (mc == null || mc.getWindow() == null) return;
- boolean isChat = ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen;
- boolean isInventory = ClientScreen.current() instanceof net.minecraft.client.gui.screens.inventory.AbstractContainerScreen>;
- boolean allowOnlyModuleList = isInventory && ClientScreen.current() != null;
- if (forceVisible) {
- allowOnlyModuleList = false;
- }
- boolean shouldHideHud = !forceVisible && ClientScreen.current() != null && !isChat && !allowOnlyModuleList;
- if (shouldHideHud) {
- return;
- }
-
- DraggableHudElement w = findById(id);
- if (w == null) return;
- if (!w.usesEngineRenderer()) return;
- boolean bypassEnabled = forceVisible && editorWidgetId != null && editorWidgetId.equals(id);
- if (!bypassEnabled && !w.isEnabled()) {
- return;
- }
- if (allowOnlyModuleList && !"module_list".equals(w.getId())) {
- return;
- }
-
- int fbw = mc.getWindow().getWidth();
- int fbh = mc.getWindow().getHeight();
- int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
- int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
- handleScreenResize(screenW, screenH);
-
- ensurePositionInitialized(w, screenW, screenH);
- if (w != dragging) {
- if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) {
- applyScreenAnchor(w, screenW, screenH);
- } else {
- applyAnchors(w, screenW, screenH, new HashSet<>());
- }
- }
-
- noteRendered(w);
- try (ProfilerPhase.Scope widgetScope =
- ProfilerPhase.scope("2d:widget:" + w.getId());
- RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) {
- w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
- }
- try (ProfilerPhase.Scope widgetScope =
- ProfilerPhase.scope("2d:widget_fg:" + w.getId());
- RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) {
- w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
- }
- }
-
- private static void ensurePositionInitialized(DraggableHudElement w, int screenW, int screenH) {
- if (w.isPositionInitialized()) return;
- DraggableLayoutValue.State saved = w.layoutValue().get();
- if (saved != null) {
- DraggableLayoutValue.State resolved = saved;
- if (!saved.logical()) {
- Minecraft mc = Minecraft.getInstance();
- if (mc != null && mc.getWindow() != null) {
- int fbw = mc.getWindow().getWidth();
- int fbh = mc.getWindow().getHeight();
- float uiScale = HudScale.scale(fbw, fbh);
- float nx = HudScale.toVirtual(saved.x(), uiScale);
- float ny = HudScale.toVirtual(saved.y(), uiScale);
- float npx = HudScale.toVirtual(saved.parentOffsetX(), uiScale);
- float npy = HudScale.toVirtual(saved.parentOffsetY(), uiScale);
- resolved = new DraggableLayoutValue.State(
- nx,
- ny,
- saved.anchorX(),
- saved.anchorY(),
- saved.parentId(),
- saved.parentSide(),
- npx,
- npy,
- true
- );
- w.layoutValue().set(resolved);
- w.saveConfig();
- }
- }
- float safeW = w.getWidth() > 0f ? w.getWidth() : 20f;
- float safeH = w.getHeight() > 0f ? w.getHeight() : 20f;
- float clampedX = saved.anchorX() == HudAnchorX.FREE
- ? Math.max(0, Math.min(resolved.x(), screenW - safeW))
- : resolved.x();
- float clampedY = saved.anchorY() == HudAnchorY.FREE
- ? Math.max(0, Math.min(resolved.y(), screenH - safeH))
- : resolved.y();
- if (saved.anchorX() == HudAnchorX.FREE && saved.anchorY() == HudAnchorY.FREE
- && (clampedX != resolved.x() || clampedY != resolved.y())) {
- DebugLog.config("[HUD] Saved position clamped for %s: (%.1f, %.1f) -> (%.1f, %.1f)",
- w.getId(), resolved.x(), resolved.y(), clampedX, clampedY);
- } else {
- DebugLog.config("[HUD] Loaded saved position for %s: (%.1f, %.1f)", w.getId(), resolved.x(), resolved.y());
- }
- w.moveTo(clampedX, clampedY);
- w.setAnchors(resolved.anchorX(), resolved.anchorY(), clampedX, clampedY);
- if (resolved.parentId() != null && resolved.parentSide() != WidgetAnchorSide.NONE) {
- w.setParentAnchor(resolved.parentId(), resolved.parentSide(), resolved.parentOffsetX(), resolved.parentOffsetY());
- } else {
- w.clearParentAnchor();
- }
- } else {
- w.applyDefaultPosition(screenW, screenH);
- w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
- w.clearParentAnchor();
- DebugLog.config("[HUD] Applied default position for %s: (%.1f, %.1f)", w.getId(), w.getX(), w.getY());
- }
- w.markPositionInitialized();
- }
-
- private static void handleDragging(Minecraft mc, int screenW, int screenH, float uiScale) {
- boolean chatOpen = ClientScreen.current() instanceof ChatScreen;
- boolean dragEditorOpen = forceVisible && ClientScreen.current() != null;
- boolean allowDragging = chatOpen || dragEditorOpen;
- float fbX = (float) mc.mouseHandler.xpos();
- float fbY = (float) mc.mouseHandler.ypos();
- float mx = HudScale.toVirtual(fbX, uiScale);
- float my = HudScale.toVirtual(fbY, uiScale);
-
- boolean left = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_1) == GLFW.GLFW_PRESS;
- boolean middle = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_MIDDLE) == GLFW.GLFW_PRESS;
-
- if (!allowDragging) {
- if (dragging != null) {
- //DebugLog.config("[HUD] Drag cancelled outside chat for %s", dragging.getId());
- }
- if (left && !prevLeft) {
- // DebugLog.config("[HUD] Mouse down ignored (chat not open) at (%.1f, %.1f)", mx, my);
- }
- if (!left && prevLeft) {
- // DebugLog.config("[HUD] Mouse up ignored (chat not open) at (%.1f, %.1f)", mx, my);
- }
- dragging = null;
- draggingWithLeft = false;
- clearSnapPreview();
- if (hudCursorActive) {
- SystemCursor.reset();
- hudCursorActive = false;
- }
- prevLeft = left;
- prevMiddle = middle;
- return;
- }
-
- if (dragging != null && !isWidgetVisibleForEditor(dragging)) {
- dragging = null;
- draggingWithLeft = false;
- clearSnapPreview();
- }
-
- if (left && !prevLeft) {
- //DebugLog.config("[HUD] Mouse down in chat at (%.1f, %.1f)", mx, my);
- for (DraggableHudElement w : WIDGETS) {
- /*DebugLog.config("[HUD] Widget %s bounds x=%.1f y=%.1f w=%.1f h=%.1f contains=%s",
- w.getId(), w.getX(), w.getY(), w.getWidth(), w.getHeight(), w.contains(mx, my));*/
- }
- DraggableHudElement clickTarget = null;
- long bestClickOrder = Long.MIN_VALUE;
- for (DraggableHudElement w : WIDGETS) {
- if (!isWidgetVisibleForEditor(w)) continue;
- if (!w.contains(mx, my)) continue;
- if (!w.isMouseOverInteractive(mx, my)) continue;
- long order = w.getRuntimeRenderOrder();
- if (order > bestClickOrder) {
- bestClickOrder = order;
- clickTarget = w;
- }
- }
- if (clickTarget != null) {
- if (clickTarget.onMouseClicked(mx, my, GLFW.GLFW_MOUSE_BUTTON_1)) {
- dragging = null;
- draggingWithLeft = false;
- clearSnapPreview();
- prevLeft = left;
- prevMiddle = middle;
- return;
- }
- // interactive hitbox: do not start drag on left click
- prevLeft = left;
- prevMiddle = middle;
- return;
- } else {
- // start drag with left click if no interactive hitbox
- DraggableHudElement candidate = null;
- long bestOrder = Long.MIN_VALUE;
- for (DraggableHudElement w : WIDGETS) {
- if (!isWidgetVisibleForEditor(w)) continue;
- if (!w.isDraggable() || !w.contains(mx, my)) continue;
- long order = w.getRuntimeRenderOrder();
- if (order > bestOrder) {
- bestOrder = order;
- candidate = w;
- }
- }
- if (candidate == null) {
- for (int i = WIDGETS.size() - 1; i >= 0; i--) {
- DraggableHudElement w = WIDGETS.get(i);
- if (!isWidgetVisibleForEditor(w)) continue;
- if (w.contains(mx, my) && w.isDraggable()) {
- candidate = w;
- break;
- }
- }
- }
- if (candidate != null) {
- dragging = candidate;
- draggingWithLeft = true;
- dragOffsetX = mx - candidate.getX();
- dragOffsetY = my - candidate.getY();
- if (isWidgetAnchorModifierDown()) {
- detachChildren(candidate);
- }
- dragging.clearParentAnchor();
- dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, dragging.getX(), dragging.getY());
- SystemCursor.set(SystemCursor.CursorType.MOVE);
- hudCursorActive = true;
- }
- }
- }
-
- if (middle && !prevMiddle) {
- // start drag
- DraggableHudElement candidate = null;
- long bestOrder = Long.MIN_VALUE;
- for (DraggableHudElement w : WIDGETS) {
- if (!isWidgetVisibleForEditor(w)) continue;
- if (!w.isDraggable() || !w.contains(mx, my)) continue;
- long order = w.getRuntimeRenderOrder();
- if (order > bestOrder) {
- bestOrder = order;
- candidate = w;
- }
- }
- if (candidate == null) {
- for (int i = WIDGETS.size() - 1; i >= 0; i--) {
- DraggableHudElement w = WIDGETS.get(i);
- if (!isWidgetVisibleForEditor(w)) continue;
- if (w.contains(mx, my) && w.isDraggable()) {
- candidate = w;
- break;
- }
- }
- }
- if (candidate != null) {
- dragging = candidate;
- draggingWithLeft = false;
- dragOffsetX = mx - candidate.getX();
- dragOffsetY = my - candidate.getY();
- if (isWidgetAnchorModifierDown()) {
- detachChildren(candidate);
- }
- dragging.clearParentAnchor();
- dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, dragging.getX(), dragging.getY());
- //DebugLog.config("[HUD] Drag start for %s at (%.1f, %.1f)", dragging.getId(), dragging.getX(), dragging.getY());
- SystemCursor.set(SystemCursor.CursorType.MOVE);
- hudCursorActive = true;
- }
- if (dragging == null) {
- //DebugLog.config("[HUD] No widget found under cursor at (%.1f, %.1f); widgets=%d", mx, my, WIDGETS.size());
- }
- } else if (!middle && prevMiddle && dragging != null && !draggingWithLeft) {
- stopDragging(screenW, screenH);
- } else if (!middle && prevMiddle) {
- //DebugLog.config("[HUD] Mouse up with no active drag at (%.1f, %.1f)", mx, my);
- }
-
- if (draggingWithLeft && !left && prevLeft && dragging != null) {
- stopDragging(screenW, screenH);
- }
-
- if (((draggingWithLeft && left) || (!draggingWithLeft && middle)) && dragging != null) {
- moveDraggedWidget(dragging, mx, my, screenW, screenH);
- if (!isWidgetAnchorModifierDown()) {
- updateSnapPreview(dragging, screenW, screenH);
- } else {
- clearSnapPreview();
- }
- //DebugLog.config("[HUD] Dragging %s -> (%.1f, %.1f)", dragging.getId(), dragging.getX(), dragging.getY());
- SystemCursor.set(SystemCursor.CursorType.MOVE);
- hudCursorActive = true;
- } else if (dragging == null) {
- clearSnapPreview();
- requestHoverCursor(mx, my);
- }
-
- prevLeft = left;
- prevMiddle = middle;
- }
-
- private static void stopDragging(int screenW, int screenH) {
- if (dragging == null) return;
- if (!isWidgetAnchorModifierDown()) {
- applyStickySnap(dragging, screenW, screenH);
- }
- dragging.clearParentAnchor();
- Minecraft mc = Minecraft.getInstance();
- boolean allowWidgetAnchoring = mc == null || !isWidgetAnchorModifierDown();
- updateDragAnchors(dragging, screenW, screenH, allowWidgetAnchoring);
- dragging.layoutValue().set(dragging.snapshotLayout());
- dragging.saveConfig();
- dragging = null;
- draggingWithLeft = false;
- clearSnapPreview();
- if (hudCursorActive) {
- SystemCursor.reset();
- hudCursorActive = false;
- }
- }
-
- private static void cancelDragging() {
- dragging = null;
- draggingWithLeft = false;
- clearSnapPreview();
- if (hudCursorActive) {
- SystemCursor.reset();
- hudCursorActive = false;
- }
- prevLeft = false;
- prevMiddle = false;
- }
-
- private static void requestHoverCursor(float mx, float my) {
- DraggableHudElement interactive = findTopVisibleWidget(mx, my, true);
- if (interactive != null) {
- SystemCursor.set(SystemCursor.CursorType.HAND);
- hudCursorActive = true;
- return;
- }
-
- DraggableHudElement draggable = findTopVisibleWidget(mx, my, false);
- if (draggable != null) {
- SystemCursor.set(SystemCursor.CursorType.MOVE);
- hudCursorActive = true;
- }
- }
-
- private static DraggableHudElement findTopVisibleWidget(float mx, float my, boolean interactiveOnly) {
- DraggableHudElement best = null;
- long bestOrder = Long.MIN_VALUE;
- for (DraggableHudElement w : WIDGETS) {
- if (!isWidgetVisibleForEditor(w)) continue;
- if (!w.contains(mx, my)) continue;
-
- if (interactiveOnly) {
- if (!w.isMouseOverInteractive(mx, my)) continue;
- } else if (!w.isDraggable()) {
- continue;
- }
-
- long order = w.getRuntimeRenderOrder();
- if (order > bestOrder) {
- bestOrder = order;
- best = w;
- }
- }
-
- if (best != null) return best;
- for (int i = WIDGETS.size() - 1; i >= 0; i--) {
- DraggableHudElement w = WIDGETS.get(i);
- if (!isWidgetVisibleForEditor(w)) continue;
- if (!w.contains(mx, my)) continue;
-
- if (interactiveOnly) {
- if (!w.isMouseOverInteractive(mx, my)) continue;
- } else if (!w.isDraggable()) {
- continue;
- }
-
- return w;
- }
-
- return null;
- }
-
- private static boolean isWidgetVisibleForEditor(DraggableHudElement w) {
- if (w == null) return false;
-
- boolean singleEditorPreview = editorWidgetId != null;
- if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) {
- return false;
- }
-
- boolean bypassEnabled = singleEditorPreview && forceVisible;
- if (!bypassEnabled && !w.isEnabled()) {
- return false;
- }
-
- if (!w.usesEngineRenderer()) return false;
- if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) return false;
- return w.getRuntimeRenderOrder() > 0L;
- }
-
- private static boolean isRenderableCandidate(DraggableHudElement w, HudPhase phase) {
- if (w == null) return false;
-
- boolean singleEditorPreview = editorWidgetId != null;
- if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) {
- return false;
- }
-
- boolean bypassEnabled = singleEditorPreview && forceVisible;
- if (!bypassEnabled && !w.isEnabled()) {
- return false;
- }
-
- if (!w.usesEngineRenderer()) return false;
- if (w.getHudPhase() != phase) return false;
- return w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL;
- }
-
- private static boolean isWidgetVisibleForLinkOverlay(DraggableHudElement w) {
- if (w == null) return false;
-
- boolean singleEditorPreview = editorWidgetId != null;
- if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) {
- return false;
- }
-
- boolean bypassEnabled = singleEditorPreview && forceVisible;
- if (!bypassEnabled && !w.isEnabled()) {
- return false;
- }
-
- return w.usesEngineRenderer() && w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL;
- }
-
- private static void applyAnchors(DraggableHudElement w, int screenW, int screenH) {
- applyAnchors(w, screenW, screenH, new HashSet<>());
- }
-
- private static void applyAnchors(DraggableHudElement w, int screenW, int screenH, Set visiting) {
- if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) {
- applyScreenAnchor(w, screenW, screenH);
- return;
- }
- DraggableHudElement parent = findById(w.getParentId());
- if (parent == null) {
- w.clearParentAnchor();
- applyScreenAnchor(w, screenW, screenH);
- return;
- }
- if (!visiting.add(w.getId())) {
- w.clearParentAnchor();
- applyScreenAnchor(w, screenW, screenH);
- return;
- }
- if (parent != dragging) {
- applyAnchors(parent, screenW, screenH, visiting);
- }
- if (isDescendantOf(parent, w)) {
- w.clearParentAnchor();
- applyScreenAnchor(w, screenW, screenH);
- } else {
- applyWidgetAnchor(w, parent, screenW, screenH);
- }
- visiting.remove(w.getId());
- }
-
- private static void applyScreenAnchor(DraggableHudElement w, int screenW, int screenH) {
- float x = w.getAnchorOffsetX();
- float y = w.getAnchorOffsetY();
- switch (w.getAnchorX()) {
- case LEFT -> x = w.getAnchorOffsetX();
- case CENTER -> x = screenW * 0.5f - w.getWidth() * 0.5f + w.getAnchorOffsetX();
- case RIGHT -> x = screenW - w.getWidth() - w.getAnchorOffsetX();
- case FREE -> x = w.getAnchorOffsetX();
- }
- switch (w.getAnchorY()) {
- case TOP -> y = w.getAnchorOffsetY();
- case CENTER -> y = screenH * 0.5f - w.getHeight() * 0.5f + w.getAnchorOffsetY();
- case BOTTOM -> y = screenH - w.getHeight() - w.getAnchorOffsetY();
- case FREE -> y = w.getAnchorOffsetY();
- }
- w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH));
- }
-
- private static void applyWidgetAnchor(DraggableHudElement w, DraggableHudElement parent, int screenW, int screenH) {
- float x = w.getX();
- float y = w.getY();
- switch (w.getParentSide()) {
- case TOP -> {
- x = parent.getX() + w.getParentOffsetX();
- y = parent.getY() - w.getHeight() - PARENT_GAP + w.getParentOffsetY();
- }
- case BOTTOM -> {
- x = parent.getX() + w.getParentOffsetX();
- y = parent.getY() + parent.getHeight() + PARENT_GAP + w.getParentOffsetY();
- }
- case LEFT -> {
- x = parent.getX() - w.getWidth() - PARENT_GAP + w.getParentOffsetX();
- y = parent.getY() + w.getParentOffsetY();
- }
- case RIGHT -> {
- x = parent.getX() + parent.getWidth() + PARENT_GAP + w.getParentOffsetX();
- y = parent.getY() + w.getParentOffsetY();
- }
- case NONE -> {
- }
- }
- w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH));
- }
-
- private static void updateDragAnchors(DraggableHudElement w, int screenW, int screenH, boolean allowWidgetAnchoring) {
- if (w.getWidth() <= 0 || w.getHeight() <= 0) {
- w.clearParentAnchor();
- w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
- return;
- }
-
- if (!allowWidgetAnchoring) {
- w.clearParentAnchor();
- w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
- return;
- }
-
- if (w.supportsWidgetAnchoring()) {
- WidgetAnchorCandidate candidate = findWidgetAnchorCandidate(w, screenW, screenH);
- if (candidate != null) {
- w.setParentAnchor(candidate.parent.getId(), candidate.side, candidate.offsetX, candidate.offsetY);
- return;
- }
- }
- w.clearParentAnchor();
-
- float x = w.getX();
- float y = w.getY();
-
- HudAnchorX ax = HudAnchorX.FREE;
- float offX = x;
- float leftDist = Math.abs(x);
- float rightDist = Math.abs((x + w.getWidth()) - screenW);
- float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f);
- if (leftDist <= SNAP_DIST) {
- ax = HudAnchorX.LEFT;
- offX = x;
- } else if (rightDist <= SNAP_DIST) {
- ax = HudAnchorX.RIGHT;
- offX = rightDist;
- } else if (centerDistX <= SNAP_DIST) {
- ax = HudAnchorX.CENTER;
- offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f;
- }
-
- HudAnchorY ay = HudAnchorY.FREE;
- float offY = y;
- float topDist = Math.abs(y);
- float bottomDist = Math.abs((y + w.getHeight()) - screenH);
- float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f);
- if (topDist <= SNAP_DIST) {
- ay = HudAnchorY.TOP;
- offY = y;
- } else if (bottomDist <= SNAP_DIST) {
- ay = HudAnchorY.BOTTOM;
- offY = bottomDist;
- } else if (centerDistY <= SNAP_DIST) {
- ay = HudAnchorY.CENTER;
- offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f;
- }
-
- w.setAnchors(ax, ay, offX, offY);
- }
-
- private static void handleScreenResize(int screenW, int screenH) {
- if (screenW <= 0 || screenH <= 0) return;
- if (lastScreenW <= 0 || lastScreenH <= 0) {
- lastScreenW = screenW;
- lastScreenH = screenH;
- return;
- }
- if (lastScreenW == screenW && lastScreenH == screenH) return;
-
- int oldW = lastScreenW;
- int oldH = lastScreenH;
- lastScreenW = screenW;
- lastScreenH = screenH;
-
- for (DraggableHudElement w : WIDGETS) {
- if (!w.isPositionInitialized()) continue;
- if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) {
- // Parent-anchored widgets will be handled when anchors are applied.
- continue;
- }
- if (w.getWidth() <= 0 || w.getHeight() <= 0) continue;
-
- boolean changed = false;
- if (w.getAnchorX() == HudAnchorX.FREE && w.getAnchorY() == HudAnchorY.FREE) {
- changed |= inferScreenAnchors(w, oldW, oldH);
- }
-
- applyScreenAnchor(w, screenW, screenH);
- changed |= clampToScreenIfFree(w, screenW, screenH);
-
- if (changed) {
- w.layoutValue().set(w.snapshotLayout());
- w.saveConfig();
- }
- }
- }
-
- private static boolean inferScreenAnchors(DraggableHudElement w, int screenW, int screenH) {
- float x = w.getX();
- float y = w.getY();
- boolean changed = false;
-
- HudAnchorX ax = w.getAnchorX();
- float offX = w.getAnchorOffsetX();
- float leftDist = Math.abs(x);
- float rightDist = Math.abs((x + w.getWidth()) - screenW);
- float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f);
- if (leftDist <= SNAP_DIST) {
- ax = HudAnchorX.LEFT;
- offX = x;
- } else if (rightDist <= SNAP_DIST) {
- ax = HudAnchorX.RIGHT;
- offX = rightDist;
- } else if (centerDistX <= SNAP_DIST) {
- ax = HudAnchorX.CENTER;
- offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f;
- }
-
- HudAnchorY ay = w.getAnchorY();
- float offY = w.getAnchorOffsetY();
- float topDist = Math.abs(y);
- float bottomDist = Math.abs((y + w.getHeight()) - screenH);
- float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f);
- if (topDist <= SNAP_DIST) {
- ay = HudAnchorY.TOP;
- offY = y;
- } else if (bottomDist <= SNAP_DIST) {
- ay = HudAnchorY.BOTTOM;
- offY = bottomDist;
- } else if (centerDistY <= SNAP_DIST) {
- ay = HudAnchorY.CENTER;
- offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f;
- }
-
- if (ax != w.getAnchorX() || ay != w.getAnchorY()
- || offX != w.getAnchorOffsetX() || offY != w.getAnchorOffsetY()) {
- w.setAnchors(ax, ay, offX, offY);
- changed = true;
- }
- return changed;
- }
-
- private static boolean clampToScreenIfFree(DraggableHudElement w, int screenW, int screenH) {
- boolean changed = false;
- float newX = w.getX();
- float newY = w.getY();
- if (w.getAnchorX() == HudAnchorX.FREE) {
- float maxX = Math.max(0f, screenW - w.getWidth());
- newX = Math.max(0f, Math.min(newX, maxX));
- }
- if (w.getAnchorY() == HudAnchorY.FREE) {
- float maxY = Math.max(0f, screenH - w.getHeight());
- newY = Math.max(0f, Math.min(newY, maxY));
- }
- if (newX != w.getX() || newY != w.getY()) {
- w.moveTo(newX, newY);
- if (w.getAnchorX() == HudAnchorX.FREE || w.getAnchorY() == HudAnchorY.FREE) {
- w.setAnchors(w.getAnchorX(), w.getAnchorY(),
- w.getAnchorX() == HudAnchorX.FREE ? newX : w.getAnchorOffsetX(),
- w.getAnchorY() == HudAnchorY.FREE ? newY : w.getAnchorOffsetY());
- }
- changed = true;
- }
- return changed;
- }
-
- private static boolean isWidgetAnchorModifierDown() {
- return KeyManager.isComboHeldAllowScreen("LEFT_ALT")
- || KeyManager.isComboHeldAllowScreen("RIGHT_ALT");
- }
-
- private static void detachChildren(DraggableHudElement parent) {
- String parentId = parent.getId();
- for (DraggableHudElement w : WIDGETS) {
- if (w == parent) continue;
- if (parentId.equals(w.getParentId()) && w.getParentSide() != WidgetAnchorSide.NONE) {
- w.clearParentAnchor();
- w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
- w.layoutValue().set(w.snapshotLayout());
- w.saveConfig();
- }
- }
- }
-
- private static WidgetAnchorCandidate findWidgetAnchorCandidate(DraggableHudElement dragging, int screenW, int screenH) {
- float bestDist = Float.MAX_VALUE;
- WidgetAnchorCandidate best = null;
- for (DraggableHudElement target : WIDGETS) {
- if (target == dragging) continue;
- if (!isWidgetVisibleForLinkOverlay(target)) continue;
- if (isDescendantOf(target, dragging)) continue;
- if (target.getWidth() <= 0 || target.getHeight() <= 0) continue;
-
- float dxOverlap = overlap(dragging.getX(), dragging.getX() + dragging.getWidth(),
- target.getX(), target.getX() + target.getWidth());
- float dyOverlap = overlap(dragging.getY(), dragging.getY() + dragging.getHeight(),
- target.getY(), target.getY() + target.getHeight());
-
- float distTop = Math.abs((dragging.getY() + dragging.getHeight()) - (target.getY() - PARENT_GAP));
- if (dxOverlap > 0 && distTop <= PARENT_SNAP_DIST && distTop < bestDist) {
- float offsetX = parentAxisOffset(dragging.getX() - target.getX());
- float desiredY = target.getY() - PARENT_GAP - dragging.getHeight();
- float desiredX = target.getX() + offsetX;
- if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
- float offsetY = 0f;
- bestDist = distTop;
- best = new WidgetAnchorCandidate(target, WidgetAnchorSide.TOP, offsetX, offsetY);
- }
-
- float distBottom = Math.abs(dragging.getY() - (target.getY() + target.getHeight() + PARENT_GAP));
- if (dxOverlap > 0 && distBottom <= PARENT_SNAP_DIST && distBottom < bestDist) {
- float offsetX = parentAxisOffset(dragging.getX() - target.getX());
- float desiredY = target.getY() + target.getHeight() + PARENT_GAP;
- float desiredX = target.getX() + offsetX;
- if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
- float offsetY = 0f;
- bestDist = distBottom;
- best = new WidgetAnchorCandidate(target, WidgetAnchorSide.BOTTOM, offsetX, offsetY);
- }
-
- float distLeft = Math.abs((dragging.getX() + dragging.getWidth()) - (target.getX() - PARENT_GAP));
- if (dyOverlap > 0 && distLeft <= PARENT_SNAP_DIST && distLeft < bestDist) {
- float offsetY = parentAxisOffset(dragging.getY() - target.getY());
- float desiredX = target.getX() - PARENT_GAP - dragging.getWidth();
- float desiredY = target.getY() + offsetY;
- if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
- float offsetX = 0f;
- bestDist = distLeft;
- best = new WidgetAnchorCandidate(target, WidgetAnchorSide.LEFT, offsetX, offsetY);
- }
-
- float distRight = Math.abs(dragging.getX() - (target.getX() + target.getWidth() + PARENT_GAP));
- if (dyOverlap > 0 && distRight <= PARENT_SNAP_DIST && distRight < bestDist) {
- float offsetY = parentAxisOffset(dragging.getY() - target.getY());
- float desiredX = target.getX() + target.getWidth() + PARENT_GAP;
- float desiredY = target.getY() + offsetY;
- if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
- float offsetX = 0f;
- bestDist = distRight;
- best = new WidgetAnchorCandidate(target, WidgetAnchorSide.RIGHT, offsetX, offsetY);
- }
- }
- return best;
- }
-
- private static float parentAxisOffset(float offset) {
- return Math.abs(offset) <= PARENT_ALIGN_SNAP_DIST ? 0f : offset;
- }
-
- private static float overlap(float a0, float a1, float b0, float b1) {
- float left = Math.max(a0, b0);
- float right = Math.min(a1, b1);
- return right - left;
- }
-
- private static DraggableHudElement findById(String id) {
- if (id == null) return null;
- for (DraggableHudElement w : WIDGETS) {
- if (id.equals(w.getId())) return w;
- }
- return null;
- }
-
- private static void moveDraggedWidget(DraggableHudElement w, float mx, float my, int screenW, int screenH) {
- float newX = mx - dragOffsetX;
- float newY = my - dragOffsetY;
- Bounds groupBounds = collectLinkedBounds(w, screenW, screenH);
- float dx = newX - w.getX();
- float dy = newY - w.getY();
-
- if (groupBounds != null && groupBounds.width() <= screenW && groupBounds.height() <= screenH) {
- dx = clamp(dx, -groupBounds.minX(), screenW - groupBounds.maxX());
- dy = clamp(dy, -groupBounds.minY(), screenH - groupBounds.maxY());
- w.moveTo(w.getX() + dx, w.getY() + dy);
- applyDescendantAnchors(w, screenW, screenH, new HashSet<>());
- return;
- }
-
- w.moveTo(clampX(newX, w, screenW), clampY(newY, w, screenH));
- applyDescendantAnchors(w, screenW, screenH, new HashSet<>());
- }
-
- private static void updateSnapPreview(DraggableHudElement w, int screenW, int screenH) {
- if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) {
- clearSnapPreview();
- return;
- }
-
- StickySnap snap = findStickySnap(w, screenW, screenH);
- if (!snap.hasX && !snap.hasY) {
- clearSnapPreview();
- return;
- }
-
- snapPreviewActive = true;
- snapPreviewHasX = snap.hasX;
- snapPreviewHasY = snap.hasY;
- snapPreviewLineX = snap.x;
- snapPreviewLineY = snap.y;
- }
-
- private static void applyStickySnap(DraggableHudElement w, int screenW, int screenH) {
- if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) return;
-
- StickySnap snap = findStickySnap(w, screenW, screenH);
- float newX = w.getX();
- float newY = w.getY();
- if (snap.hasX) newX = snap.x;
- if (snap.hasY) newY = snap.y;
- w.moveTo(clampX(newX, w, screenW), clampY(newY, w, screenH));
- }
-
- private static StickySnap findStickySnap(DraggableHudElement dragging, int screenW, int screenH) {
- float bestX = 0f;
- float bestY = 0f;
- float bestXDist = Float.MAX_VALUE;
- float bestYDist = Float.MAX_VALUE;
- boolean hasX = false;
- boolean hasY = false;
-
- boolean rightSide = dragging.getX() > screenW * 0.5f;
- for (DraggableHudElement target : WIDGETS) {
- if (target == dragging) continue;
- if (!target.isEnabled()) continue;
- if (target.getWidth() <= 0 || target.getHeight() <= 0) continue;
-
- if (rightSide) {
- float targetEdge = target.getX() + target.getWidth();
- float dragEdge = dragging.getX() + dragging.getWidth();
- float dist = Math.abs(targetEdge - dragEdge);
- if (dist <= PARENT_SNAP_DIST && dist < bestXDist) {
- bestXDist = dist;
- bestX = targetEdge - dragging.getWidth();
- hasX = true;
- }
- } else {
- float dist = Math.abs(target.getX() - dragging.getX());
- if (dist <= PARENT_SNAP_DIST && dist < bestXDist) {
- bestXDist = dist;
- bestX = target.getX();
- hasX = true;
- }
- }
-
- float distY = Math.abs(target.getY() - dragging.getY());
- if (distY <= PARENT_SNAP_DIST && distY < bestYDist) {
- bestYDist = distY;
- bestY = target.getY();
- hasY = true;
- }
- }
-
- float distTop = Math.abs(dragging.getY());
- if (distTop <= PARENT_SNAP_DIST && distTop < bestYDist) {
- bestY = 0f;
- hasY = true;
- }
-
- return new StickySnap(hasX, hasY, bestX, bestY);
- }
-
- private static void clearSnapPreview() {
- snapPreviewActive = false;
- snapPreviewHasX = false;
- snapPreviewHasY = false;
- }
-
- private static void renderDragOverlay(Renderer2D renderer,
- TextRenderer fallbackTextRenderer,
- int screenW,
- int screenH) {
- renderLinkWires(renderer, screenW, screenH);
-
- if (snapPreviewActive) {
- if (snapPreviewHasX) {
- renderer.line(snapPreviewLineX, 0f, snapPreviewLineX, screenH, SNAP_PREVIEW_LINE);
- }
- if (snapPreviewHasY) {
- renderer.line(0f, snapPreviewLineY, screenW, snapPreviewLineY, SNAP_PREVIEW_LINE);
- }
- }
-
- if (dragging == null) return;
- TextRenderer coordRenderer = Fonts.renderer("Comfortaa", FontInfo.Type.Regular,
- fallbackTextRenderer != null ? fallbackTextRenderer : TextRenderer.get());
- if (coordRenderer == null) return;
-
- String coords = "x: " + Math.round(dragging.getX()) + " y: " + Math.round(dragging.getY());
- coordRenderer.begin(DRAG_COORD_TEXT_SCALE, false, false);
- try {
- float textW = (float) coordRenderer.getWidth(coords, true);
- float textH = (float) coordRenderer.getHeight(true);
- float textX = dragging.getX();
- float textY = dragging.getY() - textH - DRAG_COORD_MARGIN;
- if (textY < DRAG_COORD_MARGIN) {
- textY = dragging.getY() + dragging.getHeight() + DRAG_COORD_MARGIN;
- }
- textX = clamp(textX, DRAG_COORD_MARGIN, Math.max(DRAG_COORD_MARGIN, screenW - textW - DRAG_COORD_MARGIN));
- textY = clamp(textY, DRAG_COORD_MARGIN, Math.max(DRAG_COORD_MARGIN, screenH - textH - DRAG_COORD_MARGIN));
- coordRenderer.render(coords, textX, textY, new RenderColor(0xFFFFFFFF), true);
- } finally {
- coordRenderer.end();
- }
- }
-
- private static void renderLinkWires(Renderer2D renderer,
- int screenW,
- int screenH) {
- Minecraft mc = Minecraft.getInstance();
- if (mc == null || mc.getWindow() == null || !(ClientScreen.current() instanceof ChatScreen))
- return;
-
- int fbw = mc.getWindow().getWidth();
- int fbh = mc.getWindow().getHeight();
- float uiScale = HudScale.scale(fbw, fbh);
- float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale);
- float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale);
-
- LinkWireColors colors = linkWireColors();
- for (DraggableHudElement child : WIDGETS) {
- if (!isWidgetVisibleForLinkOverlay(child)) continue;
- if (child == dragging) continue;
- if (child.getParentId() == null || child.getParentSide() == WidgetAnchorSide.NONE) continue;
-
- DraggableHudElement parent = findById(child.getParentId());
- if (!isWidgetVisibleForLinkOverlay(parent)) continue;
- if (!parent.contains(mx, my) && !child.contains(mx, my)) continue;
- renderLinkWire(renderer, parent, child, child.getParentSide(),
- LINK_WIRE_THICKNESS, colors.wireStart(), colors.wireEnd());
- }
-
- if (dragging == null || isWidgetAnchorModifierDown()) return;
- WidgetAnchorCandidate candidate = findWidgetAnchorCandidate(dragging, screenW, screenH);
- if (candidate == null) return;
- renderLinkWire(renderer, candidate.parent(), dragging, candidate.side(),
- LINK_PREVIEW_THICKNESS, colors.previewStart(), colors.previewEnd());
- }
-
- private static LinkWireColors linkWireColors() {
- Themes.Theme theme = Theme.theme();
- if (cachedLinkTheme == theme && cachedLinkColors != null) {
- return cachedLinkColors;
- }
-
- int wireStart = HudRenderUtil.setAlpha(
- HudRenderUtil.mixColor(theme.textMuted(), theme.strokeSoft(), 0.45f), 0x86);
- int wireEnd = HudRenderUtil.setAlpha(
- HudRenderUtil.mixColor(theme.accentSoft(), theme.windowStroke(), 0.24f), 0x78);
- int previewStart = HudRenderUtil.setAlpha(
- HudRenderUtil.mixColor(theme.textPrimary(), theme.accentSoft(), 0.38f), 0xC8);
- int previewEnd = HudRenderUtil.setAlpha(
- HudRenderUtil.mixColor(theme.accent(), theme.accentSoft(), 0.28f), 0xD8);
-
- cachedLinkTheme = theme;
- cachedLinkColors = new LinkWireColors(wireStart, wireEnd, previewStart, previewEnd);
- return cachedLinkColors;
- }
-
- private static void renderLinkWire(Renderer2D renderer,
- DraggableHudElement parent,
- DraggableHudElement child,
- WidgetAnchorSide side,
- double thickness,
- int startArgb,
- int endArgb) {
- if (parent == null || child == null || side == null || side == WidgetAnchorSide.NONE) return;
- if (parent.getWidth() <= 0f || parent.getHeight() <= 0f || child.getWidth() <= 0f || child.getHeight() <= 0f) {
- return;
- }
-
- double parentX = 0.0;
- double parentY = 0.0;
- double childX = 0.0;
- double childY = 0.0;
- double parentCx = 0.0;
- double parentCy = 0.0;
- double childCx = 0.0;
- double childCy = 0.0;
-
- switch (side) {
- case TOP -> {
- parentX = parent.getX() + parent.getWidth() * 0.5;
- parentY = parent.getY();
- childX = child.getX() + child.getWidth() * 0.5;
- childY = child.getY() + child.getHeight();
- double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45);
- parentCx = parentX;
- parentCy = parentY - c;
- childCx = childX;
- childCy = childY + c;
- }
- case BOTTOM -> {
- parentX = parent.getX() + parent.getWidth() * 0.5;
- parentY = parent.getY() + parent.getHeight();
- childX = child.getX() + child.getWidth() * 0.5;
- childY = child.getY();
- double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45);
- parentCx = parentX;
- parentCy = parentY + c;
- childCx = childX;
- childCy = childY - c;
- }
- case LEFT -> {
- parentX = parent.getX();
- parentY = parent.getY() + parent.getHeight() * 0.5;
- childX = child.getX() + child.getWidth();
- childY = child.getY() + child.getHeight() * 0.5;
- double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45);
- parentCx = parentX - c;
- parentCy = parentY;
- childCx = childX + c;
- childCy = childY;
- }
- case RIGHT -> {
- parentX = parent.getX() + parent.getWidth();
- parentY = parent.getY() + parent.getHeight() * 0.5;
- childX = child.getX();
- childY = child.getY() + child.getHeight() * 0.5;
- double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45);
- parentCx = parentX + c;
- parentCy = parentY;
- childCx = childX - c;
- childCy = childY;
- }
- case NONE -> {
- return;
- }
- }
-
- renderer.bezierConnectorGradient(childX, childY, childCx, childCy, parentCx, parentCy, parentX, parentY,
- 18, thickness, startArgb, endArgb);
- }
-
- private static void applyDragAnchorLock(DraggableHudElement w, int screenW, int screenH) {
- if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) {
- DraggableHudElement parent = findById(w.getParentId());
- if (parent != null) {
- applyWidgetAnchor(w, parent, screenW, screenH);
- }
- return;
- }
- if (w.getAnchorX() != HudAnchorX.FREE || w.getAnchorY() != HudAnchorY.FREE) {
- applyScreenAnchor(w, screenW, screenH);
- }
- }
-
- private static float clampX(float x, DraggableHudElement w, int screenW) {
- float maxX = Math.max(0f, screenW - Math.max(0f, w.getWidth()));
- return clamp(x, 0f, maxX);
- }
-
- private static float clampY(float y, DraggableHudElement w, int screenH) {
- float maxY = Math.max(0f, screenH - Math.max(0f, w.getHeight()));
- return clamp(y, 0f, maxY);
- }
-
- private static float clamp(float value, float min, float max) {
- if (max < min) return min;
- return Math.max(min, Math.min(value, max));
- }
-
- private static void applyDescendantAnchors(DraggableHudElement parent, int screenW, int screenH, Set visited) {
- if (parent == null || !visited.add(parent.getId())) return;
- String parentId = parent.getId();
- for (DraggableHudElement child : WIDGETS) {
- if (child == parent) continue;
- if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue;
- applyWidgetAnchor(child, parent, screenW, screenH);
- applyDescendantAnchors(child, screenW, screenH, visited);
- }
- }
-
- private static Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH) {
- if (root == null) return null;
- return collectLinkedBounds(root, screenW, screenH, new HashSet<>());
- }
-
- private static Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH, Set visited) {
- if (root == null || !visited.add(root.getId())) return null;
- Bounds bounds = Bounds.of(root);
- String parentId = root.getId();
- for (DraggableHudElement child : WIDGETS) {
- if (child == root) continue;
- if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue;
- applyWidgetAnchor(child, root, screenW, screenH);
- bounds = Bounds.union(bounds, collectLinkedBounds(child, screenW, screenH, visited));
- }
- return bounds;
- }
-
- private static boolean fitsLinkedBoundsAt(DraggableHudElement root,
- float desiredRootX,
- float desiredRootY,
- int screenW,
- int screenH) {
- Bounds bounds = collectLinkedBounds(root, screenW, screenH);
- if (bounds == null) return false;
- float dx = desiredRootX - root.getX();
- float dy = desiredRootY - root.getY();
- return bounds.minX() + dx >= 0f
- && bounds.minY() + dy >= 0f
- && bounds.maxX() + dx <= screenW
- && bounds.maxY() + dy <= screenH;
- }
-
- private static boolean isDescendantOf(DraggableHudElement possibleDescendant, DraggableHudElement ancestor) {
- if (possibleDescendant == null || ancestor == null) return false;
- String ancestorId = ancestor.getId();
- Set visited = new HashSet<>();
- DraggableHudElement current = possibleDescendant;
- while (current != null && current.getParentId() != null && current.getParentSide() != WidgetAnchorSide.NONE) {
- if (!visited.add(current.getId())) return false;
- if (ancestorId.equals(current.getParentId())) return true;
- current = findById(current.getParentId());
- }
- return false;
- }
-
- private static void noteRendered(DraggableHudElement w) {
- if (w == null) return;
- w.markRendered(++renderOrderSeq);
- }
-
- private record StickySnap(boolean hasX, boolean hasY, float x, float y) {
- }
-
- private record WidgetAnchorCandidate(DraggableHudElement parent, WidgetAnchorSide side, float offsetX,
- float offsetY) {
- }
-
- private record LinkWireColors(int wireStart, int wireEnd, int previewStart, int previewEnd) {
- }
-
- private record Bounds(float minX, float minY, float maxX, float maxY) {
- static Bounds of(DraggableHudElement w) {
- return new Bounds(w.getX(), w.getY(), w.getX() + w.getWidth(), w.getY() + w.getHeight());
- }
-
- static Bounds union(Bounds a, Bounds b) {
- if (a == null) return b;
- if (b == null) return a;
- return new Bounds(
- Math.min(a.minX, b.minX),
- Math.min(a.minY, b.minY),
- Math.max(a.maxX, b.maxX),
- Math.max(a.maxY, b.maxY)
- );
- }
-
- float width() {
- return maxX - minX;
- }
-
- float height() {
- return maxY - minY;
- }
+ RegistryWidgetRenderPath.renderEngineWidget(id, renderer, textRenderer, ctx, tickDelta);
}
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java
index 0f972d1..fef1ae2 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java
@@ -7,24 +7,27 @@
package dev.loki.lovisual.features.gui.hud.draggable.impl;
-import dev.loki.lovisual.features.theme.Theme;
import com.mojang.blaze3d.platform.Window;
-import dev.loki.lovisual.util.screen.ClientScreen;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiGraphicsExtractor;
-import net.minecraft.client.gui.screens.ChatScreen;
-import net.minecraft.resources.Identifier;
import dev.loki.lovisual.config.SettingDef;
-import dev.loki.lovisual.events.impl.RenderPrewarmCollectEvent;
import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.ModeValue;
import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.events.impl.RenderPrewarmCollectEvent;
import dev.loki.lovisual.features.gui.hud.HudElementRegister;
import dev.loki.lovisual.features.gui.hud.HudGlobalConfig;
-import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
-import dev.loki.lovisual.features.theme.Themes;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastCardRenderer;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.MessengerSide;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastPosition;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.PreviewToast;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastDraw;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastVisual;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastStyle;
import dev.loki.lovisual.features.module.HudPhase;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleManager;
@@ -33,11 +36,15 @@ import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.math.HudScale;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
-import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer;
+import dev.loki.lovisual.util.screen.ClientScreen;
import dev.loki.lovisual.util.wav.CustomSoundEngine;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphicsExtractor;
+import net.minecraft.client.gui.screens.ChatScreen;
+import net.minecraft.resources.Identifier;
import java.util.ArrayList;
import java.util.Iterator;
@@ -52,43 +59,9 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
private static final float BASE_MARGIN = 9f;
private static final float BASE_GAP = 3f;
private static final float BASE_BOTTOM_OFFSET = 160f;
-
- private static final float BASE_RADIUS = 4f;
- private static final float BASE_BOX_HEIGHT = 16f;
- private static final float BASE_LEFT_PAD = 7f;
- private static final float BASE_RIGHT_PAD = 9f;
- private static final float BASE_ICON_PIPE_GAP = 5f;
- private static final float BASE_TOGGLE_PIPE_GAP = 4.5f;
- private static final float BASE_PIPE_TEXT_GAP = 6f;
- private static final float BASE_TOGGLE_W = 20f;
- private static final float BASE_TOGGLE_H = 12f;
- private static final float BASE_PROGRESS_H = 1.35f;
- private static final float GLASS_BLUR_ALPHA = 255f;
- private static final float GLASS_PASS_ALPHA = 255f;
- private static final float TEXT_ALPHA_FACTOR = 0.88f;
- private static final float PIPE_ALPHA_FACTOR = 0.72f;
- private static final float TEXT_DARKEN_MIN = 0.78f;
- private static final float TEXT_DARKEN_MAX = 1.00f;
- private static final float TEXT_ANIM_SPEED = 2.2f;
private static final float LAYOUT_SMOOTH_SPEED = 14.0f;
- private static final float BASE_TOAST_SLIDE = 7.0f;
private static final float LAYOUT_SNAP_EPS = 0.12f;
- private static final String ICON_NO = "I";
- private static final String ICON_INFO = "J";
- private static final String ICON_YES = "K";
- private static final String ICON_WARN = "L";
-
- private static final int ENTER_MS = 400;
- private static final int DEFAULT_EXIT_MS = 260;
- private static final long PREVIEW_ROTATE_MS = 1000L;
- private static final int TOGGLE_TRANSITION_MS = 420;
- private static final String PREVIEW_KEY = "preview";
- private static final PreviewToast[] PREVIEW_EXAMPLES = new PreviewToast[]{
- new PreviewToast("Module - enabled!", NotifyType.YES, ToastVisual.MODULE_TOGGLE, true),
- new PreviewToast("Module - disabled!", NotifyType.NO, ToastVisual.MODULE_TOGGLE, false)
- };
-
private final NumberValue durationMs = new NumberValue<>("duration_ms", 2000, 500, 6000);
private final NumberValue fadeMs = new NumberValue<>("fade_ms", 250, 0, 1500);
private final NumberValue scaleValue = new NumberValue<>("scale", 1.35, 0.5, 3.0);
@@ -149,196 +122,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
INSTANCE.renderPreviewInternal(renderer, textRenderer, x, y, w, h);
}
- private static ToastPosition toastSlideOffset(MessengerSide side, float anim, float baseScale) {
- float offset = (1.0f - clamp01(anim)) * BASE_TOAST_SLIDE * baseScale;
- return switch (side) {
- case LEFT -> new ToastPosition(-offset, 0f);
- case TOP -> new ToastPosition(0f, -offset);
- case BOTTOM -> new ToastPosition(0f, offset);
- default -> new ToastPosition(offset, 0f);
- };
- }
-
- private static PreviewToast previewExample(long now) {
- int index = (int) ((Math.max(0L, now) / PREVIEW_ROTATE_MS) % PREVIEW_EXAMPLES.length);
- return PREVIEW_EXAMPLES[index];
- }
-
- private static long previewStart(long now) {
- long safeNow = Math.max(0L, now);
- return safeNow - safeNow % PREVIEW_ROTATE_MS;
- }
-
- private static ToastMetrics measureToast(Toast toast,
- TextRenderer lineRenderer,
- TextRenderer iconRenderer,
- float lineScale,
- float iconScale,
- float baseScale) {
- boolean moduleToggle = toast.visual == ToastVisual.MODULE_TOGGLE;
- String icon = iconFor(toast.type);
- float iconW = moduleToggle ? BASE_TOGGLE_W * baseScale : measureWidth(iconRenderer, icon, iconScale);
- float iconH = moduleToggle ? BASE_TOGGLE_H * baseScale : measureHeight(iconRenderer, iconScale);
- float pipeW = measureWidth(lineRenderer, "|", lineScale);
- float textW = measureWidth(lineRenderer, toast.line, lineScale);
- if (PREVIEW_KEY.equals(toast.key)) {
- textW = Math.max(textW, previewMaxTextWidth(lineRenderer, lineScale));
- }
- float textH = measureHeight(lineRenderer, lineScale);
-
- float leftPad = BASE_LEFT_PAD * baseScale;
- float rightPad = BASE_RIGHT_PAD * baseScale;
- float iconPipeGap = (moduleToggle ? BASE_TOGGLE_PIPE_GAP : BASE_ICON_PIPE_GAP) * baseScale;
- float pipeTextGap = BASE_PIPE_TEXT_GAP * baseScale;
-
- float boxW = leftPad + iconW + iconPipeGap + pipeW + pipeTextGap + textW + rightPad;
- float boxH = Math.max(BASE_BOX_HEIGHT * baseScale, Math.max(textH, iconH) + (6.0f * baseScale));
- float radius = boxH * 0.5f;
- float iconX = leftPad;
- float iconY = (boxH - iconH) * 0.5f;
- float pipeX = iconX + iconW + iconPipeGap;
- float textX = pipeX + pipeW + pipeTextGap;
- float textY = (boxH - textH) * 0.5f + (0.8f * baseScale);
-
- return new ToastMetrics(boxW, boxH, radius, textScale(lineScale), iconScale, iconX, iconY, iconW, iconH, pipeX, textX, textY);
- }
-
- private static float previewMaxTextWidth(TextRenderer renderer, float scale) {
- float max = 0f;
- for (PreviewToast preview : PREVIEW_EXAMPLES) {
- max = Math.max(max, measureWidth(renderer, preview.line(), scale));
- }
- return max;
- }
-
- private static float textScale(float value) {
- return value;
- }
-
- private static ToastPalette paletteFor() {
- int progressTrack = 0x33FFFFFF;
- int progressFill = 0xC4FFFFFF;
- int bg = 0x52000000;
- return new ToastPalette(
- bg,
- bg,
- 0xFFF7FAFF,
- progressTrack,
- progressFill,
- 0xFFFFFFFF
- );
- }
-
- private static NotifyType resolveType(String text, Boolean enabled) {
- if (enabled != null) return enabled ? NotifyType.YES : NotifyType.NO;
- String lower = text.toLowerCase();
- if (lower.contains("warn") || lower.contains("warning") || lower.contains("cooldown")) {
- return NotifyType.WARN;
- }
- if (lower.contains("error") || lower.contains("failed")
- || lower.contains("fail") || lower.contains("denied")
- || lower.contains("blocked") || lower.contains("disabled")) {
- return NotifyType.NO;
- }
- return NotifyType.INFO;
- }
-
- private static String iconFor(NotifyType type) {
- return switch (type) {
- case YES -> ICON_YES;
- case NO -> ICON_NO;
- case WARN -> ICON_WARN;
- case INFO -> ICON_INFO;
- };
- }
-
- private static int colorFor(NotifyType type) {
- return switch (type) {
- case YES -> 0xFF6DFF56;
- case NO -> 0xFFFF5656;
- case WARN -> 0xFFF5FF5E;
- case INFO -> 0xFF4AA3FF;
- };
- }
-
- private static int moduleToggleColor(float progress, float anim) {
- Themes.Theme theme = Theme.theme();
- int off = theme != null ? HudRenderUtil.mixColor(theme.textMuted(), theme.textPrimary(), 0.18f) : 0xFFC6D2CD;
- int on = theme != null ? theme.accent() : 0xFF5CC8E7;
- int color = HudRenderUtil.mixColor(off, on, AnimationUtility.clamp01(progress));
- return applyAlpha(color, anim);
- }
-
- private static String resolveLine(String text, Boolean enabled) {
- if (enabled != null) {
- return text + " - " + (enabled ? "enabled!" : "disabled!");
- }
- return text;
- }
-
- private static float easeOutBack(float t) {
- float c1 = 1.70158f;
- float c3 = c1 + 1f;
- float p = t - 1f;
- return 1f + c3 * p * p * p + c1 * p * p;
- }
-
- private static float clamp01(float v) {
- return Math.max(0f, Math.min(1f, v));
- }
-
- private static int applyAlpha(int argb, float alpha) {
- int a = (int) (((argb >>> 24) & 0xFF) * alpha);
- return (argb & 0x00FFFFFF) | ((a & 0xFF) << 24);
- }
-
- private static float measureWidth(TextRenderer renderer, String text, float scale) {
- if (renderer == null || text == null || text.isEmpty()) return 0f;
- renderer.begin(scale, true, false);
- float width = (float) renderer.getWidth(text, false);
- renderer.end();
- return width;
- }
-
- private static float measureHeight(TextRenderer renderer, float scale) {
- if (renderer == null) return 0f;
- renderer.begin(scale, true, false);
- float height = (float) renderer.getHeight(false);
- renderer.end();
- return height;
- }
-
- private static int scaleAlpha(int argb, float factor) {
- factor = AnimationUtility.clamp01(factor);
- int a = (argb >>> 24) & 0xFF;
- int scaledA = Math.round(a * factor);
- return (argb & 0x00FFFFFF) | ((scaledA & 0xFF) << 24);
- }
-
- private static int darken(int argb, float factor) {
- factor = AnimationUtility.clamp(factor, 0f, 1f);
- int a = (argb >>> 24) & 0xFF;
- int r = (argb >>> 16) & 0xFF;
- int g = (argb >>> 8) & 0xFF;
- int b = argb & 0xFF;
-
- r = Math.round(r * factor);
- g = Math.round(g * factor);
- b = Math.round(b * factor);
-
- return ((a & 0xFF) << 24)
- | ((r & 0xFF) << 16)
- | ((g & 0xFF) << 8)
- | (b & 0xFF);
- }
-
- private static void applyColor(RenderColor out, int argb) {
- out.a = (argb >>> 24) & 0xFF;
- out.r = (argb >>> 16) & 0xFF;
- out.g = (argb >>> 8) & 0xFF;
- out.b = argb & 0xFF;
- }
-
@Override
protected void defineSettings(List defs) {
defs.add(SettingDef.number(durationMs));
@@ -360,7 +143,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
float margin = BASE_MARGIN * scale;
float width = 170f * scale;
- float height = BASE_BOX_HEIGHT * scale;
+ float height = ToastGeometry.BASE_BOX_HEIGHT * scale;
// Position side is intentionally automatic now; the draggable position itself defines slide direction.
this.x = Math.max(margin, screenW - margin - width);
this.y = Math.max(margin, screenH - margin - BASE_BOTTOM_OFFSET * scale - height);
@@ -440,7 +223,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
float anim = sample ? 1.0f : toast.anim(now, duration, fade);
if (anim <= 0.001f) continue;
- ToastMetrics metrics = measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale);
+ ToastMetrics metrics = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale);
float remaining = sample ? 0.45f : toast.remaining(now, duration);
draws.add(new ToastDraw(toast, metrics, remaining, anim, 0f, 0f));
maxW = Math.max(maxW, metrics.boxW);
@@ -472,17 +255,18 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
};
float targetY = cursorY;
ToastPosition pos = resolveToastPosition(draw.toast, targetX, targetY, sample, baseScale);
- ToastPosition slide = toastSlideOffset(side, draw.anim, baseScale);
+ ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale);
draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, pos.x() + slide.x(), pos.y() + slide.y()));
cursorY += (metrics.boxH + gap) * draw.anim;
}
setBounds(x, y, layoutW, Math.max(1f, layoutH));
- drawGlassBlurBatch(draws, baseScale);
+ ToastGeometry.drawGlassBlurBatch(draws, baseScale);
for (ToastDraw draw : draws) {
- drawToastCard(
+ ToastCardRenderer.drawToastCard(
renderer,
+ colorTmp,
draw.toast,
lineRenderer,
iconRenderer,
@@ -559,9 +343,9 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
}
private Toast previewToast(long now) {
- PreviewToast preview = previewExample(now);
- long start = previewStart(now);
- return new Toast(PREVIEW_KEY, preview.line(), preview.type(), start, preview.visual(), preview.enabled());
+ PreviewToast preview = ToastModel.previewExample(now);
+ long start = ToastModel.previewStart(now);
+ return new Toast(ToastModel.PREVIEW_KEY, preview.line(), preview.type(), start, preview.visual(), preview.enabled());
}
private void renderPreviewInternal(Renderer2D renderer,
@@ -608,7 +392,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
float anim = sampleMode ? 1.0f : toast.anim(now, duration, fade);
if (anim <= 0.001f) continue;
- ToastMetrics metrics = measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale);
+ ToastMetrics metrics = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale);
float remaining = sampleMode ? 0.45f : toast.remaining(now, duration);
maxW = Math.max(maxW, metrics.boxW);
draws.add(new ToastDraw(toast, metrics, remaining, anim, 0f, 0f));
@@ -623,15 +407,16 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
default -> originX + screenW - margin - metrics.boxW;
};
float targetY = originY + cursorY;
- ToastPosition slide = toastSlideOffset(side, draw.anim, baseScale);
+ ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale);
draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, targetX + slide.x(), targetY + slide.y()));
cursorY += (metrics.boxH + gap) * draw.anim;
}
- drawGlassBlurBatch(draws, baseScale);
+ ToastGeometry.drawGlassBlurBatch(draws, baseScale);
for (ToastDraw draw : draws) {
- drawToastCard(
+ ToastCardRenderer.drawToastCard(
renderer,
+ colorTmp,
draw.toast,
lineRenderer,
iconRenderer,
@@ -665,7 +450,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
}
private void enqueue(String key, String text, Boolean enabled, NotifyType forcedType) {
- NotifyType type = forcedType != null ? forcedType : resolveType(text, enabled);
+ NotifyType type = forcedType != null ? forcedType : ToastStyle.resolveType(text, enabled);
enqueueInternal(key, text, enabled, type, ToastVisual.STANDARD);
}
@@ -677,7 +462,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
if (!enabledValue().get()) return;
if (hud.getHeaderMode() == HudGlobalConfig.HeaderMode.NEVER) return;
- String line = resolveLine(text, enabled);
+ String line = ToastStyle.resolveLine(text, enabled);
long now = System.currentTimeMillis();
if (key != null) {
@@ -703,225 +488,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
}
}
- private void drawToastCard(Renderer2D renderer,
- Toast toast,
- TextRenderer lineRenderer,
- TextRenderer iconRenderer,
- float x,
- float y,
- ToastMetrics metrics,
- float remaining,
- float anim,
- float baseScale) {
- ToastPalette palette = paletteFor();
-
- int glassTint = applyAlpha(palette.glassTint, anim);
- int bgLeft = applyAlpha(palette.bgLeft, anim);
- int bgRight = applyAlpha(palette.bgRight, anim);
- long now = System.currentTimeMillis();
- float wave = 0.5f + 0.5f * (float) Math.sin(now / 1000.0f * TEXT_ANIM_SPEED);
- float darkenFactor = AnimationUtility.lerp(TEXT_DARKEN_MIN, TEXT_DARKEN_MAX, wave);
-
- int text = scaleAlpha(applyAlpha(palette.text, anim), TEXT_ALPHA_FACTOR);
- text = darken(text, darkenFactor);
-
- int progressTrack = applyAlpha(palette.progressTrack, anim);
- int progress = applyAlpha(palette.progressFill, anim);
- int accent = applyAlpha(colorFor(toast.type), anim);
-
- int pipe = scaleAlpha(applyAlpha(HudRenderUtil.mixColor(palette.text, palette.progressTrack, 0.55f), anim), PIPE_ALPHA_FACTOR);
- pipe = darken(pipe, darkenFactor * 0.96f);
- float sideBottomR = Math.max(1f, metrics.boxH * 0.5f);
- float sideTopR = 0f;
-
- drawGlassPill(
- renderer,
- x,
- y,
- metrics.boxW,
- metrics.boxH,
- sideTopR,
- sideTopR,
- sideBottomR,
- sideBottomR,
- glassTint,
- anim
- );
-
- int bgMixed = HudRenderUtil.mixColor(bgLeft, bgRight, 0.5f);
- int bgA = (bgMixed >>> 24) & 0xFF;
- int bgSolid = HudRenderUtil.glassBackground(anim);
- renderer.roundedRectCornersQuad(
- x,
- y,
- metrics.boxW,
- metrics.boxH,
- sideTopR,
- sideTopR,
- sideBottomR,
- sideBottomR,
- 1.0f,
- bgSolid,
- bgSolid,
- bgSolid,
- bgSolid
- );
-
- float progressInsetX = 0.45f * baseScale;
- float progressInsetY = 0.08f * baseScale;
- float progressH = Math.max(1f, BASE_PROGRESS_H * baseScale);
- float progressY = y + progressInsetY;
- float progressX = x + progressInsetX;
- float progressAreaW = Math.max(0f, metrics.boxW - progressInsetX * 2f);
- float progressW = Math.max(0f, progressAreaW * clamp01(remaining));
- float progressTopR = 0f;
- float progressBottomR = 0.02f * baseScale;
- renderer.roundedRectCornersQuad(
- progressX,
- progressY,
- progressAreaW,
- progressH,
- progressTopR,
- progressTopR,
- progressBottomR,
- progressBottomR,
- 1.0f,
- progressTrack,
- progressTrack,
- progressTrack,
- progressTrack
- );
- if (progressW > 0f) {
- renderer.roundedRectCornersQuad(
- progressX,
- progressY,
- progressW,
- progressH,
- progressTopR,
- progressTopR,
- progressBottomR,
- progressBottomR,
- 1.0f,
- progress,
- progress,
- progress,
- progress
- );
- }
-
- if (toast.visual == ToastVisual.MODULE_TOGGLE) {
- drawModuleToggle(renderer, toast, x + metrics.iconX, y + metrics.iconY, metrics.iconW, metrics.iconH, accent, anim, now);
- } else {
- iconRenderer.begin(metrics.iconScale, false, false);
- applyColor(colorTmp, accent);
- iconRenderer.render(iconFor(toast.type), x + metrics.iconX, y + metrics.iconY, colorTmp, false);
- iconRenderer.end();
- }
-
- lineRenderer.begin(metrics.textScale, false, false);
- applyColor(colorTmp, pipe);
- lineRenderer.render("|", x + metrics.pipeX, y + metrics.textY, colorTmp, false);
- applyColor(colorTmp, text);
- lineRenderer.render(toast.line, x + metrics.textX, y + metrics.textY, colorTmp, false);
- lineRenderer.end();
- }
-
- private void drawModuleToggle(Renderer2D renderer,
- Toast toast,
- float x,
- float y,
- float w,
- float h,
- int fallbackAccent,
- float anim,
- long now) {
- if (renderer == null || w <= 0.1f || h <= 0.1f) return;
-
- float progress = toast.toggleProgress(now);
- int tint = moduleToggleColor(progress, anim);
- if (((tint >>> 24) & 0xFF) <= 0) {
- tint = fallbackAccent;
- }
-
- float svgY = y + (h - w) * 0.5f;
- SvgRenderOptions options = SvgRenderOptions.overrideColor(tint);
- if (progress <= 0.001f) {
- renderer.svg("toggle-left", x, svgY, w, w, options);
- return;
- }
- if (progress >= 0.999f) {
- renderer.svg("toggle-right", x, svgY, w, w, options);
- return;
- }
-
- renderer.svg("toggle-track", x, svgY, w, w, options);
-
- float thumbTravel = w * (3f / 24f);
- float thumbX = x + AnimationUtility.lerp(-thumbTravel, thumbTravel, progress);
- renderer.svg("toggle-thumb", thumbX, svgY, w, w, options);
- }
-
- private void drawGlassBlurBatch(List draws, float baseScale) {
- if (draws == null || draws.isEmpty()) return;
- HudGlobalConfig hud = HudGlobalConfig.get();
- if (hud == null) return;
-
- Renderer2D.COLOR.blurComposite(composite -> {
- for (ToastDraw draw : draws) {
- if (draw == null || draw.metrics == null || draw.anim <= 0.001f) continue;
- float a = clamp01(draw.anim);
- float sideBottomR = Math.max(1f, draw.metrics.boxH * 0.5f);
- composite.roundedRectCorners(
- draw.x,
- draw.y,
- draw.metrics.boxW,
- draw.metrics.boxH,
- 0f,
- 0f,
- sideBottomR,
- sideBottomR,
- hud.getBlurRadius(),
- 1.0f,
- (GLASS_BLUR_ALPHA / 255.0f) * a,
- 0xFFFFFF
- );
- }
- });
- }
-
- private void drawGlassPill(Renderer2D renderer,
- float x,
- float y,
- float w,
- float h,
- float radiusTL,
- float radiusTR,
- float radiusBR,
- float radiusBL,
- int tintArgb,
- float anim) {
- if (renderer == null) return;
- float a = clamp01(anim);
- if (a <= 0.001f) return;
-
- float glassAlpha = (GLASS_PASS_ALPHA / 255.0f) * a;
- float glassScale = Math.max(0.50f, Math.min(1.25f, h / 24.0f));
- HudRenderUtil.drawLiquidGlassCorners(
- x,
- y,
- w,
- h,
- radiusTL,
- radiusTR,
- radiusBR,
- radiusBL,
- glassScale,
- false,
- glassAlpha,
- tintArgb
- );
- }
-
private int getDurationMs() {
return durationMs.get();
}
@@ -978,23 +544,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
return Identifier.fromNamespaceAndPath("lovisual", "sounds/" + base + ".wav");
}
- private enum MessengerSide {
- RIGHT,
- LEFT,
- TOP,
- BOTTOM;
-
- static MessengerSide from(String name) {
- if (name == null) return RIGHT;
- return switch (name.toLowerCase()) {
- case "left" -> LEFT;
- case "top" -> TOP;
- case "bottom" -> BOTTOM;
- default -> RIGHT;
- };
- }
- }
-
public enum NotifyType {
YES,
NO,
@@ -1002,107 +551,9 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat
INFO
}
- private enum ToastVisual {
- STANDARD,
- MODULE_TOGGLE
- }
-
- private static final class Toast {
- private final String key;
- private String line;
- private NotifyType type;
- private ToastVisual visual;
- private Boolean stateEnabled;
- private long startMs;
- private boolean layoutReady;
- private float layoutX;
- private float layoutY;
-
- private Toast(String key, String line, NotifyType type, long startMs) {
- this(key, line, type, startMs, ToastVisual.STANDARD, null);
- }
-
- private Toast(String key, String line, NotifyType type, long startMs, ToastVisual visual, Boolean stateEnabled) {
- this.key = key;
- this.line = line;
- this.type = type;
- this.visual = visual == null ? ToastVisual.STANDARD : visual;
- this.stateEnabled = stateEnabled;
- this.startMs = startMs;
- }
-
- boolean isExpired(long now, int duration) {
- return now - startMs > duration;
- }
-
- float remaining(long now, int duration) {
- if (duration <= 0) return 0f;
- long age = Math.max(0L, now - startMs);
- return 1f - clamp01((float) age / (float) duration);
- }
-
- float anim(long now, int duration, int fade) {
- long age = Math.max(0L, now - startMs);
- if (age >= duration) return 0f;
-
- float in = clamp01((float) age / (float) ENTER_MS);
- float enter = easeOutBack(in);
-
- int outMs = Math.max(1, fade > 0 ? fade : DEFAULT_EXIT_MS);
- float out = 1f;
- if (age > duration - outMs) {
- out = clamp01((float) (duration - age) / (float) outMs);
- }
-
- return clamp01(enter * out);
- }
-
- float toggleProgress(long now) {
- boolean enabled = stateEnabled != null ? stateEnabled : type == NotifyType.YES;
- long age = Math.max(0L, now - startMs);
- float t = AnimationUtility.easeInOutCubic((float) age / (float) TOGGLE_TRANSITION_MS);
- return enabled ? t : 1f - t;
- }
- }
-
- private record PreviewToast(String line, NotifyType type, ToastVisual visual, Boolean enabled) {
- }
-
private record LayoutSize(float width, float height) {
}
- private record ToastPosition(float x, float y) {
- }
-
- private record ToastDraw(Toast toast,
- ToastMetrics metrics,
- float remaining,
- float anim,
- float x,
- float y) {
- }
-
- private record ToastMetrics(float boxW,
- float boxH,
- float radius,
- float textScale,
- float iconScale,
- float iconX,
- float iconY,
- float iconW,
- float iconH,
- float pipeX,
- float textX,
- float textY) {
- }
-
- private record ToastPalette(int bgLeft,
- int bgRight,
- int text,
- int progressTrack,
- int progressFill,
- int glassTint) {
- }
@Override
public void collectRenderPrewarm(RenderPrewarmCollectEvent event) {
super.collectRenderPrewarm(event);
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java
new file mode 100644
index 0000000..9113e3b
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java
@@ -0,0 +1,193 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier;
+
+import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastVisual;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastStyle.ToastPalette;
+import dev.loki.lovisual.render.engine.animation.AnimationUtility;
+import dev.loki.lovisual.render.engine.color.RenderColor;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+
+/**
+ * Card drawing routines for HudNotifier toasts: glass pill background, progress bar
+ * and the icon/pipe/text layout of a single toast card.
+ */
+public final class ToastCardRenderer {
+
+ public static final float BASE_PROGRESS_H = 1.35f;
+ public static final float GLASS_PASS_ALPHA = 255f;
+ public static final float TEXT_ALPHA_FACTOR = 0.88f;
+ public static final float PIPE_ALPHA_FACTOR = 0.72f;
+ public static final float TEXT_DARKEN_MIN = 0.78f;
+ public static final float TEXT_DARKEN_MAX = 1.00f;
+ public static final float TEXT_ANIM_SPEED = 2.2f;
+
+ private ToastCardRenderer() {
+ }
+
+ public static void drawToastCard(Renderer2D renderer,
+ RenderColor colorTmp,
+ Toast toast,
+ TextRenderer lineRenderer,
+ TextRenderer iconRenderer,
+ float x,
+ float y,
+ ToastMetrics metrics,
+ float remaining,
+ float anim,
+ float baseScale) {
+ ToastPalette palette = ToastStyle.paletteFor();
+
+ int glassTint = ToastStyle.applyAlpha(palette.glassTint, anim);
+ int bgLeft = ToastStyle.applyAlpha(palette.bgLeft, anim);
+ int bgRight = ToastStyle.applyAlpha(palette.bgRight, anim);
+ long now = System.currentTimeMillis();
+ float wave = 0.5f + 0.5f * (float) Math.sin(now / 1000.0f * TEXT_ANIM_SPEED);
+ float darkenFactor = AnimationUtility.lerp(TEXT_DARKEN_MIN, TEXT_DARKEN_MAX, wave);
+
+ int text = ToastStyle.scaleAlpha(ToastStyle.applyAlpha(palette.text, anim), TEXT_ALPHA_FACTOR);
+ text = ToastStyle.darken(text, darkenFactor);
+
+ int progressTrack = ToastStyle.applyAlpha(palette.progressTrack, anim);
+ int progress = ToastStyle.applyAlpha(palette.progressFill, anim);
+ int accent = ToastStyle.applyAlpha(ToastStyle.colorFor(toast.type), anim);
+
+ int pipe = ToastStyle.scaleAlpha(ToastStyle.applyAlpha(HudRenderUtil.mixColor(palette.text, palette.progressTrack, 0.55f), anim), PIPE_ALPHA_FACTOR);
+ pipe = ToastStyle.darken(pipe, darkenFactor * 0.96f);
+ float sideBottomR = Math.max(1f, metrics.boxH * 0.5f);
+ float sideTopR = 0f;
+
+ drawGlassPill(
+ renderer,
+ x,
+ y,
+ metrics.boxW,
+ metrics.boxH,
+ sideTopR,
+ sideTopR,
+ sideBottomR,
+ sideBottomR,
+ glassTint,
+ anim
+ );
+
+ int bgMixed = HudRenderUtil.mixColor(bgLeft, bgRight, 0.5f);
+ int bgA = (bgMixed >>> 24) & 0xFF;
+ int bgSolid = HudRenderUtil.glassBackground(anim);
+ renderer.roundedRectCornersQuad(
+ x,
+ y,
+ metrics.boxW,
+ metrics.boxH,
+ sideTopR,
+ sideTopR,
+ sideBottomR,
+ sideBottomR,
+ 1.0f,
+ bgSolid,
+ bgSolid,
+ bgSolid,
+ bgSolid
+ );
+
+ float progressInsetX = 0.45f * baseScale;
+ float progressInsetY = 0.08f * baseScale;
+ float progressH = Math.max(1f, BASE_PROGRESS_H * baseScale);
+ float progressY = y + progressInsetY;
+ float progressX = x + progressInsetX;
+ float progressAreaW = Math.max(0f, metrics.boxW - progressInsetX * 2f);
+ float progressW = Math.max(0f, progressAreaW * ToastModel.clamp01(remaining));
+ float progressTopR = 0f;
+ float progressBottomR = 0.02f * baseScale;
+ renderer.roundedRectCornersQuad(
+ progressX,
+ progressY,
+ progressAreaW,
+ progressH,
+ progressTopR,
+ progressTopR,
+ progressBottomR,
+ progressBottomR,
+ 1.0f,
+ progressTrack,
+ progressTrack,
+ progressTrack,
+ progressTrack
+ );
+ if (progressW > 0f) {
+ renderer.roundedRectCornersQuad(
+ progressX,
+ progressY,
+ progressW,
+ progressH,
+ progressTopR,
+ progressTopR,
+ progressBottomR,
+ progressBottomR,
+ 1.0f,
+ progress,
+ progress,
+ progress,
+ progress
+ );
+ }
+
+ if (toast.visual == ToastVisual.MODULE_TOGGLE) {
+ ToastStyle.drawModuleToggle(renderer, toast, x + metrics.iconX, y + metrics.iconY, metrics.iconW, metrics.iconH, accent, anim, now);
+ } else {
+ iconRenderer.begin(metrics.iconScale, false, false);
+ ToastStyle.applyColor(colorTmp, accent);
+ iconRenderer.render(ToastStyle.iconFor(toast.type), x + metrics.iconX, y + metrics.iconY, colorTmp, false);
+ iconRenderer.end();
+ }
+
+ lineRenderer.begin(metrics.textScale, false, false);
+ ToastStyle.applyColor(colorTmp, pipe);
+ lineRenderer.render("|", x + metrics.pipeX, y + metrics.textY, colorTmp, false);
+ ToastStyle.applyColor(colorTmp, text);
+ lineRenderer.render(toast.line, x + metrics.textX, y + metrics.textY, colorTmp, false);
+ lineRenderer.end();
+ }
+
+ public static void drawGlassPill(Renderer2D renderer,
+ float x,
+ float y,
+ float w,
+ float h,
+ float radiusTL,
+ float radiusTR,
+ float radiusBR,
+ float radiusBL,
+ int tintArgb,
+ float anim) {
+ if (renderer == null) return;
+ float a = ToastModel.clamp01(anim);
+ if (a <= 0.001f) return;
+
+ float glassAlpha = (GLASS_PASS_ALPHA / 255.0f) * a;
+ float glassScale = Math.max(0.50f, Math.min(1.25f, h / 24.0f));
+ HudRenderUtil.drawLiquidGlassCorners(
+ x,
+ y,
+ w,
+ h,
+ radiusTL,
+ radiusTR,
+ radiusBR,
+ radiusBL,
+ glassScale,
+ false,
+ glassAlpha,
+ tintArgb
+ );
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java
new file mode 100644
index 0000000..1761926
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java
@@ -0,0 +1,200 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier;
+
+import dev.loki.lovisual.features.gui.hud.HudGlobalConfig;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.PreviewToast;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastDraw;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastVisual;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+import java.util.List;
+
+/**
+ * Layout and measurement math for HudNotifier toasts: box metrics, text measurement,
+ * slide-in offsets and the per-frame blur backdrop pass over the toast draw list.
+ */
+public final class ToastGeometry {
+
+ public static final float BASE_RADIUS = 4f;
+ public static final float BASE_BOX_HEIGHT = 16f;
+ public static final float BASE_LEFT_PAD = 7f;
+ public static final float BASE_RIGHT_PAD = 9f;
+ public static final float BASE_ICON_PIPE_GAP = 5f;
+ public static final float BASE_TOGGLE_PIPE_GAP = 4.5f;
+ public static final float BASE_PIPE_TEXT_GAP = 6f;
+ public static final float BASE_TOGGLE_W = 20f;
+ public static final float BASE_TOGGLE_H = 12f;
+ public static final float BASE_TOAST_SLIDE = 7.0f;
+ public static final float GLASS_BLUR_ALPHA = 255f;
+
+ private ToastGeometry() {
+ }
+
+ public static ToastPosition toastSlideOffset(MessengerSide side, float anim, float baseScale) {
+ float offset = (1.0f - ToastModel.clamp01(anim)) * BASE_TOAST_SLIDE * baseScale;
+ return switch (side) {
+ case LEFT -> new ToastPosition(-offset, 0f);
+ case TOP -> new ToastPosition(0f, -offset);
+ case BOTTOM -> new ToastPosition(0f, offset);
+ default -> new ToastPosition(offset, 0f);
+ };
+ }
+
+ public static ToastMetrics measureToast(Toast toast,
+ TextRenderer lineRenderer,
+ TextRenderer iconRenderer,
+ float lineScale,
+ float iconScale,
+ float baseScale) {
+ boolean moduleToggle = toast.visual == ToastVisual.MODULE_TOGGLE;
+ String icon = ToastStyle.iconFor(toast.type);
+ float iconW = moduleToggle ? BASE_TOGGLE_W * baseScale : measureWidth(iconRenderer, icon, iconScale);
+ float iconH = moduleToggle ? BASE_TOGGLE_H * baseScale : measureHeight(iconRenderer, iconScale);
+ float pipeW = measureWidth(lineRenderer, "|", lineScale);
+ float textW = measureWidth(lineRenderer, toast.line, lineScale);
+ if (ToastModel.PREVIEW_KEY.equals(toast.key)) {
+ textW = Math.max(textW, previewMaxTextWidth(lineRenderer, lineScale));
+ }
+ float textH = measureHeight(lineRenderer, lineScale);
+
+ float leftPad = BASE_LEFT_PAD * baseScale;
+ float rightPad = BASE_RIGHT_PAD * baseScale;
+ float iconPipeGap = (moduleToggle ? BASE_TOGGLE_PIPE_GAP : BASE_ICON_PIPE_GAP) * baseScale;
+ float pipeTextGap = BASE_PIPE_TEXT_GAP * baseScale;
+
+ float boxW = leftPad + iconW + iconPipeGap + pipeW + pipeTextGap + textW + rightPad;
+ float boxH = Math.max(BASE_BOX_HEIGHT * baseScale, Math.max(textH, iconH) + (6.0f * baseScale));
+ float radius = boxH * 0.5f;
+ float iconX = leftPad;
+ float iconY = (boxH - iconH) * 0.5f;
+ float pipeX = iconX + iconW + iconPipeGap;
+ float textX = pipeX + pipeW + pipeTextGap;
+ float textY = (boxH - textH) * 0.5f + (0.8f * baseScale);
+
+ return new ToastMetrics(boxW, boxH, radius, textScale(lineScale), iconScale, iconX, iconY, iconW, iconH, pipeX, textX, textY);
+ }
+
+ public static float previewMaxTextWidth(TextRenderer renderer, float scale) {
+ float max = 0f;
+ for (PreviewToast preview : ToastModel.PREVIEW_EXAMPLES) {
+ max = Math.max(max, measureWidth(renderer, preview.line(), scale));
+ }
+ return max;
+ }
+
+ public static float textScale(float value) {
+ return value;
+ }
+
+ public static float measureWidth(TextRenderer renderer, String text, float scale) {
+ if (renderer == null || text == null || text.isEmpty()) return 0f;
+ renderer.begin(scale, true, false);
+ float width = (float) renderer.getWidth(text, false);
+ renderer.end();
+ return width;
+ }
+
+ public static float measureHeight(TextRenderer renderer, float scale) {
+ if (renderer == null) return 0f;
+ renderer.begin(scale, true, false);
+ float height = (float) renderer.getHeight(false);
+ renderer.end();
+ return height;
+ }
+
+ public static void drawGlassBlurBatch(List draws, float baseScale) {
+ if (draws == null || draws.isEmpty()) return;
+ HudGlobalConfig hud = HudGlobalConfig.get();
+ if (hud == null) return;
+
+ Renderer2D.COLOR.blurComposite(composite -> {
+ for (ToastDraw draw : draws) {
+ if (draw == null || draw.metrics == null || draw.anim <= 0.001f) continue;
+ float a = ToastModel.clamp01(draw.anim);
+ float sideBottomR = Math.max(1f, draw.metrics.boxH * 0.5f);
+ composite.roundedRectCorners(
+ draw.x,
+ draw.y,
+ draw.metrics.boxW,
+ draw.metrics.boxH,
+ 0f,
+ 0f,
+ sideBottomR,
+ sideBottomR,
+ hud.getBlurRadius(),
+ 1.0f,
+ (GLASS_BLUR_ALPHA / 255.0f) * a,
+ 0xFFFFFF
+ );
+ }
+ });
+ }
+
+ public enum MessengerSide {
+ RIGHT,
+ LEFT,
+ TOP,
+ BOTTOM;
+
+ public static MessengerSide from(String name) {
+ if (name == null) return RIGHT;
+ return switch (name.toLowerCase()) {
+ case "left" -> LEFT;
+ case "top" -> TOP;
+ case "bottom" -> BOTTOM;
+ default -> RIGHT;
+ };
+ }
+ }
+
+ public record ToastPosition(float x, float y) {
+ }
+
+ public static final class ToastMetrics {
+ public final float boxW;
+ public final float boxH;
+ public final float radius;
+ public final float textScale;
+ public final float iconScale;
+ public final float iconX;
+ public final float iconY;
+ public final float iconW;
+ public final float iconH;
+ public final float pipeX;
+ public final float textX;
+ public final float textY;
+
+ public ToastMetrics(float boxW,
+ float boxH,
+ float radius,
+ float textScale,
+ float iconScale,
+ float iconX,
+ float iconY,
+ float iconW,
+ float iconH,
+ float pipeX,
+ float textX,
+ float textY) {
+ this.boxW = boxW;
+ this.boxH = boxH;
+ this.radius = radius;
+ this.textScale = textScale;
+ this.iconScale = iconScale;
+ this.iconX = iconX;
+ this.iconY = iconY;
+ this.iconW = iconW;
+ this.iconH = iconH;
+ this.pipeX = pipeX;
+ this.textX = textX;
+ this.textY = textY;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java
new file mode 100644
index 0000000..41ebec3
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java
@@ -0,0 +1,142 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier;
+
+import dev.loki.lovisual.features.gui.hud.draggable.impl.HudNotifier;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics;
+import dev.loki.lovisual.render.engine.animation.AnimationUtility;
+
+/**
+ * Toast state model for HudNotifier: lifecycle timing, preview rotation data, the
+ * mutable toast entries rendered by the notifier and the per-frame draw items.
+ */
+public final class ToastModel {
+
+ public static final int ENTER_MS = 400;
+ public static final int DEFAULT_EXIT_MS = 260;
+ public static final long PREVIEW_ROTATE_MS = 1000L;
+ public static final int TOGGLE_TRANSITION_MS = 420;
+ public static final String PREVIEW_KEY = "preview";
+ public static final PreviewToast[] PREVIEW_EXAMPLES = new PreviewToast[]{
+ new PreviewToast("Module - enabled!", HudNotifier.NotifyType.YES, ToastVisual.MODULE_TOGGLE, true),
+ new PreviewToast("Module - disabled!", HudNotifier.NotifyType.NO, ToastVisual.MODULE_TOGGLE, false)
+ };
+
+ private ToastModel() {
+ }
+
+ public static PreviewToast previewExample(long now) {
+ int index = (int) ((Math.max(0L, now) / PREVIEW_ROTATE_MS) % PREVIEW_EXAMPLES.length);
+ return PREVIEW_EXAMPLES[index];
+ }
+
+ public static long previewStart(long now) {
+ long safeNow = Math.max(0L, now);
+ return safeNow - safeNow % PREVIEW_ROTATE_MS;
+ }
+
+ public static float easeOutBack(float t) {
+ float c1 = 1.70158f;
+ float c3 = c1 + 1f;
+ float p = t - 1f;
+ return 1f + c3 * p * p * p + c1 * p * p;
+ }
+
+ public static float clamp01(float v) {
+ return Math.max(0f, Math.min(1f, v));
+ }
+
+ public enum ToastVisual {
+ STANDARD,
+ MODULE_TOGGLE
+ }
+
+ public record PreviewToast(String line, HudNotifier.NotifyType type, ToastVisual visual, Boolean enabled) {
+ }
+
+ public static final class Toast {
+ public final String key;
+ public String line;
+ public HudNotifier.NotifyType type;
+ public ToastVisual visual;
+ public Boolean stateEnabled;
+ public long startMs;
+ public boolean layoutReady;
+ public float layoutX;
+ public float layoutY;
+
+ public Toast(String key, String line, HudNotifier.NotifyType type, long startMs) {
+ this(key, line, type, startMs, ToastVisual.STANDARD, null);
+ }
+
+ public Toast(String key, String line, HudNotifier.NotifyType type, long startMs, ToastVisual visual, Boolean stateEnabled) {
+ this.key = key;
+ this.line = line;
+ this.type = type;
+ this.visual = visual == null ? ToastVisual.STANDARD : visual;
+ this.stateEnabled = stateEnabled;
+ this.startMs = startMs;
+ }
+
+ public boolean isExpired(long now, int duration) {
+ return now - startMs > duration;
+ }
+
+ public float remaining(long now, int duration) {
+ if (duration <= 0) return 0f;
+ long age = Math.max(0L, now - startMs);
+ return 1f - clamp01((float) age / (float) duration);
+ }
+
+ public float anim(long now, int duration, int fade) {
+ long age = Math.max(0L, now - startMs);
+ if (age >= duration) return 0f;
+
+ float in = clamp01((float) age / (float) ENTER_MS);
+ float enter = easeOutBack(in);
+
+ int outMs = Math.max(1, fade > 0 ? fade : DEFAULT_EXIT_MS);
+ float out = 1f;
+ if (age > duration - outMs) {
+ out = clamp01((float) (duration - age) / (float) outMs);
+ }
+
+ return clamp01(enter * out);
+ }
+
+ public float toggleProgress(long now) {
+ boolean enabled = stateEnabled != null ? stateEnabled : type == HudNotifier.NotifyType.YES;
+ long age = Math.max(0L, now - startMs);
+ float t = AnimationUtility.easeInOutCubic((float) age / (float) TOGGLE_TRANSITION_MS);
+ return enabled ? t : 1f - t;
+ }
+ }
+
+ public static final class ToastDraw {
+ public final Toast toast;
+ public final ToastMetrics metrics;
+ public final float remaining;
+ public final float anim;
+ public final float x;
+ public final float y;
+
+ public ToastDraw(Toast toast,
+ ToastMetrics metrics,
+ float remaining,
+ float anim,
+ float x,
+ float y) {
+ this.toast = toast;
+ this.metrics = metrics;
+ this.remaining = remaining;
+ this.anim = anim;
+ this.x = x;
+ this.y = y;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java
new file mode 100644
index 0000000..227a05e
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java
@@ -0,0 +1,190 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier;
+
+import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.HudNotifier;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.HudNotifier.NotifyType;
+import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast;
+import dev.loki.lovisual.features.theme.Theme;
+import dev.loki.lovisual.features.theme.Themes;
+import dev.loki.lovisual.render.engine.animation.AnimationUtility;
+import dev.loki.lovisual.render.engine.color.RenderColor;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
+
+/**
+ * Color and icon styling for HudNotifier toasts: palettes, notification type colors,
+ * the small ARGB helpers shared by the toast drawing routines and the module-toggle
+ * switch artwork.
+ */
+public final class ToastStyle {
+
+ public static final String ICON_NO = "I";
+ public static final String ICON_INFO = "J";
+ public static final String ICON_YES = "K";
+ public static final String ICON_WARN = "L";
+
+ private ToastStyle() {
+ }
+
+ public static ToastPalette paletteFor() {
+ int progressTrack = 0x33FFFFFF;
+ int progressFill = 0xC4FFFFFF;
+ int bg = 0x52000000;
+ return new ToastPalette(
+ bg,
+ bg,
+ 0xFFF7FAFF,
+ progressTrack,
+ progressFill,
+ 0xFFFFFFFF
+ );
+ }
+
+ public static NotifyType resolveType(String text, Boolean enabled) {
+ if (enabled != null) return enabled ? NotifyType.YES : NotifyType.NO;
+ String lower = text.toLowerCase();
+ if (lower.contains("warn") || lower.contains("warning") || lower.contains("cooldown")) {
+ return NotifyType.WARN;
+ }
+ if (lower.contains("error") || lower.contains("failed")
+ || lower.contains("fail") || lower.contains("denied")
+ || lower.contains("blocked") || lower.contains("disabled")) {
+ return NotifyType.NO;
+ }
+ return NotifyType.INFO;
+ }
+
+ public static String iconFor(NotifyType type) {
+ return switch (type) {
+ case YES -> ICON_YES;
+ case NO -> ICON_NO;
+ case WARN -> ICON_WARN;
+ case INFO -> ICON_INFO;
+ };
+ }
+
+ public static int colorFor(NotifyType type) {
+ return switch (type) {
+ case YES -> 0xFF6DFF56;
+ case NO -> 0xFFFF5656;
+ case WARN -> 0xFFF5FF5E;
+ case INFO -> 0xFF4AA3FF;
+ };
+ }
+
+ public static int moduleToggleColor(float progress, float anim) {
+ Themes.Theme theme = Theme.theme();
+ int off = theme != null ? HudRenderUtil.mixColor(theme.textMuted(), theme.textPrimary(), 0.18f) : 0xFFC6D2CD;
+ int on = theme != null ? theme.accent() : 0xFF5CC8E7;
+ int color = HudRenderUtil.mixColor(off, on, AnimationUtility.clamp01(progress));
+ return applyAlpha(color, anim);
+ }
+
+ public static String resolveLine(String text, Boolean enabled) {
+ if (enabled != null) {
+ return text + " - " + (enabled ? "enabled!" : "disabled!");
+ }
+ return text;
+ }
+
+ public static int applyAlpha(int argb, float alpha) {
+ int a = (int) (((argb >>> 24) & 0xFF) * alpha);
+ return (argb & 0x00FFFFFF) | ((a & 0xFF) << 24);
+ }
+
+ public static int scaleAlpha(int argb, float factor) {
+ factor = AnimationUtility.clamp01(factor);
+ int a = (argb >>> 24) & 0xFF;
+ int scaledA = Math.round(a * factor);
+ return (argb & 0x00FFFFFF) | ((scaledA & 0xFF) << 24);
+ }
+
+ public static int darken(int argb, float factor) {
+ factor = AnimationUtility.clamp(factor, 0f, 1f);
+ int a = (argb >>> 24) & 0xFF;
+ int r = (argb >>> 16) & 0xFF;
+ int g = (argb >>> 8) & 0xFF;
+ int b = argb & 0xFF;
+
+ r = Math.round(r * factor);
+ g = Math.round(g * factor);
+ b = Math.round(b * factor);
+
+ return ((a & 0xFF) << 24)
+ | ((r & 0xFF) << 16)
+ | ((g & 0xFF) << 8)
+ | (b & 0xFF);
+ }
+
+ public static void applyColor(RenderColor out, int argb) {
+ out.a = (argb >>> 24) & 0xFF;
+ out.r = (argb >>> 16) & 0xFF;
+ out.g = (argb >>> 8) & 0xFF;
+ out.b = argb & 0xFF;
+ }
+
+ public static void drawModuleToggle(Renderer2D renderer,
+ Toast toast,
+ float x,
+ float y,
+ float w,
+ float h,
+ int fallbackAccent,
+ float anim,
+ long now) {
+ if (renderer == null || w <= 0.1f || h <= 0.1f) return;
+
+ float progress = toast.toggleProgress(now);
+ int tint = moduleToggleColor(progress, anim);
+ if (((tint >>> 24) & 0xFF) <= 0) {
+ tint = fallbackAccent;
+ }
+
+ float svgY = y + (h - w) * 0.5f;
+ SvgRenderOptions options = SvgRenderOptions.overrideColor(tint);
+ if (progress <= 0.001f) {
+ renderer.svg("toggle-left", x, svgY, w, w, options);
+ return;
+ }
+ if (progress >= 0.999f) {
+ renderer.svg("toggle-right", x, svgY, w, w, options);
+ return;
+ }
+
+ renderer.svg("toggle-track", x, svgY, w, w, options);
+
+ float thumbTravel = w * (3f / 24f);
+ float thumbX = x + AnimationUtility.lerp(-thumbTravel, thumbTravel, progress);
+ renderer.svg("toggle-thumb", thumbX, svgY, w, w, options);
+ }
+
+ public static final class ToastPalette {
+ public final int bgLeft;
+ public final int bgRight;
+ public final int text;
+ public final int progressTrack;
+ public final int progressFill;
+ public final int glassTint;
+
+ public ToastPalette(int bgLeft,
+ int bgRight,
+ int text,
+ int progressTrack,
+ int progressFill,
+ int glassTint) {
+ this.bgLeft = bgLeft;
+ this.bgRight = bgRight;
+ this.text = text;
+ this.progressTrack = progressTrack;
+ this.progressFill = progressFill;
+ this.glassTint = glassTint;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java
new file mode 100644
index 0000000..77a3742
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java
@@ -0,0 +1,168 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.interaction;
+
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.HudAnchorX;
+import dev.loki.lovisual.features.gui.hud.HudAnchorY;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.render.helpers.SystemCursor;
+import dev.loki.lovisual.util.screen.ClientScreen;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.ChatScreen;
+import org.lwjgl.glfw.GLFW;
+
+/**
+ * Обработка ввода drag-and-drop виджетов при открытом чате / drag-редакторе.
+ */
+public final class RegistryDragController {
+
+ private RegistryDragController() {
+ }
+
+ public static void handleDragging(Minecraft mc, int screenW, int screenH, float uiScale) {
+ boolean chatOpen = ClientScreen.current() instanceof ChatScreen;
+ boolean dragEditorOpen = RegistryState.forceVisible && ClientScreen.current() != null;
+ boolean allowDragging = chatOpen || dragEditorOpen;
+ float fbX = (float) mc.mouseHandler.xpos();
+ float fbY = (float) mc.mouseHandler.ypos();
+ float mx = HudScale.toVirtual(fbX, uiScale);
+ float my = HudScale.toVirtual(fbY, uiScale);
+
+ boolean left = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_1) == GLFW.GLFW_PRESS;
+ boolean middle = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_MIDDLE) == GLFW.GLFW_PRESS;
+
+ if (!allowDragging) {
+ if (RegistryState.dragging != null) {
+ //DebugLog.config("[HUD] Drag cancelled outside chat for %s", RegistryState.dragging.getId());
+ }
+ if (left && !RegistryState.prevLeft) {
+ // DebugLog.config("[HUD] Mouse down ignored (chat not open) at (%.1f, %.1f)", mx, my);
+ }
+ if (!left && RegistryState.prevLeft) {
+ // DebugLog.config("[HUD] Mouse up ignored (chat not open) at (%.1f, %.1f)", mx, my);
+ }
+ RegistryState.dragging = null;
+ RegistryState.draggingWithLeft = false;
+ RegistryDragSession.clearSnapPreview();
+ if (RegistryState.hudCursorActive) {
+ SystemCursor.reset();
+ RegistryState.hudCursorActive = false;
+ }
+ RegistryState.prevLeft = left;
+ RegistryState.prevMiddle = middle;
+ return;
+ }
+
+ if (RegistryState.dragging != null && !RegistryWidgetStore.isWidgetVisibleForEditor(RegistryState.dragging)) {
+ RegistryState.dragging = null;
+ RegistryState.draggingWithLeft = false;
+ RegistryDragSession.clearSnapPreview();
+ }
+
+ if (left && !RegistryState.prevLeft) {
+ //DebugLog.config("[HUD] Mouse down in chat at (%.1f, %.1f)", mx, my);
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ /*DebugLog.config("[HUD] Widget %s bounds x=%.1f y=%.1f w=%.1f h=%.1f contains=%s",
+ w.getId(), w.getX(), w.getY(), w.getWidth(), w.getHeight(), w.contains(mx, my));*/
+ }
+ DraggableHudElement clickTarget = null;
+ long bestClickOrder = Long.MIN_VALUE;
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (!RegistryWidgetStore.isWidgetVisibleForEditor(w)) continue;
+ if (!w.contains(mx, my)) continue;
+ if (!w.isMouseOverInteractive(mx, my)) continue;
+ long order = w.getRuntimeRenderOrder();
+ if (order > bestClickOrder) {
+ bestClickOrder = order;
+ clickTarget = w;
+ }
+ }
+ if (clickTarget != null) {
+ if (clickTarget.onMouseClicked(mx, my, GLFW.GLFW_MOUSE_BUTTON_1)) {
+ RegistryState.dragging = null;
+ RegistryState.draggingWithLeft = false;
+ RegistryDragSession.clearSnapPreview();
+ RegistryState.prevLeft = left;
+ RegistryState.prevMiddle = middle;
+ return;
+ }
+ // interactive hitbox: do not start drag on left click
+ RegistryState.prevLeft = left;
+ RegistryState.prevMiddle = middle;
+ return;
+ } else {
+ // start drag with left click if no interactive hitbox
+ DraggableHudElement candidate = RegistryWidgetStore.pickDragCandidate(mx, my);
+ if (candidate != null) {
+ RegistryState.dragging = candidate;
+ RegistryState.draggingWithLeft = true;
+ RegistryState.dragOffsetX = mx - candidate.getX();
+ RegistryState.dragOffsetY = my - candidate.getY();
+ if (RegistryWidgetStore.isWidgetAnchorModifierDown()) {
+ RegistryWidgetStore.detachChildren(candidate);
+ }
+ RegistryState.dragging.clearParentAnchor();
+ RegistryState.dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, RegistryState.dragging.getX(), RegistryState.dragging.getY());
+ SystemCursor.set(SystemCursor.CursorType.MOVE);
+ RegistryState.hudCursorActive = true;
+ }
+ }
+ }
+
+ if (middle && !RegistryState.prevMiddle) {
+ // start drag
+ DraggableHudElement candidate = RegistryWidgetStore.pickDragCandidate(mx, my);
+ if (candidate != null) {
+ RegistryState.dragging = candidate;
+ RegistryState.draggingWithLeft = false;
+ RegistryState.dragOffsetX = mx - candidate.getX();
+ RegistryState.dragOffsetY = my - candidate.getY();
+ if (RegistryWidgetStore.isWidgetAnchorModifierDown()) {
+ RegistryWidgetStore.detachChildren(candidate);
+ }
+ RegistryState.dragging.clearParentAnchor();
+ RegistryState.dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, RegistryState.dragging.getX(), RegistryState.dragging.getY());
+ //DebugLog.config("[HUD] Drag start for %s at (%.1f, %.1f)", RegistryState.dragging.getId(), RegistryState.dragging.getX(), RegistryState.dragging.getY());
+ SystemCursor.set(SystemCursor.CursorType.MOVE);
+ RegistryState.hudCursorActive = true;
+ }
+ if (RegistryState.dragging == null) {
+ //DebugLog.config("[HUD] No widget found under cursor at (%.1f, %.1f); widgets=%d", mx, my, RegistryState.WIDGETS.size());
+ }
+ } else if (!middle && RegistryState.prevMiddle && RegistryState.dragging != null && !RegistryState.draggingWithLeft) {
+ RegistryDragSession.stopDragging(screenW, screenH);
+ } else if (!middle && RegistryState.prevMiddle) {
+ //DebugLog.config("[HUD] Mouse up with no active drag at (%.1f, %.1f)", mx, my);
+ }
+
+ if (RegistryState.draggingWithLeft && !left && RegistryState.prevLeft && RegistryState.dragging != null) {
+ RegistryDragSession.stopDragging(screenW, screenH);
+ }
+
+ if (((RegistryState.draggingWithLeft && left) || (!RegistryState.draggingWithLeft && middle)) && RegistryState.dragging != null) {
+ RegistryDragSession.moveDraggedWidget(RegistryState.dragging, mx, my, screenW, screenH);
+ if (!RegistryWidgetStore.isWidgetAnchorModifierDown()) {
+ RegistryDragSession.updateSnapPreview(RegistryState.dragging, screenW, screenH);
+ } else {
+ RegistryDragSession.clearSnapPreview();
+ }
+ //DebugLog.config("[HUD] Dragging %s -> (%.1f, %.1f)", RegistryState.dragging.getId(), RegistryState.dragging.getX(), RegistryState.dragging.getY());
+ SystemCursor.set(SystemCursor.CursorType.MOVE);
+ RegistryState.hudCursorActive = true;
+ } else if (RegistryState.dragging == null) {
+ RegistryDragSession.clearSnapPreview();
+ RegistryDragSession.requestHoverCursor(mx, my);
+ }
+
+ RegistryState.prevLeft = left;
+ RegistryState.prevMiddle = middle;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java
new file mode 100644
index 0000000..1ecacee
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java
@@ -0,0 +1,200 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.interaction;
+
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistrySnapSupport;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout;
+import dev.loki.lovisual.render.helpers.SystemCursor;
+import net.minecraft.client.Minecraft;
+
+import java.util.HashSet;
+
+/** Жизненный цикл перетаскивания: завершение, перемещение, snap-превью, курсор. */
+public final class RegistryDragSession {
+
+ private RegistryDragSession() {
+ }
+
+ public static void stopDragging(int screenW, int screenH) {
+ if (RegistryState.dragging == null) return;
+ if (!RegistryWidgetStore.isWidgetAnchorModifierDown()) {
+ applyStickySnap(RegistryState.dragging, screenW, screenH);
+ }
+ RegistryState.dragging.clearParentAnchor();
+ Minecraft mc = Minecraft.getInstance();
+ boolean allowWidgetAnchoring = mc == null || !RegistryWidgetStore.isWidgetAnchorModifierDown();
+ RegistrySnapSupport.updateDragAnchors(RegistryState.dragging, screenW, screenH, allowWidgetAnchoring);
+ RegistryState.dragging.layoutValue().set(RegistryState.dragging.snapshotLayout());
+ RegistryState.dragging.saveConfig();
+ RegistryState.dragging = null;
+ RegistryState.draggingWithLeft = false;
+ clearSnapPreview();
+ if (RegistryState.hudCursorActive) {
+ SystemCursor.reset();
+ RegistryState.hudCursorActive = false;
+ }
+ }
+
+ public static void cancelDragging() {
+ RegistryState.dragging = null;
+ RegistryState.draggingWithLeft = false;
+ clearSnapPreview();
+ if (RegistryState.hudCursorActive) {
+ SystemCursor.reset();
+ RegistryState.hudCursorActive = false;
+ }
+ RegistryState.prevLeft = false;
+ RegistryState.prevMiddle = false;
+ }
+
+ public static void moveDraggedWidget(DraggableHudElement w, float mx, float my, int screenW, int screenH) {
+ float newX = mx - RegistryState.dragOffsetX;
+ float newY = my - RegistryState.dragOffsetY;
+ RegistryState.Bounds groupBounds = collectLinkedBounds(w, screenW, screenH);
+ float dx = newX - w.getX();
+ float dy = newY - w.getY();
+
+ if (groupBounds != null && groupBounds.width() <= screenW && groupBounds.height() <= screenH) {
+ dx = RegistryAnchorLayout.clamp(dx, -groupBounds.minX(), screenW - groupBounds.maxX());
+ dy = RegistryAnchorLayout.clamp(dy, -groupBounds.minY(), screenH - groupBounds.maxY());
+ w.moveTo(w.getX() + dx, w.getY() + dy);
+ RegistryAnchorLayout.applyDescendantAnchors(w, screenW, screenH, new HashSet<>());
+ return;
+ }
+
+ w.moveTo(RegistryAnchorLayout.clampX(newX, w, screenW), RegistryAnchorLayout.clampY(newY, w, screenH));
+ RegistryAnchorLayout.applyDescendantAnchors(w, screenW, screenH, new HashSet<>());
+ }
+
+ public static void updateSnapPreview(DraggableHudElement w, int screenW, int screenH) {
+ if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) {
+ clearSnapPreview();
+ return;
+ }
+
+ RegistryState.StickySnap snap = RegistrySnapSupport.findStickySnap(w, screenW, screenH);
+ if (!snap.hasX() && !snap.hasY()) {
+ clearSnapPreview();
+ return;
+ }
+
+ RegistryState.snapPreviewActive = true;
+ RegistryState.snapPreviewHasX = snap.hasX();
+ RegistryState.snapPreviewHasY = snap.hasY();
+ RegistryState.snapPreviewLineX = snap.x();
+ RegistryState.snapPreviewLineY = snap.y();
+ }
+
+ public static void applyStickySnap(DraggableHudElement w, int screenW, int screenH) {
+ if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) return;
+
+ RegistryState.StickySnap snap = RegistrySnapSupport.findStickySnap(w, screenW, screenH);
+ float newX = w.getX();
+ float newY = w.getY();
+ if (snap.hasX()) newX = snap.x();
+ if (snap.hasY()) newY = snap.y();
+ w.moveTo(RegistryAnchorLayout.clampX(newX, w, screenW), RegistryAnchorLayout.clampY(newY, w, screenH));
+ }
+
+ public static void clearSnapPreview() {
+ RegistryState.snapPreviewActive = false;
+ RegistryState.snapPreviewHasX = false;
+ RegistryState.snapPreviewHasY = false;
+ }
+
+ public static void requestHoverCursor(float mx, float my) {
+ DraggableHudElement interactive = findTopVisibleWidget(mx, my, true);
+ if (interactive != null) {
+ SystemCursor.set(SystemCursor.CursorType.HAND);
+ RegistryState.hudCursorActive = true;
+ return;
+ }
+
+ DraggableHudElement draggable = findTopVisibleWidget(mx, my, false);
+ if (draggable != null) {
+ SystemCursor.set(SystemCursor.CursorType.MOVE);
+ RegistryState.hudCursorActive = true;
+ }
+ }
+
+ public static DraggableHudElement findTopVisibleWidget(float mx, float my, boolean interactiveOnly) {
+ DraggableHudElement best = null;
+ long bestOrder = Long.MIN_VALUE;
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (!RegistryWidgetStore.isWidgetVisibleForEditor(w)) continue;
+ if (!w.contains(mx, my)) continue;
+
+ if (interactiveOnly) {
+ if (!w.isMouseOverInteractive(mx, my)) continue;
+ } else if (!w.isDraggable()) {
+ continue;
+ }
+
+ long order = w.getRuntimeRenderOrder();
+ if (order > bestOrder) {
+ bestOrder = order;
+ best = w;
+ }
+ }
+
+ if (best != null) return best;
+ for (int i = RegistryState.WIDGETS.size() - 1; i >= 0; i--) {
+ DraggableHudElement w = RegistryState.WIDGETS.get(i);
+ if (!RegistryWidgetStore.isWidgetVisibleForEditor(w)) continue;
+ if (!w.contains(mx, my)) continue;
+
+ if (interactiveOnly) {
+ if (!w.isMouseOverInteractive(mx, my)) continue;
+ } else if (!w.isDraggable()) {
+ continue;
+ }
+
+ return w;
+ }
+
+ return null;
+ }
+
+ public static RegistryState.Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH) {
+ if (root == null) return null;
+ return collectLinkedBounds(root, screenW, screenH, new HashSet<>());
+ }
+
+ public static RegistryState.Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH,
+ java.util.Set visited) {
+ if (root == null || !visited.add(root.getId())) return null;
+ RegistryState.Bounds bounds = RegistryState.Bounds.of(root);
+ String parentId = root.getId();
+ for (DraggableHudElement child : RegistryState.WIDGETS) {
+ if (child == root) continue;
+ if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue;
+ RegistryAnchorLayout.applyWidgetAnchor(child, root, screenW, screenH);
+ bounds = RegistryState.Bounds.union(bounds, collectLinkedBounds(child, screenW, screenH, visited));
+ }
+ return bounds;
+ }
+
+ public static boolean fitsLinkedBoundsAt(DraggableHudElement root,
+ float desiredRootX,
+ float desiredRootY,
+ int screenW,
+ int screenH) {
+ RegistryState.Bounds bounds = collectLinkedBounds(root, screenW, screenH);
+ if (bounds == null) return false;
+ float dx = desiredRootX - root.getX();
+ float dy = desiredRootY - root.getY();
+ return bounds.minX() + dx >= 0f
+ && bounds.minY() + dy >= 0f
+ && bounds.maxX() + dx <= screenW
+ && bounds.maxY() + dy <= screenH;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java
new file mode 100644
index 0000000..53debfd
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java
@@ -0,0 +1,200 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.registry;
+
+import dev.loki.lovisual.features.gui.hud.*;
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableLayoutValue;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.util.logging.DebugLog;
+import net.minecraft.client.Minecraft;
+
+import java.util.*;
+
+/** Применение якорей и инициализация сохранённых позиций виджетов. */
+public final class RegistryAnchorLayout {
+
+ private RegistryAnchorLayout() {
+ }
+
+ public static void ensurePositionInitialized(DraggableHudElement w, int screenW, int screenH) {
+ if (w.isPositionInitialized()) return;
+ DraggableLayoutValue.State saved = w.layoutValue().get();
+ if (saved != null) {
+ DraggableLayoutValue.State resolved = saved;
+ if (!saved.logical()) {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc != null && mc.getWindow() != null) {
+ int fbw = mc.getWindow().getWidth();
+ int fbh = mc.getWindow().getHeight();
+ float uiScale = HudScale.scale(fbw, fbh);
+ float nx = HudScale.toVirtual(saved.x(), uiScale);
+ float ny = HudScale.toVirtual(saved.y(), uiScale);
+ float npx = HudScale.toVirtual(saved.parentOffsetX(), uiScale);
+ float npy = HudScale.toVirtual(saved.parentOffsetY(), uiScale);
+ resolved = new DraggableLayoutValue.State(
+ nx,
+ ny,
+ saved.anchorX(),
+ saved.anchorY(),
+ saved.parentId(),
+ saved.parentSide(),
+ npx,
+ npy,
+ true
+ );
+ w.layoutValue().set(resolved);
+ w.saveConfig();
+ }
+ }
+ float safeW = w.getWidth() > 0f ? w.getWidth() : 20f;
+ float safeH = w.getHeight() > 0f ? w.getHeight() : 20f;
+ float clampedX = saved.anchorX() == HudAnchorX.FREE
+ ? Math.max(0, Math.min(resolved.x(), screenW - safeW))
+ : resolved.x();
+ float clampedY = saved.anchorY() == HudAnchorY.FREE
+ ? Math.max(0, Math.min(resolved.y(), screenH - safeH))
+ : resolved.y();
+ if (saved.anchorX() == HudAnchorX.FREE && saved.anchorY() == HudAnchorY.FREE
+ && (clampedX != resolved.x() || clampedY != resolved.y())) {
+ DebugLog.config("[HUD] Saved position clamped for %s: (%.1f, %.1f) -> (%.1f, %.1f)",
+ w.getId(), resolved.x(), resolved.y(), clampedX, clampedY);
+ } else {
+ DebugLog.config("[HUD] Loaded saved position for %s: (%.1f, %.1f)", w.getId(), resolved.x(), resolved.y());
+ }
+ w.moveTo(clampedX, clampedY);
+ w.setAnchors(resolved.anchorX(), resolved.anchorY(), clampedX, clampedY);
+ if (resolved.parentId() != null && resolved.parentSide() != WidgetAnchorSide.NONE) {
+ w.setParentAnchor(resolved.parentId(), resolved.parentSide(), resolved.parentOffsetX(), resolved.parentOffsetY());
+ } else {
+ w.clearParentAnchor();
+ }
+ } else {
+ w.applyDefaultPosition(screenW, screenH);
+ w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
+ w.clearParentAnchor();
+ DebugLog.config("[HUD] Applied default position for %s: (%.1f, %.1f)", w.getId(), w.getX(), w.getY());
+ }
+ w.markPositionInitialized();
+ }
+
+ public static void applyAnchors(DraggableHudElement w, int screenW, int screenH) {
+ applyAnchors(w, screenW, screenH, new HashSet<>());
+ }
+
+ public static void applyAnchors(DraggableHudElement w, int screenW, int screenH, Set visiting) {
+ if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) {
+ applyScreenAnchor(w, screenW, screenH);
+ return;
+ }
+ DraggableHudElement parent = RegistryWidgetStore.findById(w.getParentId());
+ if (parent == null) {
+ w.clearParentAnchor();
+ applyScreenAnchor(w, screenW, screenH);
+ return;
+ }
+ if (!visiting.add(w.getId())) {
+ w.clearParentAnchor();
+ applyScreenAnchor(w, screenW, screenH);
+ return;
+ }
+ if (parent != RegistryState.dragging) {
+ applyAnchors(parent, screenW, screenH, visiting);
+ }
+ if (RegistryWidgetStore.isDescendantOf(parent, w)) {
+ w.clearParentAnchor();
+ applyScreenAnchor(w, screenW, screenH);
+ } else {
+ applyWidgetAnchor(w, parent, screenW, screenH);
+ }
+ visiting.remove(w.getId());
+ }
+
+ public static void applyScreenAnchor(DraggableHudElement w, int screenW, int screenH) {
+ float x = w.getAnchorOffsetX();
+ float y = w.getAnchorOffsetY();
+ switch (w.getAnchorX()) {
+ case LEFT -> x = w.getAnchorOffsetX();
+ case CENTER -> x = screenW * 0.5f - w.getWidth() * 0.5f + w.getAnchorOffsetX();
+ case RIGHT -> x = screenW - w.getWidth() - w.getAnchorOffsetX();
+ case FREE -> x = w.getAnchorOffsetX();
+ }
+ switch (w.getAnchorY()) {
+ case TOP -> y = w.getAnchorOffsetY();
+ case CENTER -> y = screenH * 0.5f - w.getHeight() * 0.5f + w.getAnchorOffsetY();
+ case BOTTOM -> y = screenH - w.getHeight() - w.getAnchorOffsetY();
+ case FREE -> y = w.getAnchorOffsetY();
+ }
+ w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH));
+ }
+
+ public static void applyWidgetAnchor(DraggableHudElement w, DraggableHudElement parent, int screenW, int screenH) {
+ float x = w.getX();
+ float y = w.getY();
+ switch (w.getParentSide()) {
+ case TOP -> {
+ x = parent.getX() + w.getParentOffsetX();
+ y = parent.getY() - w.getHeight() - RegistryState.PARENT_GAP + w.getParentOffsetY();
+ }
+ case BOTTOM -> {
+ x = parent.getX() + w.getParentOffsetX();
+ y = parent.getY() + parent.getHeight() + RegistryState.PARENT_GAP + w.getParentOffsetY();
+ }
+ case LEFT -> {
+ x = parent.getX() - w.getWidth() - RegistryState.PARENT_GAP + w.getParentOffsetX();
+ y = parent.getY() + w.getParentOffsetY();
+ }
+ case RIGHT -> {
+ x = parent.getX() + parent.getWidth() + RegistryState.PARENT_GAP + w.getParentOffsetX();
+ y = parent.getY() + w.getParentOffsetY();
+ }
+ case NONE -> {
+ }
+ }
+ w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH));
+ }
+
+ public static void applyDragAnchorLock(DraggableHudElement w, int screenW, int screenH) {
+ if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) {
+ DraggableHudElement parent = RegistryWidgetStore.findById(w.getParentId());
+ if (parent != null) {
+ applyWidgetAnchor(w, parent, screenW, screenH);
+ }
+ return;
+ }
+ if (w.getAnchorX() != HudAnchorX.FREE || w.getAnchorY() != HudAnchorY.FREE) {
+ applyScreenAnchor(w, screenW, screenH);
+ }
+ }
+
+ public static void applyDescendantAnchors(DraggableHudElement parent, int screenW, int screenH, Set visited) {
+ if (parent == null || !visited.add(parent.getId())) return;
+ String parentId = parent.getId();
+ for (DraggableHudElement child : RegistryState.WIDGETS) {
+ if (child == parent) continue;
+ if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue;
+ applyWidgetAnchor(child, parent, screenW, screenH);
+ applyDescendantAnchors(child, screenW, screenH, visited);
+ }
+ }
+
+ public static float clampX(float x, DraggableHudElement w, int screenW) {
+ float maxX = Math.max(0f, screenW - Math.max(0f, w.getWidth()));
+ return clamp(x, 0f, maxX);
+ }
+
+ public static float clampY(float y, DraggableHudElement w, int screenH) {
+ float maxY = Math.max(0f, screenH - Math.max(0f, w.getHeight()));
+ return clamp(y, 0f, maxY);
+ }
+
+ public static float clamp(float value, float min, float max) {
+ if (max < min) return min;
+ return Math.max(min, Math.min(value, max));
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java
new file mode 100644
index 0000000..75d286d
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java
@@ -0,0 +1,199 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.registry;
+
+import dev.loki.lovisual.features.gui.hud.*;
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragSession;
+
+/** Математика прилипания: якоря-кандидаты, sticky snap, якоря после drag. */
+public final class RegistrySnapSupport {
+
+ private RegistrySnapSupport() {
+ }
+
+ public static RegistryState.WidgetAnchorCandidate findWidgetAnchorCandidate(DraggableHudElement dragging, int screenW, int screenH) {
+ float bestDist = Float.MAX_VALUE;
+ RegistryState.WidgetAnchorCandidate best = null;
+ for (DraggableHudElement target : RegistryState.WIDGETS) {
+ if (target == dragging) continue;
+ if (!RegistryWidgetStore.isWidgetVisibleForLinkOverlay(target)) continue;
+ if (RegistryWidgetStore.isDescendantOf(target, dragging)) continue;
+ if (target.getWidth() <= 0 || target.getHeight() <= 0) continue;
+
+ float dxOverlap = overlap(dragging.getX(), dragging.getX() + dragging.getWidth(),
+ target.getX(), target.getX() + target.getWidth());
+ float dyOverlap = overlap(dragging.getY(), dragging.getY() + dragging.getHeight(),
+ target.getY(), target.getY() + target.getHeight());
+
+ float distTop = Math.abs((dragging.getY() + dragging.getHeight()) - (target.getY() - RegistryState.PARENT_GAP));
+ if (dxOverlap > 0 && distTop <= RegistryState.PARENT_SNAP_DIST && distTop < bestDist) {
+ float offsetX = parentAxisOffset(dragging.getX() - target.getX());
+ float desiredY = target.getY() - RegistryState.PARENT_GAP - dragging.getHeight();
+ float desiredX = target.getX() + offsetX;
+ if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
+ float offsetY = 0f;
+ bestDist = distTop;
+ best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.TOP, offsetX, offsetY);
+ }
+
+ float distBottom = Math.abs(dragging.getY() - (target.getY() + target.getHeight() + RegistryState.PARENT_GAP));
+ if (dxOverlap > 0 && distBottom <= RegistryState.PARENT_SNAP_DIST && distBottom < bestDist) {
+ float offsetX = parentAxisOffset(dragging.getX() - target.getX());
+ float desiredY = target.getY() + target.getHeight() + RegistryState.PARENT_GAP;
+ float desiredX = target.getX() + offsetX;
+ if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
+ float offsetY = 0f;
+ bestDist = distBottom;
+ best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.BOTTOM, offsetX, offsetY);
+ }
+
+ float distLeft = Math.abs((dragging.getX() + dragging.getWidth()) - (target.getX() - RegistryState.PARENT_GAP));
+ if (dyOverlap > 0 && distLeft <= RegistryState.PARENT_SNAP_DIST && distLeft < bestDist) {
+ float offsetY = parentAxisOffset(dragging.getY() - target.getY());
+ float desiredX = target.getX() - RegistryState.PARENT_GAP - dragging.getWidth();
+ float desiredY = target.getY() + offsetY;
+ if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
+ float offsetX = 0f;
+ bestDist = distLeft;
+ best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.LEFT, offsetX, offsetY);
+ }
+
+ float distRight = Math.abs(dragging.getX() - (target.getX() + target.getWidth() + RegistryState.PARENT_GAP));
+ if (dyOverlap > 0 && distRight <= RegistryState.PARENT_SNAP_DIST && distRight < bestDist) {
+ float offsetY = parentAxisOffset(dragging.getY() - target.getY());
+ float desiredX = target.getX() + target.getWidth() + RegistryState.PARENT_GAP;
+ float desiredY = target.getY() + offsetY;
+ if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue;
+ float offsetX = 0f;
+ bestDist = distRight;
+ best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.RIGHT, offsetX, offsetY);
+ }
+ }
+ return best;
+ }
+
+ public static float parentAxisOffset(float offset) {
+ return Math.abs(offset) <= RegistryState.PARENT_ALIGN_SNAP_DIST ? 0f : offset;
+ }
+
+ public static float overlap(float a0, float a1, float b0, float b1) {
+ float left = Math.max(a0, b0);
+ float right = Math.min(a1, b1);
+ return right - left;
+ }
+
+ public static RegistryState.StickySnap findStickySnap(DraggableHudElement dragging, int screenW, int screenH) {
+ float bestX = 0f;
+ float bestY = 0f;
+ float bestXDist = Float.MAX_VALUE;
+ float bestYDist = Float.MAX_VALUE;
+ boolean hasX = false;
+ boolean hasY = false;
+
+ boolean rightSide = dragging.getX() > screenW * 0.5f;
+ for (DraggableHudElement target : RegistryState.WIDGETS) {
+ if (target == dragging) continue;
+ if (!target.isEnabled()) continue;
+ if (target.getWidth() <= 0 || target.getHeight() <= 0) continue;
+
+ if (rightSide) {
+ float targetEdge = target.getX() + target.getWidth();
+ float dragEdge = dragging.getX() + dragging.getWidth();
+ float dist = Math.abs(targetEdge - dragEdge);
+ if (dist <= RegistryState.PARENT_SNAP_DIST && dist < bestXDist) {
+ bestXDist = dist;
+ bestX = targetEdge - dragging.getWidth();
+ hasX = true;
+ }
+ } else {
+ float dist = Math.abs(target.getX() - dragging.getX());
+ if (dist <= RegistryState.PARENT_SNAP_DIST && dist < bestXDist) {
+ bestXDist = dist;
+ bestX = target.getX();
+ hasX = true;
+ }
+ }
+
+ float distY = Math.abs(target.getY() - dragging.getY());
+ if (distY <= RegistryState.PARENT_SNAP_DIST && distY < bestYDist) {
+ bestYDist = distY;
+ bestY = target.getY();
+ hasY = true;
+ }
+ }
+
+ float distTop = Math.abs(dragging.getY());
+ if (distTop <= RegistryState.PARENT_SNAP_DIST && distTop < bestYDist) {
+ bestY = 0f;
+ hasY = true;
+ }
+
+ return new RegistryState.StickySnap(hasX, hasY, bestX, bestY);
+ }
+
+ public static void updateDragAnchors(DraggableHudElement w, int screenW, int screenH, boolean allowWidgetAnchoring) {
+ if (w.getWidth() <= 0 || w.getHeight() <= 0) {
+ w.clearParentAnchor();
+ w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
+ return;
+ }
+
+ if (!allowWidgetAnchoring) {
+ w.clearParentAnchor();
+ w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
+ return;
+ }
+
+ if (w.supportsWidgetAnchoring()) {
+ RegistryState.WidgetAnchorCandidate candidate = findWidgetAnchorCandidate(w, screenW, screenH);
+ if (candidate != null) {
+ w.setParentAnchor(candidate.parent().getId(), candidate.side(), candidate.offsetX(), candidate.offsetY());
+ return;
+ }
+ }
+ w.clearParentAnchor();
+
+ float x = w.getX();
+ float y = w.getY();
+
+ HudAnchorX ax = HudAnchorX.FREE;
+ float offX = x;
+ float leftDist = Math.abs(x);
+ float rightDist = Math.abs((x + w.getWidth()) - screenW);
+ float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f);
+ if (leftDist <= RegistryState.SNAP_DIST) {
+ ax = HudAnchorX.LEFT;
+ offX = x;
+ } else if (rightDist <= RegistryState.SNAP_DIST) {
+ ax = HudAnchorX.RIGHT;
+ offX = rightDist;
+ } else if (centerDistX <= RegistryState.SNAP_DIST) {
+ ax = HudAnchorX.CENTER;
+ offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f;
+ }
+
+ HudAnchorY ay = HudAnchorY.FREE;
+ float offY = y;
+ float topDist = Math.abs(y);
+ float bottomDist = Math.abs((y + w.getHeight()) - screenH);
+ float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f);
+ if (topDist <= RegistryState.SNAP_DIST) {
+ ay = HudAnchorY.TOP;
+ offY = y;
+ } else if (bottomDist <= RegistryState.SNAP_DIST) {
+ ay = HudAnchorY.BOTTOM;
+ offY = bottomDist;
+ } else if (centerDistY <= RegistryState.SNAP_DIST) {
+ ay = HudAnchorY.CENTER;
+ offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f;
+ }
+
+ w.setAnchors(ax, ay, offX, offY);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java
new file mode 100644
index 0000000..696f27a
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.registry;
+
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide;
+import dev.loki.lovisual.features.theme.Themes;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Общее состояние реестра HUD-виджетов: константы, поля drag-and-drop
+ * и структуры данных раскладки.
+ */
+public final class RegistryState {
+
+ private RegistryState() {
+ }
+
+ public static final List WIDGETS = new ArrayList<>();
+ public static final float SNAP_DIST = 8f;
+ public static final float PARENT_SNAP_DIST = 10f;
+ public static final float PARENT_ALIGN_SNAP_DIST = 5f;
+ public static final float PARENT_GAP = 6f;
+ public static final int SNAP_PREVIEW_LINE = 0xFFFFFFFF;
+ public static final float DRAG_COORD_TEXT_SCALE = 0.85f;
+ public static final float DRAG_COORD_MARGIN = 4f;
+ public static final float LINK_WIRE_THICKNESS = 1.05f;
+ public static final float LINK_PREVIEW_THICKNESS = 1.55f;
+
+ public static DraggableHudElement dragging;
+ public static float dragOffsetX;
+ public static float dragOffsetY;
+ public static boolean draggingWithLeft;
+ public static boolean prevLeft;
+ public static boolean prevMiddle;
+ public static long renderOrderSeq;
+ public static boolean forceVisible;
+ public static boolean snapPreviewActive;
+ public static boolean snapPreviewHasX;
+ public static boolean snapPreviewHasY;
+ public static float snapPreviewLineX;
+ public static float snapPreviewLineY;
+ public static String editorWidgetId;
+ public static boolean hudCursorActive;
+ public static int lastScreenW = -1;
+ public static int lastScreenH = -1;
+ public static Themes.Theme cachedLinkTheme;
+ public static LinkWireColors cachedLinkColors;
+
+ public record StickySnap(boolean hasX, boolean hasY, float x, float y) {
+ }
+
+ public record WidgetAnchorCandidate(DraggableHudElement parent, WidgetAnchorSide side, float offsetX,
+ float offsetY) {
+ }
+
+ public record LinkWireColors(int wireStart, int wireEnd, int previewStart, int previewEnd) {
+ }
+
+ public record Bounds(float minX, float minY, float maxX, float maxY) {
+
+ public static Bounds of(DraggableHudElement w) {
+ return new Bounds(w.getX(), w.getY(), w.getX() + w.getWidth(), w.getY() + w.getHeight());
+ }
+
+ public static Bounds union(Bounds a, Bounds b) {
+ if (a == null) return b;
+ if (b == null) return a;
+ return new Bounds(
+ Math.min(a.minX, b.minX),
+ Math.min(a.minY, b.minY),
+ Math.max(a.maxX, b.maxX),
+ Math.max(a.maxY, b.maxY)
+ );
+ }
+
+ public float width() {
+ return maxX - minX;
+ }
+
+ public float height() {
+ return maxY - minY;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java
new file mode 100644
index 0000000..e02bf40
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java
@@ -0,0 +1,196 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.registry;
+
+import dev.loki.lovisual.Lifecycle;
+import dev.loki.lovisual.features.gui.hud.*;
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.module.HudPhase;
+import dev.loki.lovisual.util.input.KeyManager;
+import dev.loki.lovisual.util.logging.DebugLog;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Хранение и поиск виджетов реестра + предикаты видимости для редактора.
+ */
+public final class RegistryWidgetStore {
+
+ private RegistryWidgetStore() {
+ }
+
+ public static void register(DraggableHudElement widget) {
+ if (!RegistryState.WIDGETS.contains(widget)) {
+ widget.postInit();
+ RegistryState.WIDGETS.add(widget);
+ DebugLog.config("[HUD] Registered widget: %s", widget.getId());
+ }
+ }
+
+ public static List getWidgets() {
+ HudElements.ensureLoaded();
+ return Collections.unmodifiableList(RegistryState.WIDGETS);
+ }
+
+ public static DraggableHudElement getById(String id) {
+ HudElements.ensureLoaded();
+ return findById(id);
+ }
+
+ public static boolean isEnabled(String id) {
+ if (!Lifecycle.canRunHud()) return false;
+ DraggableHudElement w = findById(id);
+ return w != null && w.isEnabled();
+ }
+
+ public static T get(Class type) {
+ if (type == null) return null;
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (type.isInstance(w)) {
+ return type.cast(w);
+ }
+ }
+ return null;
+ }
+
+ public static boolean isEnabled(Class extends DraggableHudElement> type) {
+ if (!Lifecycle.canRunHud()) return false;
+ DraggableHudElement w = get(type);
+ return w != null && w.isEnabled();
+ }
+
+ public static void tickAll() {
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ w.onTick();
+ }
+ }
+
+ public static DraggableHudElement findById(String id) {
+ if (id == null) return null;
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (id.equals(w.getId())) return w;
+ }
+ return null;
+ }
+
+ public static void noteRendered(DraggableHudElement w) {
+ if (w == null) return;
+ w.markRendered(++RegistryState.renderOrderSeq);
+ }
+
+ public static boolean isWidgetVisibleForEditor(DraggableHudElement w) {
+ if (w == null) return false;
+
+ boolean singleEditorPreview = RegistryState.editorWidgetId != null;
+ if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) {
+ return false;
+ }
+
+ boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible;
+ if (!bypassEnabled && !w.isEnabled()) {
+ return false;
+ }
+
+ if (!w.usesEngineRenderer()) return false;
+ if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) return false;
+ return w.getRuntimeRenderOrder() > 0L;
+ }
+
+ public static boolean isRenderableCandidate(DraggableHudElement w, HudPhase phase) {
+ if (w == null) return false;
+
+ boolean singleEditorPreview = RegistryState.editorWidgetId != null;
+ if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) {
+ return false;
+ }
+
+ boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible;
+ if (!bypassEnabled && !w.isEnabled()) {
+ return false;
+ }
+
+ if (!w.usesEngineRenderer()) return false;
+ if (w.getHudPhase() != phase) return false;
+ return w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL;
+ }
+
+ public static boolean isWidgetVisibleForLinkOverlay(DraggableHudElement w) {
+ if (w == null) return false;
+
+ boolean singleEditorPreview = RegistryState.editorWidgetId != null;
+ if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) {
+ return false;
+ }
+
+ boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible;
+ if (!bypassEnabled && !w.isEnabled()) {
+ return false;
+ }
+
+ return w.usesEngineRenderer() && w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL;
+ }
+
+ public static boolean isDescendantOf(DraggableHudElement possibleDescendant, DraggableHudElement ancestor) {
+ if (possibleDescendant == null || ancestor == null) return false;
+ String ancestorId = ancestor.getId();
+ Set visited = new HashSet<>();
+ DraggableHudElement current = possibleDescendant;
+ while (current != null && current.getParentId() != null && current.getParentSide() != WidgetAnchorSide.NONE) {
+ if (!visited.add(current.getId())) return false;
+ if (ancestorId.equals(current.getParentId())) return true;
+ current = findById(current.getParentId());
+ }
+ return false;
+ }
+
+ public static void detachChildren(DraggableHudElement parent) {
+ String parentId = parent.getId();
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (w == parent) continue;
+ if (parentId.equals(w.getParentId()) && w.getParentSide() != WidgetAnchorSide.NONE) {
+ w.clearParentAnchor();
+ w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY());
+ w.layoutValue().set(w.snapshotLayout());
+ w.saveConfig();
+ }
+ }
+ }
+
+ public static boolean isWidgetAnchorModifierDown() {
+ return KeyManager.isComboHeldAllowScreen("LEFT_ALT")
+ || KeyManager.isComboHeldAllowScreen("RIGHT_ALT");
+ }
+
+ public static DraggableHudElement pickDragCandidate(float mx, float my) {
+ DraggableHudElement candidate = null;
+ long bestOrder = Long.MIN_VALUE;
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (!isWidgetVisibleForEditor(w)) continue;
+ if (!w.isDraggable() || !w.contains(mx, my)) continue;
+ long order = w.getRuntimeRenderOrder();
+ if (order > bestOrder) {
+ bestOrder = order;
+ candidate = w;
+ }
+ }
+ if (candidate == null) {
+ for (int i = RegistryState.WIDGETS.size() - 1; i >= 0; i--) {
+ DraggableHudElement w = RegistryState.WIDGETS.get(i);
+ if (!isWidgetVisibleForEditor(w)) continue;
+ if (w.contains(mx, my) && w.isDraggable()) {
+ candidate = w;
+ break;
+ }
+ }
+ }
+ return candidate;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java
new file mode 100644
index 0000000..16b924b
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java
@@ -0,0 +1,200 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.render;
+
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
+import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistrySnapSupport;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore;
+import dev.loki.lovisual.features.theme.Theme;
+import dev.loki.lovisual.features.theme.Themes;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.text.Fonts;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+import dev.loki.lovisual.util.screen.ClientScreen;
+import net.minecraft.client.Minecraft;
+
+/** Оверлей перетаскивания: координаты, snap-линии и провода родительских связей. */
+public final class RegistryOverlayRenderer {
+
+ private RegistryOverlayRenderer() {
+ }
+
+ public static void renderDragOverlay(Renderer2D renderer,
+ TextRenderer fallbackTextRenderer,
+ int screenW,
+ int screenH) {
+ renderLinkWires(renderer, screenW, screenH);
+
+ if (RegistryState.snapPreviewActive) {
+ if (RegistryState.snapPreviewHasX) {
+ renderer.line(RegistryState.snapPreviewLineX, 0f, RegistryState.snapPreviewLineX, screenH, RegistryState.SNAP_PREVIEW_LINE);
+ }
+ if (RegistryState.snapPreviewHasY) {
+ renderer.line(0f, RegistryState.snapPreviewLineY, screenW, RegistryState.snapPreviewLineY, RegistryState.SNAP_PREVIEW_LINE);
+ }
+ }
+
+ if (RegistryState.dragging == null) return;
+ TextRenderer coordRenderer = Fonts.renderer("Comfortaa", dev.loki.lovisual.render.engine.text.FontInfo.Type.Regular,
+ fallbackTextRenderer != null ? fallbackTextRenderer : TextRenderer.get());
+ if (coordRenderer == null) return;
+
+ String coords = "x: " + Math.round(RegistryState.dragging.getX()) + " y: " + Math.round(RegistryState.dragging.getY());
+ coordRenderer.begin(RegistryState.DRAG_COORD_TEXT_SCALE, false, false);
+ try {
+ float textW = (float) coordRenderer.getWidth(coords, true);
+ float textH = (float) coordRenderer.getHeight(true);
+ float textX = RegistryState.dragging.getX();
+ float textY = RegistryState.dragging.getY() - textH - RegistryState.DRAG_COORD_MARGIN;
+ if (textY < RegistryState.DRAG_COORD_MARGIN) {
+ textY = RegistryState.dragging.getY() + RegistryState.dragging.getHeight() + RegistryState.DRAG_COORD_MARGIN;
+ }
+ textX = RegistryAnchorLayout.clamp(textX, RegistryState.DRAG_COORD_MARGIN,
+ Math.max(RegistryState.DRAG_COORD_MARGIN, screenW - textW - RegistryState.DRAG_COORD_MARGIN));
+ textY = RegistryAnchorLayout.clamp(textY, RegistryState.DRAG_COORD_MARGIN,
+ Math.max(RegistryState.DRAG_COORD_MARGIN, screenH - textH - RegistryState.DRAG_COORD_MARGIN));
+ coordRenderer.render(coords, textX, textY, new dev.loki.lovisual.render.engine.color.RenderColor(0xFFFFFFFF), true);
+ } finally {
+ coordRenderer.end();
+ }
+ }
+
+ private static void renderLinkWires(Renderer2D renderer,
+ int screenW,
+ int screenH) {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.getWindow() == null || !(ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen))
+ return;
+
+ int fbw = mc.getWindow().getWidth();
+ int fbh = mc.getWindow().getHeight();
+ float uiScale = HudScale.scale(fbw, fbh);
+ float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale);
+ float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale);
+
+ RegistryState.LinkWireColors colors = linkWireColors();
+ for (DraggableHudElement child : RegistryState.WIDGETS) {
+ if (!RegistryWidgetStore.isWidgetVisibleForLinkOverlay(child)) continue;
+ if (child == RegistryState.dragging) continue;
+ if (child.getParentId() == null || child.getParentSide() == WidgetAnchorSide.NONE) continue;
+
+ DraggableHudElement parent = RegistryWidgetStore.findById(child.getParentId());
+ if (!RegistryWidgetStore.isWidgetVisibleForLinkOverlay(parent)) continue;
+ if (!parent.contains(mx, my) && !child.contains(mx, my)) continue;
+ renderLinkWire(renderer, parent, child, child.getParentSide(),
+ RegistryState.LINK_WIRE_THICKNESS, colors.wireStart(), colors.wireEnd());
+ }
+
+ if (RegistryState.dragging == null || RegistryWidgetStore.isWidgetAnchorModifierDown()) return;
+ RegistryState.WidgetAnchorCandidate candidate =
+ RegistrySnapSupport.findWidgetAnchorCandidate(RegistryState.dragging, screenW, screenH);
+ if (candidate == null) return;
+ renderLinkWire(renderer, candidate.parent(), RegistryState.dragging, candidate.side(),
+ RegistryState.LINK_PREVIEW_THICKNESS, colors.previewStart(), colors.previewEnd());
+ }
+
+ private static RegistryState.LinkWireColors linkWireColors() {
+ Themes.Theme theme = Theme.theme();
+ if (RegistryState.cachedLinkTheme == theme && RegistryState.cachedLinkColors != null) {
+ return RegistryState.cachedLinkColors;
+ }
+
+ int wireStart = HudRenderUtil.setAlpha(
+ HudRenderUtil.mixColor(theme.textMuted(), theme.strokeSoft(), 0.45f), 0x86);
+ int wireEnd = HudRenderUtil.setAlpha(
+ HudRenderUtil.mixColor(theme.accentSoft(), theme.windowStroke(), 0.24f), 0x78);
+ int previewStart = HudRenderUtil.setAlpha(
+ HudRenderUtil.mixColor(theme.textPrimary(), theme.accentSoft(), 0.38f), 0xC8);
+ int previewEnd = HudRenderUtil.setAlpha(
+ HudRenderUtil.mixColor(theme.accent(), theme.accentSoft(), 0.28f), 0xD8);
+
+ RegistryState.cachedLinkTheme = theme;
+ RegistryState.cachedLinkColors = new RegistryState.LinkWireColors(wireStart, wireEnd, previewStart, previewEnd);
+ return RegistryState.cachedLinkColors;
+ }
+
+ private static void renderLinkWire(Renderer2D renderer,
+ DraggableHudElement parent,
+ DraggableHudElement child,
+ WidgetAnchorSide side,
+ double thickness,
+ int startArgb,
+ int endArgb) {
+ if (parent == null || child == null || side == null || side == WidgetAnchorSide.NONE) return;
+ if (parent.getWidth() <= 0f || parent.getHeight() <= 0f || child.getWidth() <= 0f || child.getHeight() <= 0f) {
+ return;
+ }
+
+ double parentX = 0.0;
+ double parentY = 0.0;
+ double childX = 0.0;
+ double childY = 0.0;
+ double parentCx = 0.0;
+ double parentCy = 0.0;
+ double childCx = 0.0;
+ double childCy = 0.0;
+
+ switch (side) {
+ case TOP -> {
+ parentX = parent.getX() + parent.getWidth() * 0.5;
+ parentY = parent.getY();
+ childX = child.getX() + child.getWidth() * 0.5;
+ childY = child.getY() + child.getHeight();
+ double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45);
+ parentCx = parentX;
+ parentCy = parentY - c;
+ childCx = childX;
+ childCy = childY + c;
+ }
+ case BOTTOM -> {
+ parentX = parent.getX() + parent.getWidth() * 0.5;
+ parentY = parent.getY() + parent.getHeight();
+ childX = child.getX() + child.getWidth() * 0.5;
+ childY = child.getY();
+ double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45);
+ parentCx = parentX;
+ parentCy = parentY + c;
+ childCx = childX;
+ childCy = childY - c;
+ }
+ case LEFT -> {
+ parentX = parent.getX();
+ parentY = parent.getY() + parent.getHeight() * 0.5;
+ childX = child.getX() + child.getWidth();
+ childY = child.getY() + child.getHeight() * 0.5;
+ double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45);
+ parentCx = parentX - c;
+ parentCy = parentY;
+ childCx = childX + c;
+ childCy = childY;
+ }
+ case RIGHT -> {
+ parentX = parent.getX() + parent.getWidth();
+ parentY = parent.getY() + parent.getHeight() * 0.5;
+ childX = child.getX();
+ childY = child.getY() + child.getHeight() * 0.5;
+ double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45);
+ parentCx = parentX + c;
+ parentCy = parentY;
+ childCx = childX - c;
+ childCy = childY;
+ }
+ case NONE -> {
+ return;
+ }
+ }
+
+ renderer.bezierConnectorGradient(childX, childY, childCx, childCy, parentCx, parentCy, parentX, parentY,
+ 18, thickness, startArgb, endArgb);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java
new file mode 100644
index 0000000..588388f
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java
@@ -0,0 +1,171 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.render;
+
+import dev.loki.lovisual.Lifecycle;
+import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
+import dev.loki.lovisual.features.gui.hud.*;
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragController;
+import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragSession;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore;
+import dev.loki.lovisual.features.module.HudPhase;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.render.engine.profiler.ProfilerPhase;
+import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+import net.minecraft.client.Minecraft;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Пакетный проход движка рендера: отбор и отрисовка всех виджетов фазы + drag-оверлей.
+ */
+public final class RegistryRenderPipeline {
+
+ private RegistryRenderPipeline() {
+ }
+
+ public static boolean hasEngineWork(HudPhase phase, boolean foreground) {
+ if (!Lifecycle.canRunHud()) return false;
+ if (foreground && RegistryState.dragging != null) return true;
+
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (RegistryWidgetStore.isRenderableCandidate(w, phase)) return true;
+ }
+ return false;
+ }
+
+ public static void renderAllEngine(HudPhase phase,
+ Renderer2D renderer,
+ TextRenderer textRenderer,
+ net.minecraft.client.gui.GuiGraphicsExtractor ctx,
+ float tickDelta) {
+ if (!Lifecycle.canRunHud()) {
+ RegistryDragSession.cancelDragging();
+ return;
+ }
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.getWindow() == null) return;
+ boolean allowOnlyModuleList = false;
+
+ int fbw = mc.getWindow().getWidth();
+ int fbh = mc.getWindow().getHeight();
+ int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
+ int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
+ RegistryResizeHandler.handleScreenResize(screenW, screenH);
+
+ List renderList = new ArrayList<>();
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ boolean singleEditorPreview = RegistryState.editorWidgetId != null;
+ if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) {
+ continue;
+ }
+ boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible;
+ if (!bypassEnabled && !w.isEnabled()) {
+ continue;
+ }
+ if (!w.usesEngineRenderer()) {
+ continue;
+ }
+ if (w.getHudPhase() != phase) {
+ continue;
+ }
+ if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) {
+ continue;
+ }
+ RegistryAnchorLayout.ensurePositionInitialized(w, screenW, screenH);
+ if (allowOnlyModuleList && !"module_list".equals(w.getId())) {
+ continue;
+ }
+ renderList.add(w);
+ }
+
+ for (DraggableHudElement w : renderList) {
+ if (w == RegistryState.dragging) continue;
+ if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) {
+ RegistryAnchorLayout.applyScreenAnchor(w, screenW, screenH);
+ }
+ }
+ for (DraggableHudElement w : renderList) {
+ if (w == RegistryState.dragging) continue;
+ if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) {
+ RegistryAnchorLayout.applyAnchors(w, screenW, screenH, new HashSet<>());
+ }
+ }
+
+ for (DraggableHudElement w : renderList) {
+ RegistryWidgetStore.noteRendered(w);
+ try (ProfilerPhase.Scope widgetScope =
+ ProfilerPhase.scope("2d:widget:" + w.getId());
+ RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) {
+ w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
+ }
+ }
+ }
+
+ public static void renderAllEngineForeground(HudPhase phase,
+ Renderer2D renderer,
+ TextRenderer textRenderer,
+ net.minecraft.client.gui.GuiGraphicsExtractor ctx,
+ float tickDelta) {
+ if (!Lifecycle.canRunHud()) {
+ RegistryDragSession.cancelDragging();
+ return;
+ }
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.getWindow() == null) return;
+ boolean allowOnlyModuleList = false;
+
+ int fbw = mc.getWindow().getWidth();
+ int fbh = mc.getWindow().getHeight();
+ int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
+ int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
+ RegistryResizeHandler.handleScreenResize(screenW, screenH);
+
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ boolean singleEditorPreview = RegistryState.editorWidgetId != null;
+ if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) {
+ continue;
+ }
+ boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible;
+ if (!bypassEnabled && !w.isEnabled()) {
+ continue;
+ }
+ if (!w.usesEngineRenderer()) {
+ continue;
+ }
+ if (w.getHudPhase() != phase) {
+ continue;
+ }
+ if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) {
+ continue;
+ }
+ if (allowOnlyModuleList && !"module_list".equals(w.getId())) {
+ continue;
+ }
+ try (ProfilerPhase.Scope widgetScope =
+ ProfilerPhase.scope("2d:widget_fg:" + w.getId());
+ RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) {
+ w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
+ }
+ }
+
+ RegistryOverlayRenderer.renderDragOverlay(renderer, textRenderer, screenW, screenH);
+ BetterChatRenderer.renderCapturedTooltip(ctx);
+ if (phase == HudPhase.AFTER_SUBTITLES) {
+ float uiScale = HudScale.scale(fbw, fbh);
+ RegistryDragController.handleDragging(mc, screenW, screenH, uiScale);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java
new file mode 100644
index 0000000..7383396
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java
@@ -0,0 +1,130 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.render;
+
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.HudAnchorX;
+import dev.loki.lovisual.features.gui.hud.HudAnchorY;
+import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+
+/**
+ * Реакция раскладки на изменение логического размера экрана.
+ */
+public final class RegistryResizeHandler {
+
+ private RegistryResizeHandler() {
+ }
+
+ public static void handleScreenResize(int screenW, int screenH) {
+ if (screenW <= 0 || screenH <= 0) return;
+ if (RegistryState.lastScreenW <= 0 || RegistryState.lastScreenH <= 0) {
+ RegistryState.lastScreenW = screenW;
+ RegistryState.lastScreenH = screenH;
+ return;
+ }
+ if (RegistryState.lastScreenW == screenW && RegistryState.lastScreenH == screenH) return;
+
+ int oldW = RegistryState.lastScreenW;
+ int oldH = RegistryState.lastScreenH;
+ RegistryState.lastScreenW = screenW;
+ RegistryState.lastScreenH = screenH;
+
+ for (DraggableHudElement w : RegistryState.WIDGETS) {
+ if (!w.isPositionInitialized()) continue;
+ if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) {
+ // Parent-anchored widgets will be handled when anchors are applied.
+ continue;
+ }
+ if (w.getWidth() <= 0 || w.getHeight() <= 0) continue;
+
+ boolean changed = false;
+ if (w.getAnchorX() == HudAnchorX.FREE && w.getAnchorY() == HudAnchorY.FREE) {
+ changed |= inferScreenAnchors(w, oldW, oldH);
+ }
+
+ RegistryAnchorLayout.applyScreenAnchor(w, screenW, screenH);
+ changed |= clampToScreenIfFree(w, screenW, screenH);
+
+ if (changed) {
+ w.layoutValue().set(w.snapshotLayout());
+ w.saveConfig();
+ }
+ }
+ }
+
+ private static boolean inferScreenAnchors(DraggableHudElement w, int screenW, int screenH) {
+ float x = w.getX();
+ float y = w.getY();
+ boolean changed = false;
+
+ HudAnchorX ax = w.getAnchorX();
+ float offX = w.getAnchorOffsetX();
+ float leftDist = Math.abs(x);
+ float rightDist = Math.abs((x + w.getWidth()) - screenW);
+ float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f);
+ if (leftDist <= RegistryState.SNAP_DIST) {
+ ax = HudAnchorX.LEFT;
+ offX = x;
+ } else if (rightDist <= RegistryState.SNAP_DIST) {
+ ax = HudAnchorX.RIGHT;
+ offX = rightDist;
+ } else if (centerDistX <= RegistryState.SNAP_DIST) {
+ ax = HudAnchorX.CENTER;
+ offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f;
+ }
+
+ HudAnchorY ay = w.getAnchorY();
+ float offY = w.getAnchorOffsetY();
+ float topDist = Math.abs(y);
+ float bottomDist = Math.abs((y + w.getHeight()) - screenH);
+ float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f);
+ if (topDist <= RegistryState.SNAP_DIST) {
+ ay = HudAnchorY.TOP;
+ offY = y;
+ } else if (bottomDist <= RegistryState.SNAP_DIST) {
+ ay = HudAnchorY.BOTTOM;
+ offY = bottomDist;
+ } else if (centerDistY <= RegistryState.SNAP_DIST) {
+ ay = HudAnchorY.CENTER;
+ offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f;
+ }
+
+ if (ax != w.getAnchorX() || ay != w.getAnchorY()
+ || offX != w.getAnchorOffsetX() || offY != w.getAnchorOffsetY()) {
+ w.setAnchors(ax, ay, offX, offY);
+ changed = true;
+ }
+ return changed;
+ }
+
+ private static boolean clampToScreenIfFree(DraggableHudElement w, int screenW, int screenH) {
+ boolean changed = false;
+ float newX = w.getX();
+ float newY = w.getY();
+ if (w.getAnchorX() == HudAnchorX.FREE) {
+ float maxX = Math.max(0f, screenW - w.getWidth());
+ newX = Math.max(0f, Math.min(newX, maxX));
+ }
+ if (w.getAnchorY() == HudAnchorY.FREE) {
+ float maxY = Math.max(0f, screenH - w.getHeight());
+ newY = Math.max(0f, Math.min(newY, maxY));
+ }
+ if (newX != w.getX() || newY != w.getY()) {
+ w.moveTo(newX, newY);
+ if (w.getAnchorX() == HudAnchorX.FREE || w.getAnchorY() == HudAnchorY.FREE) {
+ w.setAnchors(w.getAnchorX(), w.getAnchorY(),
+ w.getAnchorX() == HudAnchorX.FREE ? newX : w.getAnchorOffsetX(),
+ w.getAnchorY() == HudAnchorY.FREE ? newY : w.getAnchorOffsetY());
+ }
+ changed = true;
+ }
+ return changed;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java
new file mode 100644
index 0000000..a3c4a53
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java
@@ -0,0 +1,110 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.draggable.render;
+
+import dev.loki.lovisual.Lifecycle;
+import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
+import dev.loki.lovisual.features.gui.hud.HudRenderSpace;
+import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide;
+import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragSession;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState;
+import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore;
+import dev.loki.lovisual.features.module.HudPhase;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.render.engine.profiler.ProfilerPhase;
+import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+import dev.loki.lovisual.util.screen.ClientScreen;
+import net.minecraft.client.Minecraft;
+
+import java.util.HashSet;
+
+/**
+ * Путь отрисовки одиночного виджета по id (превью в редакторе и запросы движка).
+ */
+public final class RegistryWidgetRenderPath {
+
+ private RegistryWidgetRenderPath() {
+ }
+
+ public static boolean hasEngineWidgetWork(String id) {
+ if (!Lifecycle.canRunHud()) return false;
+ if (id == null || id.isBlank()) return false;
+ if (RegistryState.editorWidgetId != null && !RegistryState.editorWidgetId.equals(id)) return false;
+
+ DraggableHudElement w = RegistryWidgetStore.findById(id);
+ if (w == null) return false;
+ if (!w.usesEngineRenderer()) return false;
+ boolean bypassEnabled = RegistryState.forceVisible && RegistryState.editorWidgetId != null && RegistryState.editorWidgetId.equals(id);
+ return bypassEnabled || w.isEnabled();
+ }
+
+ public static void renderEngineWidget(String id,
+ Renderer2D renderer,
+ TextRenderer textRenderer,
+ net.minecraft.client.gui.GuiGraphicsExtractor ctx,
+ float tickDelta) {
+ if (!Lifecycle.canRunHud()) {
+ RegistryDragSession.cancelDragging();
+ return;
+ }
+ if (RegistryState.editorWidgetId != null && (!RegistryState.editorWidgetId.equals(id))) return;
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.getWindow() == null) return;
+ boolean isChat = ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen;
+ boolean isInventory = ClientScreen.current() instanceof net.minecraft.client.gui.screens.inventory.AbstractContainerScreen>;
+ boolean allowOnlyModuleList = isInventory && ClientScreen.current() != null;
+ if (RegistryState.forceVisible) {
+ allowOnlyModuleList = false;
+ }
+ boolean shouldHideHud = !RegistryState.forceVisible && ClientScreen.current() != null && !isChat && !allowOnlyModuleList;
+ if (shouldHideHud) {
+ return;
+ }
+
+ DraggableHudElement w = RegistryWidgetStore.findById(id);
+ if (w == null) return;
+ if (!w.usesEngineRenderer()) return;
+ boolean bypassEnabled = RegistryState.forceVisible && RegistryState.editorWidgetId != null && RegistryState.editorWidgetId.equals(id);
+ if (!bypassEnabled && !w.isEnabled()) {
+ return;
+ }
+ if (allowOnlyModuleList && !"module_list".equals(w.getId())) {
+ return;
+ }
+
+ int fbw = mc.getWindow().getWidth();
+ int fbh = mc.getWindow().getHeight();
+ int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
+ int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
+ RegistryResizeHandler.handleScreenResize(screenW, screenH);
+
+ RegistryAnchorLayout.ensurePositionInitialized(w, screenW, screenH);
+ if (w != RegistryState.dragging) {
+ if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) {
+ RegistryAnchorLayout.applyScreenAnchor(w, screenW, screenH);
+ } else {
+ RegistryAnchorLayout.applyAnchors(w, screenW, screenH, new HashSet<>());
+ }
+ }
+
+ RegistryWidgetStore.noteRendered(w);
+ try (ProfilerPhase.Scope widgetScope =
+ ProfilerPhase.scope("2d:widget:" + w.getId());
+ RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) {
+ w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
+ }
+ try (ProfilerPhase.Scope widgetScope =
+ ProfilerPhase.scope("2d:widget_fg:" + w.getId());
+ RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) {
+ w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java
index b0cbf27..dc5d7c6 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java
@@ -17,6 +17,8 @@ import net.minecraft.util.Util;
import net.minecraft.world.item.ItemStack;
import dev.loki.lovisual.config.SettingDef;
import dev.loki.lovisual.features.gui.hud.AbstractHudElement;
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.visuals.Animations;
import dev.loki.lovisual.features.gui.hud.HudElementRegister;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.features.theme.Themes;
@@ -104,7 +106,7 @@ public final class CustomHotbar extends AbstractHudElement {
ItemStack offhand = player.getOffhandItem();
boolean hasOffhand = !offhand.isEmpty();
- float baseY = h - 22f;
+ float baseY = h - 22f + hotbarRaiseOffset();
boolean blur = shouldBlur(bm);
boolean glass = shouldGlass(bm);
@@ -219,7 +221,7 @@ public final class CustomHotbar extends AbstractHudElement {
if (bm == null || !bm.isHudHotbarEnabled()) return;
int i = mc.getWindow().getGuiScaledWidth() / 2;
- int baseY = mc.getWindow().getGuiScaledHeight() - 22;
+ float baseY = mc.getWindow().getGuiScaledHeight() - 22 + hotbarRaiseOffset();
int selected = getSelectedSlot(player);
HotbarSelection selection = updateSelectionAnimation(selected);
@@ -327,6 +329,16 @@ public final class CustomHotbar extends AbstractHudElement {
return ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot();
}
+ /**
+ * Vertical GUI-pixel offset applied to the whole hotbar while the Animations
+ * module slides it into place. The animated value itself is advanced once per
+ * frame inside the module; this only reads the settled value.
+ */
+ private static float hotbarRaiseOffset() {
+ Animations animations = Modules.get(Animations.class);
+ return animations != null ? animations.getHotbarRaiseOffset() : 0.0f;
+ }
+
private static OffHandMode getOffHandMode() {
CustomHotbar bm = CustomHotbar.get();
if (bm == null) return OffHandMode.MERGED;
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java
index 824cf5b..1125ee5 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java
@@ -10,8 +10,6 @@ package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
-import net.minecraft.client.gui.components.BossHealthOverlay;
-import net.minecraft.client.gui.components.LerpingBossEvent;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.resources.Identifier;
@@ -28,11 +26,12 @@ import dev.loki.lovisual.features.gui.hud.AbstractHudElement;
import dev.loki.lovisual.features.gui.hud.HudElementRegister;
import dev.loki.lovisual.features.gui.hud.HudRenderSpace;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
+import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandBoundsPatches;
+import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandRuntimePatches;
+import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandUiUtil;
+import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls;
import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts;
import dev.loki.lovisual.features.module.HudPhase;
-import dev.loki.lovisual.features.module.Modules;
-import dev.loki.lovisual.features.module.modules.misc.ClickGui;
-import dev.loki.lovisual.mixins.accessors.BossHealthOverlayAccessor;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.core.RenderPhase;
@@ -41,32 +40,30 @@ import dev.loki.lovisual.render.engine.core.ViewportContext;
import dev.loki.lovisual.render.engine.math.HudScale;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
-import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds;
import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiRuntime;
import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.UiProjectionMode;
import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.UiRenderContext;
import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.CachedUiScriptRuntime;
-import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiBoundsPatchSet;
-import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptColor;
-import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptPatchSet;
-import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps;
import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptModule;
import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptModuleHandle;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps;
import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.Lifecycle;
import dev.loki.lovisual.util.media.MediaSessionService;
-import dev.loki.lovisual.util.media.RepeatMode;
import dev.loki.lovisual.util.pvp.PvpState;
import java.time.LocalTime;
import java.util.List;
-import java.util.Map;
-import java.util.UUID;
import static dev.loki.lovisual.features.theme.Theme.theme;
+/**
+ * Dynamic Island: оркестрация режимов (time/pvp/music/clickgui) и анимаций.
+ * Патчинг UI-скрипта, bounds-патчи, утилиты и геометрия медиа-контролов
+ * делегированы в {@code impl/dynamicisland/}.
+ */
@HudElementRegister(order = 60)
public final class DynamicIsland extends AbstractHudElement {
private static final String LAYOUT_ID = "lovisual:api/hud/static/dynamic_island";
@@ -83,25 +80,16 @@ public final class DynamicIsland extends AbstractHudElement {
private static final float PVP_MAX_WIDTH = 96f;
private static final float MUSIC_EXPANDED_WIDTH = 338f;
- private static final float CONTEXT_GAP = 7f;
- private static final float CONTEXT_CHIP_H = 20f;
- private static final float CONTEXT_CHIP_Y = 7.5f;
private static final float TIME_CONTEXT_WIDTH = 57f;
- private static final float PVP_CONTEXT_WIDTH = 46f;
private static final float COMPACT_PAD_X = 11f;
private static final float COMPACT_GAP = 9f;
private static final float COMPACT_ART = 22f;
private static final float COMPACT_WAVE_W = 15f;
private static final float COMPACT_FADE = 15f;
- private static final float EXPANDED_PAD_X = 13f;
private static final float EXPANDED_PAD_Y = 11f;
- private static final float EXPANDED_ART = 36f;
private static final float PROGRESS_H = 5f;
- private static final float CONTROL_SIZE = 27f;
- private static final float CONTROL_GAP = 7f;
private static final float CONTROL_TIME_GAP = 10f;
- private static final float CONTROL_ROW_GAP_Y = -5f;
private static final int ICON_PAUSE = 0xEA02;
private static final int ICON_PLAY = 0xEA03;
@@ -109,8 +97,6 @@ public final class DynamicIsland extends AbstractHudElement {
private static final int ICON_NEXT = 0xEA05;
private static final int ICON_SHUFFLE = 0xEA06;
- private static final int BASE_SHADOW = 0xFF000000;
- private static final int BASE_STROKE = 0xFFFFFFFF;
private static final int STATUS_PVP = 0xFFFF6368;
private static final int PROGRESS_BG = 0x2AFFFFFF;
@@ -127,6 +113,7 @@ public final class DynamicIsland extends AbstractHudElement {
private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID);
private final CachedUiScriptRuntime scriptRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter());
+ private final IslandControls controls = new IslandControls();
private float visibilityAnim;
private float expandAnim;
@@ -143,23 +130,6 @@ public final class DynamicIsland extends AbstractHudElement {
private IslandMode currentMode = IslandMode.TIME;
private MediaSessionService.Snapshot currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable());
- private float shuffleX;
- private float shuffleY;
- private float prevX;
- private float prevY;
- private float playX;
- private float playY;
- private float nextX;
- private float nextY;
- private float repeatX;
- private float repeatY;
- private float controlSize;
- private float shuffleHover;
- private float prevHover;
- private float playHover;
- private float nextHover;
- private float repeatHover;
-
private int uiFillTop;
private int uiFillBottom;
private int uiTextPrimary;
@@ -185,206 +155,6 @@ public final class DynamicIsland extends AbstractHudElement {
return INSTANCE.clickGuiShellVisible;
}
- private static void putControlBounds(UiBoundsPatchSet patches,
- String key,
- float x,
- float y,
- float size) {
- putBounds(patches, key + ":hover", x, y, size, size);
- putBounds(patches, key, x, y + 4.0f, size, Math.max(0.0f, size - 4.0f));
- }
-
- private static void putSvgControlBounds(UiBoundsPatchSet patches,
- String key,
- float x,
- float y,
- float size) {
- float iconSize = Math.max(0.0f, size * 0.74f);
- putBounds(patches, key + ":hover", x, y, size, size);
- putBounds(patches, key, x + (size - iconSize) * 0.5f, y + (size - iconSize) * 0.5f, iconSize, iconSize);
- }
-
- private static void putBounds(UiBoundsPatchSet patches,
- String key,
- float x,
- float y,
- float width,
- float height) {
- patches.put(key, x, y, width, height);
- }
-
- private static void patchControl(UiScriptPatchSet patches,
- String key,
- String icon,
- String iconColor,
- String accentSoft,
- float expandedAlpha,
- float hover) {
- patchText(patches, key, icon, scaleHexAlpha(iconColor, expandedAlpha));
- patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha));
- }
-
- private static void patchSvgControl(UiScriptPatchSet patches,
- String key,
- String asset,
- String iconColor,
- String accentSoft,
- float expandedAlpha,
- float hover) {
- patchImage(patches, key, asset, scaleHexAlpha(iconColor, expandedAlpha));
- patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha));
- }
-
- private static void patchText(UiScriptPatchSet patches,
- String key,
- String text,
- String color) {
- patches.text(key, text, color);
- }
-
- private static void patchClippedText(UiScriptPatchSet patches,
- String key,
- float measuredWidth,
- float boxWidth,
- float scrollTime,
- float delay,
- float speed,
- float fadeWidth) {
- patches.clippedText(key, measuredWidth, boxWidth, scrollTime, delay, speed, fadeWidth);
- }
-
- private static void patchImage(UiScriptPatchSet patches,
- String key,
- String asset,
- String tint) {
- patches.image(key, asset, tint);
- }
-
- private static void patchShape(UiScriptPatchSet patches,
- String key,
- Object... pairs) {
- patches.props(key, pairs);
- }
-
- private static void patchShape(UiScriptPatchSet patches,
- String key,
- String prop,
- Object value) {
- patches.put(key, prop, value);
- }
-
- private static void patchShape(UiScriptPatchSet patches,
- String key,
- String propA,
- Object valueA,
- String propB,
- Object valueB) {
- patches.put(key, propA, valueA, propB, valueB);
- }
-
- private static String stringProp(UiScriptProps props, String key, String fallback) {
- return props.string(key, fallback);
- }
-
- private static float numberProp(UiScriptProps props, String key, float fallback) {
- return props.number(key, fallback);
- }
-
- private static boolean boolProp(UiScriptProps props, String key, boolean fallback) {
- return props.bool(key, fallback);
- }
-
- private static float clamp01(float value) {
- return Math.max(0.0f, Math.min(1.0f, value));
- }
-
- private static String scaleHexAlpha(String hex, float amount) {
- return UiScriptColor.alpha(hex, amount);
- }
-
- private static int shuffleColor(MediaSessionService.Snapshot snapshot) {
- if (snapshot == null || !snapshot.supportsShuffle()) {
- return 0x99000000;
- }
- return snapshot.isShuffleActive() ? 0xFFFFFFFF : 0x7AB7BEC8;
- }
-
- private static int repeatColor(MediaSessionService.Snapshot snapshot) {
- if (snapshot == null || !snapshot.supportsRepeat()) {
- return 0x99000000;
- }
- RepeatMode mode = snapshot.repeatMode();
- return mode == RepeatMode.ALL || mode == RepeatMode.ONE ? 0xFFFFFFFF : 0x7AB7BEC8;
- }
-
- private static String repeatSvg(MediaSessionService.Snapshot snapshot) {
- if (snapshot == null || !snapshot.supportsRepeat()) {
- return "repeat-off";
- }
- return snapshot.repeatMode() == RepeatMode.ONE ? "repeat-1" : "repeat";
- }
-
- private static boolean clickGuiEnabled() {
- ClickGui clickGui = Modules.get(ClickGui.class);
- return clickGui != null && clickGui.isEnabled();
- }
-
- private static boolean isSettingsLikeScreen(Screen screen) {
- if (screen == null) return false;
- String name = screen.getClass().getName();
- String simple = screen.getClass().getSimpleName();
- if (name.contains(".gui.screen.option.") || name.contains(".gui.screen.pack.")) return true;
- return simple.contains("Options")
- || simple.contains("Option")
- || simple.contains("Controls")
- || simple.contains("Keybind")
- || simple.contains("Video")
- || simple.contains("Sound")
- || simple.contains("Language")
- || simple.contains("Accessibility")
- || simple.contains("Skin")
- || simple.contains("Pack")
- || simple.contains("Datapack")
- || simple.contains("Customize")
- || simple.contains("Telemetry")
- || simple.contains("Credits");
- }
-
- private static String formatClock(LocalTime time) {
- return String.format("%02d:%02d", time.getHour(), time.getMinute());
- }
-
- private static String formatSecondsOnly(float seconds) {
- return Math.max(0L, (long) Math.ceil(Math.max(0f, seconds))) + "s";
- }
-
- private static String formatShortTime(float seconds) {
- return formatShortTime((long) Math.ceil(Math.max(0f, seconds)));
- }
-
- private static String formatShortTime(long seconds) {
- long sec = Math.max(0L, seconds);
- long m = sec / 60L;
- long s = sec % 60L;
- return String.format("%d:%02d", m, s);
- }
-
- private static String iconString(int codepoint) {
- return new String(Character.toChars(codepoint));
- }
-
- private static String hex(int argb) {
- return String.format("#%08X", argb);
- }
-
- private static float clamp(float value, float min, float max) {
- return Math.max(min, Math.min(max, value));
- }
-
- private static boolean hit(float mx, float my, float x, float y, float w, float h) {
- return mx >= x && mx <= x + w && my >= y && my <= y + h;
- }
-
@Override
protected void defineSettings(List defs) {
defs.add(SettingDef.bool(syncTheme));
@@ -429,24 +199,24 @@ public final class DynamicIsland extends AbstractHudElement {
if (currentMode == IslandMode.MUSIC && expandAnim > 0.01f && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
if (currentSnapshot != null && currentSnapshot.supportsShuffle()
- && hit(mx, my, shuffleX, shuffleY, controlSize, controlSize)) {
+ && IslandUiUtil.hit(mx, my, controls.shuffleX, controls.shuffleY, controls.controlSize, controls.controlSize)) {
mediaService.toggleShuffle();
return true;
}
- if (hit(mx, my, prevX, prevY, controlSize, controlSize)) {
+ if (IslandUiUtil.hit(mx, my, controls.prevX, controls.prevY, controls.controlSize, controls.controlSize)) {
mediaService.previous();
return true;
}
- if (hit(mx, my, playX, playY, controlSize, controlSize)) {
+ if (IslandUiUtil.hit(mx, my, controls.playX, controls.playY, controls.controlSize, controls.controlSize)) {
mediaService.playPause();
return true;
}
- if (hit(mx, my, nextX, nextY, controlSize, controlSize)) {
+ if (IslandUiUtil.hit(mx, my, controls.nextX, controls.nextY, controls.controlSize, controls.controlSize)) {
mediaService.next();
return true;
}
if (currentSnapshot != null && currentSnapshot.supportsRepeat()
- && hit(mx, my, repeatX, repeatY, controlSize, controlSize)) {
+ && IslandUiUtil.hit(mx, my, controls.repeatX, controls.repeatY, controls.controlSize, controls.controlSize)) {
mediaService.cycleRepeatMode();
return true;
}
@@ -474,7 +244,7 @@ public final class DynamicIsland extends AbstractHudElement {
if (!Lifecycle.canRunHud()) return;
if (mc == null || mc.getWindow() == null || ctx == null) return;
Screen currentScreen = ClientScreen.current();
- boolean clickGuiOverlay = isEnabled() && clickGuiEnabled();
+ boolean clickGuiOverlay = isEnabled() && IslandUiUtil.clickGuiEnabled();
if (!clickGuiOverlay && !isScreenOverlayAllowed(currentScreen)) return;
FullScreenRenderer.ensureInit();
@@ -513,7 +283,7 @@ public final class DynamicIsland extends AbstractHudElement {
Screen currentScreen = ClientScreen.current();
boolean chatOpen = currentScreen instanceof ChatScreen;
- boolean clickGuiVisible = clickGuiEnabled();
+ boolean clickGuiVisible = IslandUiUtil.clickGuiEnabled();
boolean clickGuiTabShellScreen = currentScreen instanceof ClickGuiScreen;
ClickGuiRenderer.ClickGuiIslandState clickGuiState = ClickGuiRenderer.islandState();
boolean clickGuiBridge = isEnabled()
@@ -525,7 +295,7 @@ public final class DynamicIsland extends AbstractHudElement {
if (clickGuiVisible && clickGuiTabShellScreen && !clickGuiBridge) {
clickGuiShellVisible = false;
setBounds(0f, 0f, 0f, 0f);
- resetControls();
+ controls.reset();
return;
}
boolean shouldShow = clickGuiBridge
@@ -547,7 +317,7 @@ public final class DynamicIsland extends AbstractHudElement {
if (!shouldShow && visibilityAnim <= 0.01f) {
clickGuiShellVisible = false;
setBounds(0f, 0f, 0f, 0f);
- resetControls();
+ controls.reset();
return;
}
@@ -560,7 +330,7 @@ public final class DynamicIsland extends AbstractHudElement {
IslandMetrics metrics = measureMetrics(titleRenderer, valueRenderer, metaRenderer);
float targetMainWidth = switch (currentMode) {
case TIME -> TIME_WIDTH;
- case PVP -> clamp(metrics.pvpWidth, PVP_MIN_WIDTH, PVP_MAX_WIDTH);
+ case PVP -> IslandUiUtil.clamp(metrics.pvpWidth, PVP_MIN_WIDTH, PVP_MAX_WIDTH);
case MUSIC -> AnimationUtility.lerp(metrics.musicCompactWidth, MUSIC_EXPANDED_WIDTH, expandAnim);
case CLICKGUI -> clickGuiState.tabBarW();
};
@@ -572,7 +342,7 @@ public final class DynamicIsland extends AbstractHudElement {
boolean clickGuiContext = currentMode == IslandMode.CLICKGUI;
float contextPresence = musicContext ? 1.0f - AnimationUtility.easeOutCubic(expandAnim) : (clickGuiContext ? 1.0f : 0.0f);
float targetLeftContextWidth = TIME_CONTEXT_WIDTH * contextPresence;
- float targetRightContextWidth = PvpState.isActive() ? PVP_CONTEXT_WIDTH * contextPresence : 0.0f;
+ float targetRightContextWidth = PvpState.isActive() ? IslandBoundsPatches.PVP_CONTEXT_WIDTH * contextPresence : 0.0f;
mainWidthAnim = mainWidthAnim < 0f ? targetMainWidth : AnimationUtility.approach(mainWidthAnim, targetMainWidth, dt, 12f);
heightAnim = heightAnim < 0f ? targetHeight : AnimationUtility.approach(heightAnim, targetHeight, dt, 12f);
@@ -585,8 +355,8 @@ public final class DynamicIsland extends AbstractHudElement {
float drawMainWidth = mainWidthAnim;
float drawHeight = heightAnim;
- float leftOccupied = leftContextWidthAnim > 0.5f ? leftContextWidthAnim + CONTEXT_GAP : 0.0f;
- float rightOccupied = rightContextWidthAnim > 0.5f ? CONTEXT_GAP + rightContextWidthAnim : 0.0f;
+ float leftOccupied = leftContextWidthAnim > 0.5f ? leftContextWidthAnim + IslandBoundsPatches.CONTEXT_GAP : 0.0f;
+ float rightOccupied = rightContextWidthAnim > 0.5f ? IslandBoundsPatches.CONTEXT_GAP + rightContextWidthAnim : 0.0f;
float drawWidth = leftOccupied + drawMainWidth + rightOccupied;
widthAnim = drawWidth;
float drawX = currentMode == IslandMode.CLICKGUI
@@ -594,7 +364,7 @@ public final class DynamicIsland extends AbstractHudElement {
: (screenW - drawMainWidth) * 0.5f - leftOccupied;
float drawY = currentMode == IslandMode.CLICKGUI
? clickGuiState.tabBarY() - (1.0f - AnimationUtility.easeOutCubic(clickGuiState.lifecycle())) * 18.0f
- : TOP_Y + bossBarOffset(screenH);
+ : TOP_Y + IslandUiUtil.bossBarOffset(mc, screenH, TOP_Y);
float mainX = leftOccupied;
float alpha = AnimationUtility.easeOutCubic(visibilityAnim);
float radius = currentMode == IslandMode.CLICKGUI ? drawHeight * 0.5f : AnimationUtility.lerp(CLOSED_RADIUS, EXPANDED_RADIUS, expandAnim);
@@ -605,16 +375,16 @@ public final class DynamicIsland extends AbstractHudElement {
float expandedContentAlpha = currentMode == IslandMode.MUSIC ? AnimationUtility.easeOutCubic(expandAnim) * alpha : 0f;
float compactContentAlpha = currentMode == IslandMode.MUSIC ? (1f - AnimationUtility.easeOutCubic(expandAnim)) * alpha : 0f;
if (expandedContentAlpha > 0.01f) {
- updateMusicControls(drawX + mainX, drawY, drawMainWidth, valueRenderer);
- updateControlHover(isInteractiveScreen(currentScreen));
+ controls.updateLayout(drawX + mainX, drawY, drawMainWidth, valueRenderer, currentSnapshot);
+ controls.updateHover(isInteractiveScreen(currentScreen), mc, currentSnapshot);
} else {
- resetControls();
+ controls.reset();
}
if (!renderScripted(renderer, fallback, titleRenderer, metaRenderer, ctx, tickDelta, screenW, screenH, drawWidth, drawHeight,
drawMainWidth, mainX, leftContextWidthAnim, rightContextWidthAnim,
alpha, compactContentAlpha, expandedContentAlpha, radius, accent)) {
- drawFallback(renderer, drawX + mainX, drawY, drawMainWidth, drawHeight, radius, alpha, accent);
+ IslandUiUtil.drawFallback(renderer, drawX + mainX, drawY, drawMainWidth, drawHeight, radius, alpha, accent, uiFillTop);
}
}
@@ -647,7 +417,7 @@ public final class DynamicIsland extends AbstractHudElement {
alpha, compactAlpha, expandedAlpha, radius, accent,
titleRenderer, metaRenderer
);
- long treeSignature = islandStructuralSignature(props);
+ long treeSignature = IslandRuntimePatches.islandStructuralSignature(props);
long dataSignature = CachedUiScriptRuntime.signature(props.asMap());
long layoutSignature = treeSignature;
UiRuntime baked = scriptRuntime.bake(
@@ -665,223 +435,14 @@ public final class DynamicIsland extends AbstractHudElement {
width,
height,
props::asMap,
- () -> islandRuntimePatches(props),
- () -> islandBoundsPatches(props)
+ () -> IslandRuntimePatches.islandRuntimePatches(props),
+ () -> IslandBoundsPatches.islandBoundsPatches(props, x, y, controls)
);
if (baked == null) return false;
baked.render(new UiRenderContext(renderer, textRenderer, ctx, tickDelta, UiProjectionMode.UNSCALED_LOGICAL));
return true;
}
- private long islandStructuralSignature(UiScriptProps props) {
- long h = 0xcbf29ce484222325L;
- h = CachedUiScriptRuntime.mix(h, stringProp(props, "mode", ""));
- h = CachedUiScriptRuntime.mix(h, boolProp(props, "blur", false));
- h = CachedUiScriptRuntime.mix(h, numberProp(props, "blurAlpha", 0.0f));
- h = CachedUiScriptRuntime.mix(h, !stringProp(props, "artworkTexture", "").isEmpty());
- h = CachedUiScriptRuntime.mix(h, "music".equals(stringProp(props, "mode", "")));
- h = CachedUiScriptRuntime.mix(h, boolProp(props, "showShuffle", false));
- h = CachedUiScriptRuntime.mix(h, boolProp(props, "showRepeat", false));
- return h;
- }
-
- private Map> islandRuntimePatches(UiScriptProps props) {
- UiScriptPatchSet patches = UiScriptPatchSet.create(48);
- float compactAlpha = numberProp(props, "compactAlpha", 0.0f);
- float expandedAlpha = numberProp(props, "expandedAlpha", 0.0f);
- float contextAlpha = numberProp(props, "contextAlpha", compactAlpha);
- float width = numberProp(props, "width", 0.0f);
- float mainWidth = numberProp(props, "mainWidth", width);
- float progress = clamp01(numberProp(props, "progress", 0.0f));
- String mode = stringProp(props, "mode", "");
- boolean musicMode = "music".equals(mode);
- boolean clickGuiMode = "clickgui".equals(mode);
- boolean shellMode = musicMode || clickGuiMode;
- boolean pvpMode = "pvp".equals(mode);
- boolean timeMode = "time".equals(mode);
- String primary = stringProp(props, "textPrimary", "#FFFFFFFF");
- String secondary = stringProp(props, "textSecondary", "#99FFFFFF");
- String muted = stringProp(props, "textMuted", "#66FFFFFF");
- String accent = stringProp(props, "accent", "#FFFFFFFF");
- String accentSoft = stringProp(props, "accentSoft", "#33FFFFFF");
-
- if (shellMode) {
- float shellBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * numberProp(props, "alpha", 0.0f);
- patchShape(patches, "dynamic-island", "renderRadius", props.get("radius"), "renderBlurAlpha", shellBlurAlpha);
- patchShape(patches, "shell:blur", "blurAlpha", shellBlurAlpha, "radius", props.get("radius"));
- patchShape(patches, "shell:shadow", "fill", props.get("shadow"), "radius", props.get("radius"));
- patchShape(patches, "shell:fill", "startColor", props.get("fillTop"), "endColor", props.get("fillBottom"), "radius", props.get("radius"));
- patchShape(patches, "shell:tint", "startColor", props.get("tintTop"), "endColor", props.get("tintBottom"), "radius", props.get("radius"));
- patchShape(patches, "shell:stroke", "stroke", props.get("stroke"), "radius", props.get("radius"));
- }
-
- if (timeMode) {
- patchShape(patches, "time:box", "fill", props.get("timeFill"), "stroke", props.get("timeStroke"));
- patchText(patches, "time", stringProp(props, "time", ""), primary);
- }
-
- if (pvpMode) {
- patchShape(patches, "pvp:chip", "fill", props.get("pvpFill"), "stroke", props.get("pvpStroke"));
- patchText(patches, "pvp:timer", stringProp(props, "pvpTimer", "0s"), primary);
- }
-
- if (musicMode || clickGuiMode) {
- float contextBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * contextAlpha;
- patchShape(patches, "context:time:blur", "blurAlpha", contextBlurAlpha, "radius", 10.0f);
- patchShape(patches, "context:time:box", "fill", scaleHexAlpha(stringProp(props, "contextFill", "#00000000"), contextAlpha), "stroke", scaleHexAlpha(stringProp(props, "contextStroke", "#00000000"), contextAlpha));
- patchText(patches, "context:time", stringProp(props, "time", ""), scaleHexAlpha(primary, contextAlpha));
- float pvpContextAlpha = boolProp(props, "pvpContextVisible", false) ? contextAlpha : 0.0f;
- patchShape(patches, "context:pvp:blur", "blurAlpha", numberProp(props, "blurAlpha", 0.0f) * pvpContextAlpha, "radius", 10.0f);
- patchShape(patches, "context:pvp:box", "fill", scaleHexAlpha(stringProp(props, "pvpFill", "#00000000"), pvpContextAlpha), "stroke", scaleHexAlpha(stringProp(props, "pvpStroke", "#00000000"), pvpContextAlpha));
- patchText(patches, "context:pvp", stringProp(props, "pvpTimer", ""), scaleHexAlpha(primary, pvpContextAlpha));
- }
-
- if (musicMode) {
- patchText(patches, "music:elapsed:compact", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, compactAlpha));
- patchText(patches, "music:title:compact", stringProp(props, "title", ""), scaleHexAlpha(primary, compactAlpha));
- patchClippedText(patches, "music:title:compact", numberProp(props, "titleWidthCompact", 0.0f), Math.max(0.0f, mainWidth - 124.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 15.0f);
- patchShape(patches, "music:divider", "fill", scaleHexAlpha(muted, 0.4f * compactAlpha));
- patchShape(patches, "wave:0", "fill", scaleHexAlpha(accent, 0.75f * compactAlpha));
- patchShape(patches, "wave:1", "fill", scaleHexAlpha(accent, 0.83f * compactAlpha));
- patchShape(patches, "wave:2", "fill", scaleHexAlpha(accent, 0.91f * compactAlpha));
- patchImage(patches, "artwork:compact", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", compactAlpha));
- patchShape(patches, "artwork:compact:fallback", "fill", scaleHexAlpha(accent, 0.92f * compactAlpha), "stroke", scaleHexAlpha("#33FFFFFF", compactAlpha));
- }
-
- if (musicMode) {
- patchText(patches, "music:title:expanded", stringProp(props, "title", ""), scaleHexAlpha(primary, expandedAlpha));
- patchClippedText(patches, "music:title:expanded", numberProp(props, "titleWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 16.0f);
- patchText(patches, "music:artist:expanded", stringProp(props, "artist", ""), scaleHexAlpha(secondary, expandedAlpha));
- patchClippedText(patches, "music:artist:expanded", numberProp(props, "artistWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.4f, 14.0f, 16.0f);
- patchShape(patches, "music:progress:bg", "fill", scaleHexAlpha(stringProp(props, "progressBg", "#00FFFFFF"), expandedAlpha));
- patchShape(patches, "music:progress:fill",
- "fill", scaleHexAlpha(accent, expandedAlpha),
- "renderWidth", Math.max(0.0f, mainWidth - 26.0f) * progress);
- patchText(patches, "music:elapsed:expanded", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, expandedAlpha));
- patchText(patches, "music:total:expanded", stringProp(props, "total", "0:00"), scaleHexAlpha(secondary, expandedAlpha));
- patchImage(patches, "artwork:expanded", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", expandedAlpha));
- patchShape(patches, "artwork:expanded:fallback", "fill", scaleHexAlpha(accent, 0.92f * expandedAlpha), "stroke", scaleHexAlpha("#33FFFFFF", expandedAlpha));
- if (boolProp(props, "showShuffle", false)) {
- patchControl(patches, "music:shuffle", stringProp(props, "iconShuffle", ""), stringProp(props, "shuffleColor", primary), accentSoft, expandedAlpha, numberProp(props, "shuffleHover", 0.0f));
- }
- patchControl(patches, "music:prev", stringProp(props, "iconPrev", ""), primary, accentSoft, expandedAlpha, numberProp(props, "prevHover", 0.0f));
- patchControl(patches, "music:play", stringProp(props, "iconPlay", ""), primary, accentSoft, expandedAlpha, numberProp(props, "playHover", 0.0f));
- patchControl(patches, "music:next", stringProp(props, "iconNext", ""), primary, accentSoft, expandedAlpha, numberProp(props, "nextHover", 0.0f));
- if (boolProp(props, "showRepeat", false)) {
- patchSvgControl(patches, "music:repeat", stringProp(props, "repeatAsset", "repeat-off"), stringProp(props, "repeatColor", "#99000000"), accentSoft, expandedAlpha, numberProp(props, "repeatHover", 0.0f));
- }
- }
-
- return patches.asMap();
- }
-
- private Map islandBoundsPatches(UiScriptProps props) {
- UiBoundsPatchSet patches = UiBoundsPatchSet.create(48);
- float rootX = x;
- float rootY = y;
- float width = numberProp(props, "width", 0.0f);
- float height = numberProp(props, "height", 0.0f);
- float mainX = numberProp(props, "mainX", 0.0f);
- float mainWidth = numberProp(props, "mainWidth", width);
- float leftContextWidth = numberProp(props, "leftContextWidth", 0.0f);
- float rightContextWidth = numberProp(props, "rightContextWidth", 0.0f);
- String mode = stringProp(props, "mode", "");
- boolean musicMode = "music".equals(mode);
- boolean clickGuiMode = "clickgui".equals(mode);
-
- putBounds(patches, "dynamic-island", rootX, rootY, width, height);
- putBounds(patches, "content", rootX, rootY, width, height);
-
- if (musicMode || clickGuiMode) {
- putBounds(patches, "shell:blur", rootX + mainX, rootY, mainWidth, height);
- putBounds(patches, "shell:shadow", rootX + mainX, rootY, mainWidth, height);
- putBounds(patches, "shell:fill", rootX + mainX, rootY, mainWidth, height);
- putBounds(patches, "shell:tint", rootX + mainX, rootY, mainWidth, height);
- putBounds(patches, "shell:stroke", rootX + mainX, rootY, mainWidth, height);
- }
-
- if (musicMode || clickGuiMode) {
- putBounds(patches, "context:time:blur", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H);
- putBounds(patches, "context:time:box", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H);
- putBounds(patches, "context:time", rootX, rootY + 10.1f, leftContextWidth, 14.0f);
- float rightX = mainX + mainWidth + CONTEXT_GAP;
- putBounds(patches, "context:pvp:blur", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H);
- putBounds(patches, "context:pvp:box", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H);
- putBounds(patches, "context:pvp", rootX + rightX, rootY + 10.1f, rightContextWidth, 14.0f);
- }
-
- if (musicMode) {
- putMusicCompactBounds(patches, rootX, rootY, props, mainX, mainWidth);
- putMusicExpandedBounds(patches, rootX, rootY, props, mainWidth);
- } else if ("pvp".equals(mode)) {
- float chipW = Math.min(mainWidth - 16.0f, Math.max(42.0f, PVP_CONTEXT_WIDTH + 8.0f));
- float chipX = mainX + (mainWidth - chipW) * 0.5f;
- putBounds(patches, "pvp:chip", rootX + chipX, rootY + CONTEXT_CHIP_Y, chipW, CONTEXT_CHIP_H);
- putBounds(patches, "pvp:timer", rootX + chipX, rootY + 10.1f, chipW, 14.0f);
- } else {
- putBounds(patches, "time:box", rootX + mainX, rootY + CONTEXT_CHIP_Y, mainWidth, CONTEXT_CHIP_H);
- putBounds(patches, "time", rootX + mainX, rootY + 7.8f, mainWidth, 20.0f);
- }
-
- return patches.asMap();
- }
-
- private void putMusicCompactBounds(UiBoundsPatchSet patches,
- float rootX,
- float rootY,
- UiScriptProps props,
- float mainX,
- float width) {
- float waveX = mainX + width - 26.0f;
- putBounds(patches, "music:elapsed:compact", rootX + mainX + 11.0f, rootY + 10.5f, 38.0f, 13.0f);
- putBounds(patches, "music:divider", rootX + mainX + 49.0f, rootY + 7.0f, 1.0f, 18.0f);
- putBounds(patches, "artwork:compact", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f);
- putBounds(patches, "artwork:compact:fallback", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f);
- float titleW = Math.max(0.0f, width - 124.0f);
- putBounds(patches, "music:title:compact:clip", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f);
- putBounds(patches, "music:title:compact", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f);
-
- float centerY = 17.5f;
- float t = Util.getMillis() / 1000.0f;
- boolean playing = boolProp(props, "playing", false);
- for (int i = 0; i < 3; i++) {
- float barH = playing
- ? (float) (3.0f + Math.abs(Math.sin(t * 5.2f + i * 0.85f)) * 8.0f)
- : 4.0f + i;
- putBounds(patches, "wave:" + i, rootX + waveX + i * 5.0f, rootY + centerY - barH * 0.5f, 2.5f, barH);
- }
- }
-
- private void putMusicExpandedBounds(UiBoundsPatchSet patches,
- float rootX,
- float rootY,
- UiScriptProps props,
- float width) {
- float mainX = numberProp(props, "mainX", 0.0f);
- float progressW = Math.max(0.0f, width - 26.0f);
- putBounds(patches, "artwork:expanded", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f);
- putBounds(patches, "artwork:expanded:fallback", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f);
- float titleW = Math.max(0.0f, width - 72.0f);
- putBounds(patches, "music:title:expanded:clip", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f);
- putBounds(patches, "music:title:expanded", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f);
- putBounds(patches, "music:artist:expanded:clip", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f);
- putBounds(patches, "music:artist:expanded", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f);
- putBounds(patches, "music:progress:bg", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f);
- putBounds(patches, "music:progress:fill", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f);
- putBounds(patches, "music:elapsed:expanded", rootX + mainX + 13.0f, rootY + 65.0f, 44.0f, 14.0f);
- putBounds(patches, "music:total:expanded", rootX + mainX + width - 57.0f, rootY + 65.0f, 44.0f, 14.0f);
-
- if (boolProp(props, "showShuffle", false)) {
- putControlBounds(patches, "music:shuffle", shuffleX, shuffleY, controlSize);
- }
- putControlBounds(patches, "music:prev", prevX, prevY, controlSize);
- putControlBounds(patches, "music:play", playX, playY, controlSize);
- putControlBounds(patches, "music:next", nextX, nextY, controlSize);
- if (boolProp(props, "showRepeat", false)) {
- putSvgControlBounds(patches, "music:repeat", repeatX, repeatY, controlSize);
- }
- }
-
private UiScriptProps buildProps(int screenW,
int screenH,
float width,
@@ -909,7 +470,7 @@ public final class DynamicIsland extends AbstractHudElement {
props.put("mainX", mainX);
props.put("leftContextWidth", leftContextWidth);
props.put("rightContextWidth", rightContextWidth);
- props.put("contextGap", CONTEXT_GAP);
+ props.put("contextGap", IslandBoundsPatches.CONTEXT_GAP);
boolean pvpContextVisible = (currentMode == IslandMode.MUSIC || currentMode == IslandMode.CLICKGUI) && PvpState.isActive() && rightContextWidth > 0.5f;
props.put("pvpContextVisible", pvpContextVisible);
props.put("contextAlpha", currentMode == IslandMode.MUSIC ? compactAlpha : (currentMode == IslandMode.CLICKGUI ? alpha : 0.0f));
@@ -922,70 +483,70 @@ public final class DynamicIsland extends AbstractHudElement {
props.put("blur", blur.get());
props.put("blurAlpha", blurAlpha.get() / 255.0f);
props.put("radius", radius);
- props.put("time", formatClock(LocalTime.now()));
- props.put("pvpTimer", pvpContextVisible || currentMode == IslandMode.PVP ? formatSecondsOnly(predictedPvpSeconds()) : "");
+ props.put("time", IslandUiUtil.formatClock(LocalTime.now()));
+ props.put("pvpTimer", pvpContextVisible || currentMode == IslandMode.PVP ? IslandUiUtil.formatSecondsOnly(predictedPvpSeconds()) : "");
ClickGuiRenderer.ClickGuiIslandState clickGuiState = ClickGuiRenderer.islandState();
props.put("clickGuiLabel", clickGuiState.activeLabel());
props.put("clickGuiTabCount", clickGuiState.tabCount());
props.put("clickGuiPicker", clickGuiState.pickerActive());
props.put("title", title);
props.put("artist", artist);
- props.put("titleWidthCompact", measureWidth(titleRenderer, title, 0.82f));
- props.put("titleWidthExpanded", measureWidth(titleRenderer, title, 0.86f));
- props.put("artistWidthExpanded", measureWidth(metaRenderer, artist, 0.72f));
+ props.put("titleWidthCompact", IslandUiUtil.measureWidth(titleRenderer, title, 0.82f));
+ props.put("titleWidthExpanded", IslandUiUtil.measureWidth(titleRenderer, title, 0.86f));
+ props.put("artistWidthExpanded", IslandUiUtil.measureWidth(metaRenderer, artist, 0.72f));
props.put("titleScrollTime", titleScrollSeconds(title, artist, duration));
- props.put("elapsed", formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L));
- props.put("total", formatShortTime(currentSnapshot != null ? currentSnapshot.durationSeconds() : 0L));
+ props.put("elapsed", IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L));
+ props.put("total", IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.durationSeconds() : 0L));
props.put("progress", currentSnapshot != null && currentSnapshot.durationSeconds() > 0
? AnimationUtility.clamp((float) currentSnapshot.predictedPositionSeconds() / (float) currentSnapshot.durationSeconds(), 0f, 1f)
: 0f);
props.put("playing", currentSnapshot != null && currentSnapshot.isPlaying());
Identifier artwork = currentSnapshot != null ? currentSnapshot.artworkTexture() : null;
props.put("artworkTexture", artwork != null ? artwork.toString() : "");
- props.put("fillTop", hex(HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha)));
- props.put("fillBottom", hex(HudRenderUtil.scaleAlpha(uiFillBottom, 0.88f * alpha)));
- props.put("tintTop", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.16f * alpha)));
- props.put("tintBottom", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF000000, 0.25f), 0.112f * alpha)));
- props.put("shadow", hex(HudRenderUtil.scaleAlpha(BASE_SHADOW, 0.28f * alpha)));
- props.put("stroke", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(syncTheme.get() ? theme().windowStroke() : BASE_STROKE, accent, 0.18f), 0.18f * alpha)));
- props.put("highlight", hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.08f * alpha)));
- props.put("contextFill", hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha)));
- props.put("contextStroke", hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha)));
- props.put("timeFill", hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.48f * alpha)));
- props.put("timeStroke", hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha)));
- props.put("pvpFill", hex(HudRenderUtil.scaleAlpha(STATUS_PVP, 0.86f * alpha)));
- props.put("pvpStroke", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(STATUS_PVP, 0xFFFFFFFF, 0.32f), 0.34f * alpha)));
- props.put("accent", hex(HudRenderUtil.scaleAlpha(accent, alpha)));
- props.put("accentSoft", hex(HudRenderUtil.scaleAlpha(uiAccentSoft, alpha)));
- props.put("textPrimary", hex(HudRenderUtil.scaleAlpha(uiTextPrimary, alpha)));
- props.put("textSecondary", hex(HudRenderUtil.scaleAlpha(uiTextSecondary, alpha)));
- props.put("textMuted", hex(HudRenderUtil.scaleAlpha(uiTextMuted, alpha)));
- props.put("progressBg", hex(HudRenderUtil.scaleAlpha(PROGRESS_BG, expandedAlpha)));
- props.put("shuffleX", shuffleX - x);
- props.put("shuffleY", shuffleY - y);
- props.put("prevX", prevX - x);
- props.put("prevY", prevY - y);
- props.put("playX", playX - x);
- props.put("playY", playY - y);
- props.put("nextX", nextX - x);
- props.put("nextY", nextY - y);
- props.put("repeatX", repeatX - x);
- props.put("repeatY", repeatY - y);
- props.put("controlSize", controlSize);
+ props.put("fillTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha)));
+ props.put("fillBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillBottom, 0.88f * alpha)));
+ props.put("tintTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.16f * alpha)));
+ props.put("tintBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF000000, 0.25f), 0.112f * alpha)));
+ props.put("shadow", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(IslandUiUtil.BASE_SHADOW, 0.28f * alpha)));
+ props.put("stroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(syncTheme.get() ? theme().windowStroke() : IslandUiUtil.BASE_STROKE, accent, 0.18f), 0.18f * alpha)));
+ props.put("highlight", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.08f * alpha)));
+ props.put("contextFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha)));
+ props.put("contextStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha)));
+ props.put("timeFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.48f * alpha)));
+ props.put("timeStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha)));
+ props.put("pvpFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(STATUS_PVP, 0.86f * alpha)));
+ props.put("pvpStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(STATUS_PVP, 0xFFFFFFFF, 0.32f), 0.34f * alpha)));
+ props.put("accent", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(accent, alpha)));
+ props.put("accentSoft", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiAccentSoft, alpha)));
+ props.put("textPrimary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextPrimary, alpha)));
+ props.put("textSecondary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextSecondary, alpha)));
+ props.put("textMuted", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextMuted, alpha)));
+ props.put("progressBg", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(PROGRESS_BG, expandedAlpha)));
+ props.put("shuffleX", controls.shuffleX - x);
+ props.put("shuffleY", controls.shuffleY - y);
+ props.put("prevX", controls.prevX - x);
+ props.put("prevY", controls.prevY - y);
+ props.put("playX", controls.playX - x);
+ props.put("playY", controls.playY - y);
+ props.put("nextX", controls.nextX - x);
+ props.put("nextY", controls.nextY - y);
+ props.put("repeatX", controls.repeatX - x);
+ props.put("repeatY", controls.repeatY - y);
+ props.put("controlSize", controls.controlSize);
props.put("showShuffle", currentSnapshot != null && currentSnapshot.supportsShuffle());
props.put("showRepeat", currentSnapshot != null && currentSnapshot.supportsRepeat());
- props.put("shuffleHover", shuffleHover);
- props.put("prevHover", prevHover);
- props.put("playHover", playHover);
- props.put("nextHover", nextHover);
- props.put("repeatHover", repeatHover);
- props.put("iconShuffle", iconString(ICON_SHUFFLE));
- props.put("iconPrev", iconString(ICON_PREV));
- props.put("iconPlay", iconString(currentSnapshot != null && currentSnapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY));
- props.put("iconNext", iconString(ICON_NEXT));
- props.put("shuffleColor", hex(shuffleColor(currentSnapshot)));
- props.put("repeatColor", hex(repeatColor(currentSnapshot)));
- props.put("repeatAsset", repeatSvg(currentSnapshot));
+ props.put("shuffleHover", controls.shuffleHover);
+ props.put("prevHover", controls.prevHover);
+ props.put("playHover", controls.playHover);
+ props.put("nextHover", controls.nextHover);
+ props.put("repeatHover", controls.repeatHover);
+ props.put("iconShuffle", IslandUiUtil.iconString(ICON_SHUFFLE));
+ props.put("iconPrev", IslandUiUtil.iconString(ICON_PREV));
+ props.put("iconPlay", IslandUiUtil.iconString(currentSnapshot != null && currentSnapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY));
+ props.put("iconNext", IslandUiUtil.iconString(ICON_NEXT));
+ props.put("shuffleColor", IslandUiUtil.hex(IslandUiUtil.shuffleColor(currentSnapshot)));
+ props.put("repeatColor", IslandUiUtil.hex(IslandUiUtil.repeatColor(currentSnapshot)));
+ props.put("repeatAsset", IslandUiUtil.repeatSvg(currentSnapshot));
return props;
}
@@ -1023,55 +584,15 @@ public final class DynamicIsland extends AbstractHudElement {
private IslandMetrics measureMetrics(TextRenderer titleRenderer,
TextRenderer valueRenderer,
TextRenderer metaRenderer) {
- float pvpTimerW = reservedTimeWidth(valueRenderer, formatSecondsOnly(predictedPvpSeconds()), 0.84f);
+ float pvpTimerW = IslandUiUtil.reservedTimeWidth(valueRenderer, IslandUiUtil.formatSecondsOnly(predictedPvpSeconds()), 0.84f);
float pvpWidth = 24f + pvpTimerW;
String title = currentSnapshot != null ? currentSnapshot.title() : "";
- float titleW = measureWidth(titleRenderer, title, 0.82f);
- String compactElapsed = formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L);
- float elapsedW = reservedTimeWidth(valueRenderer, compactElapsed, 0.72f);
+ float titleW = IslandUiUtil.measureWidth(titleRenderer, title, 0.82f);
+ String compactElapsed = IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L);
+ float elapsedW = IslandUiUtil.reservedTimeWidth(valueRenderer, compactElapsed, 0.72f);
float musicWidth = 10f + elapsedW + 18f + COMPACT_ART + COMPACT_GAP + titleW + COMPACT_GAP + COMPACT_WAVE_W + 10f;
- return new IslandMetrics(pvpWidth, clamp(musicWidth, 226f, 308f));
- }
-
- private void updateMusicControls(float drawX, float drawY, float width, TextRenderer valueRenderer) {
- float progressX = drawX + EXPANDED_PAD_X;
- float progressW = width - EXPANDED_PAD_X * 2f;
- float progressY = drawY + 52f;
- float timeH = measureHeight(valueRenderer, 0.80f);
-
- controlSize = CONTROL_SIZE;
- float controlGap = CONTROL_GAP;
- boolean showShuffle = currentSnapshot != null && currentSnapshot.supportsShuffle();
- boolean showRepeat = currentSnapshot != null && currentSnapshot.supportsRepeat();
- int activeControls = 3 + (showShuffle ? 1 : 0) + (showRepeat ? 1 : 0);
- float totalControlsW = controlSize * activeControls + controlGap * Math.max(0, activeControls - 1);
- float controlsX = drawX + (width - totalControlsW) * 0.5f;
- float controlsY = progressY + 5f + timeH + CONTROL_ROW_GAP_Y;
- float cursorX = controlsX;
-
- if (showShuffle) {
- shuffleX = cursorX;
- shuffleY = controlsY;
- cursorX += controlSize + controlGap;
- } else {
- shuffleX = shuffleY = 0f;
- }
- prevX = cursorX;
- prevY = controlsY;
- cursorX += controlSize + controlGap;
- playX = cursorX;
- playY = controlsY;
- cursorX += controlSize + controlGap;
- nextX = cursorX;
- nextY = controlsY;
- cursorX += controlSize + controlGap;
- if (showRepeat) {
- repeatX = cursorX;
- repeatY = controlsY;
- } else {
- repeatX = repeatY = 0f;
- }
+ return new IslandMetrics(pvpWidth, IslandUiUtil.clamp(musicWidth, 226f, 308f));
}
private boolean isInteractiveScreen(Screen screen) {
@@ -1081,91 +602,7 @@ public final class DynamicIsland extends AbstractHudElement {
private boolean isScreenOverlayAllowed(Screen screen) {
if (screen == null || screen instanceof ChatScreen) return false;
if (screen instanceof ClickGuiScreen) return false;
- return !isSettingsLikeScreen(screen);
- }
-
- private float bossBarOffset(int logicalScreenH) {
- if (mc == null || mc.gui == null || mc.getWindow() == null) return 0f;
- BossHealthOverlay bossBarHud = mc.gui.hud.getBossOverlay();
- if (bossBarHud == null) return 0f;
- Map bars;
- try {
- bars = ((BossHealthOverlayAccessor) bossBarHud).getBars();
- } catch (Throwable ignored) {
- return 0f;
- }
- if (bars == null || bars.isEmpty()) return 0f;
-
- int scaledH = Math.max(1, mc.getWindow().getGuiScaledHeight());
- int y = 12;
- int rows = 0;
- for (LerpingBossEvent ignored : bars.values()) {
- rows++;
- y += 19;
- if (y >= scaledH / 3) break;
- }
- if (rows <= 0) return 0f;
-
- float bossBottomScaled = 17f + 19f * (rows - 1);
- float hudScale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight());
- float windowScale = (float) mc.getWindow().getGuiScale();
- float bossBottomLogical = (bossBottomScaled + 8f) * windowScale / Math.max(0.001f, hudScale);
- return Math.max(0f, Math.min(logicalScreenH * 0.30f, bossBottomLogical) - TOP_Y);
- }
-
- private void drawFallback(Renderer2D renderer,
- float drawX,
- float drawY,
- float drawWidth,
- float drawHeight,
- float radius,
- float alpha,
- int accent) {
- renderer.roundedRectShadow(drawX, drawY, drawWidth, drawHeight, radius, 8f, 14f,
- HudRenderUtil.scaleAlpha(BASE_SHADOW, 0.28f * alpha));
- renderer.roundedRect(drawX, drawY, drawWidth, drawHeight, radius, 1.1f,
- HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha));
- renderer.roundedRectStroke(drawX, drawY, drawWidth, drawHeight, radius, 1.1f, 1f,
- HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(BASE_STROKE, accent, 0.18f), 0.18f * alpha));
- }
-
- private void updateControlHover(boolean chatOpen) {
- if (!chatOpen || controlSize <= 0f || mc == null || mc.getWindow() == null) {
- shuffleHover = AnimationUtility.approach(shuffleHover, 0f, 0.25f);
- prevHover = AnimationUtility.approach(prevHover, 0f, 0.25f);
- playHover = AnimationUtility.approach(playHover, 0f, 0.25f);
- nextHover = AnimationUtility.approach(nextHover, 0f, 0.25f);
- repeatHover = AnimationUtility.approach(repeatHover, 0f, 0.25f);
- return;
- }
-
- int fbw = mc.getWindow().getWidth();
- int fbh = mc.getWindow().getHeight();
- float uiScale = HudScale.scale(fbw, fbh);
- float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale);
- float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale);
- boolean showShuffle = currentSnapshot != null && currentSnapshot.supportsShuffle();
- boolean showRepeat = currentSnapshot != null && currentSnapshot.supportsRepeat();
-
- shuffleHover = AnimationUtility.approach(shuffleHover, showShuffle && hit(mx, my, shuffleX, shuffleY, controlSize, controlSize) ? 1f : 0f, 0.25f);
- prevHover = AnimationUtility.approach(prevHover, hit(mx, my, prevX, prevY, controlSize, controlSize) ? 1f : 0f, 0.25f);
- playHover = AnimationUtility.approach(playHover, hit(mx, my, playX, playY, controlSize, controlSize) ? 1f : 0f, 0.25f);
- nextHover = AnimationUtility.approach(nextHover, hit(mx, my, nextX, nextY, controlSize, controlSize) ? 1f : 0f, 0.25f);
- repeatHover = AnimationUtility.approach(repeatHover, showRepeat && hit(mx, my, repeatX, repeatY, controlSize, controlSize) ? 1f : 0f, 0.25f);
- }
-
- private void resetControls() {
- shuffleX = shuffleY = 0f;
- prevX = prevY = 0f;
- playX = playY = 0f;
- nextX = nextY = 0f;
- repeatX = repeatY = 0f;
- controlSize = 0f;
- shuffleHover = 0f;
- prevHover = 0f;
- playHover = 0f;
- nextHover = 0f;
- repeatHover = 0f;
+ return !IslandUiUtil.isSettingsLikeScreen(screen);
}
private int accentFor(IslandMode mode) {
@@ -1208,40 +645,6 @@ public final class DynamicIsland extends AbstractHudElement {
return Math.max(0f, secondsLeft - sinceUpdate / 1000f);
}
- private float measureWidth(TextRenderer renderer, String text, float scale) {
- if (renderer == null || text == null || text.isEmpty()) return 0f;
- renderer.begin(scale, true, false);
- try {
- return (float) renderer.getWidth(text, false);
- } finally {
- renderer.end();
- }
- }
-
- private float measureHeight(TextRenderer renderer, float scale) {
- if (renderer == null) return 0f;
- renderer.begin(scale, true, false);
- try {
- return (float) renderer.getHeight(false);
- } finally {
- renderer.end();
- }
- }
-
- private float reservedTimeWidth(TextRenderer renderer, String text, float scale) {
- return measureWidth(renderer, reserveTimeTemplate(text), scale);
- }
-
- private String reserveTimeTemplate(String text) {
- if (text == null || text.isEmpty()) return "0:00";
- StringBuilder out = new StringBuilder(text.length());
- for (int i = 0; i < text.length(); i++) {
- char c = text.charAt(i);
- out.append(Character.isDigit(c) ? '8' : c);
- }
- return out.toString();
- }
-
private enum IslandMode {
TIME,
MUSIC,
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java
new file mode 100644
index 0000000..1cace62
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java
@@ -0,0 +1,141 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland;
+
+import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiBoundsPatchSet;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps;
+import net.minecraft.util.Util;
+
+import java.util.Map;
+
+import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandPatchOps.*;
+
+/**
+ * Bounds-патчи узлов UI-скрипта Dynamic Island по режимам (time/pvp/music/clickgui).
+ */
+public final class IslandBoundsPatches {
+
+ public static final float CONTEXT_GAP = 7f;
+ public static final float CONTEXT_CHIP_H = 20f;
+ public static final float CONTEXT_CHIP_Y = 7.5f;
+ public static final float PVP_CONTEXT_WIDTH = 46f;
+
+ private IslandBoundsPatches() {
+ }
+
+ public static Map islandBoundsPatches(UiScriptProps props,
+ float rootX,
+ float rootY,
+ IslandControls controls) {
+ UiBoundsPatchSet patches = UiBoundsPatchSet.create(48);
+ float width = numberProp(props, "width", 0.0f);
+ float height = numberProp(props, "height", 0.0f);
+ float mainX = numberProp(props, "mainX", 0.0f);
+ float mainWidth = numberProp(props, "mainWidth", width);
+ float leftContextWidth = numberProp(props, "leftContextWidth", 0.0f);
+ float rightContextWidth = numberProp(props, "rightContextWidth", 0.0f);
+ String mode = stringProp(props, "mode", "");
+ boolean musicMode = "music".equals(mode);
+ boolean clickGuiMode = "clickgui".equals(mode);
+
+ putBounds(patches, "dynamic-island", rootX, rootY, width, height);
+ putBounds(patches, "content", rootX, rootY, width, height);
+
+ if (musicMode || clickGuiMode) {
+ putBounds(patches, "shell:blur", rootX + mainX, rootY, mainWidth, height);
+ putBounds(patches, "shell:shadow", rootX + mainX, rootY, mainWidth, height);
+ putBounds(patches, "shell:fill", rootX + mainX, rootY, mainWidth, height);
+ putBounds(patches, "shell:tint", rootX + mainX, rootY, mainWidth, height);
+ putBounds(patches, "shell:stroke", rootX + mainX, rootY, mainWidth, height);
+ }
+
+ if (musicMode || clickGuiMode) {
+ putBounds(patches, "context:time:blur", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H);
+ putBounds(patches, "context:time:box", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H);
+ putBounds(patches, "context:time", rootX, rootY + 10.1f, leftContextWidth, 14.0f);
+ float rightX = mainX + mainWidth + CONTEXT_GAP;
+ putBounds(patches, "context:pvp:blur", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H);
+ putBounds(patches, "context:pvp:box", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H);
+ putBounds(patches, "context:pvp", rootX + rightX, rootY + 10.1f, rightContextWidth, 14.0f);
+ }
+
+ if (musicMode) {
+ putMusicCompactBounds(patches, rootX, rootY, props, mainX, mainWidth);
+ putMusicExpandedBounds(patches, rootX, rootY, props, mainWidth, controls);
+ } else if ("pvp".equals(mode)) {
+ float chipW = Math.min(mainWidth - 16.0f, Math.max(42.0f, PVP_CONTEXT_WIDTH + 8.0f));
+ float chipX = mainX + (mainWidth - chipW) * 0.5f;
+ putBounds(patches, "pvp:chip", rootX + chipX, rootY + CONTEXT_CHIP_Y, chipW, CONTEXT_CHIP_H);
+ putBounds(patches, "pvp:timer", rootX + chipX, rootY + 10.1f, chipW, 14.0f);
+ } else {
+ putBounds(patches, "time:box", rootX + mainX, rootY + CONTEXT_CHIP_Y, mainWidth, CONTEXT_CHIP_H);
+ putBounds(patches, "time", rootX + mainX, rootY + 7.8f, mainWidth, 20.0f);
+ }
+
+ return patches.asMap();
+ }
+
+ private static void putMusicCompactBounds(UiBoundsPatchSet patches,
+ float rootX,
+ float rootY,
+ UiScriptProps props,
+ float mainX,
+ float width) {
+ float waveX = mainX + width - 26.0f;
+ putBounds(patches, "music:elapsed:compact", rootX + mainX + 11.0f, rootY + 10.5f, 38.0f, 13.0f);
+ putBounds(patches, "music:divider", rootX + mainX + 49.0f, rootY + 7.0f, 1.0f, 18.0f);
+ putBounds(patches, "artwork:compact", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f);
+ putBounds(patches, "artwork:compact:fallback", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f);
+ float titleW = Math.max(0.0f, width - 124.0f);
+ putBounds(patches, "music:title:compact:clip", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f);
+ putBounds(patches, "music:title:compact", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f);
+
+ float centerY = 17.5f;
+ float t = Util.getMillis() / 1000.0f;
+ boolean playing = boolProp(props, "playing", false);
+ for (int i = 0; i < 3; i++) {
+ float barH = playing
+ ? (float) (3.0f + Math.abs(Math.sin(t * 5.2f + i * 0.85f)) * 8.0f)
+ : 4.0f + i;
+ putBounds(patches, "wave:" + i, rootX + waveX + i * 5.0f, rootY + centerY - barH * 0.5f, 2.5f, barH);
+ }
+ }
+
+ private static void putMusicExpandedBounds(UiBoundsPatchSet patches,
+ float rootX,
+ float rootY,
+ UiScriptProps props,
+ float width,
+ IslandControls controls) {
+ float mainX = numberProp(props, "mainX", 0.0f);
+ float progressW = Math.max(0.0f, width - 26.0f);
+ putBounds(patches, "artwork:expanded", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f);
+ putBounds(patches, "artwork:expanded:fallback", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f);
+ float titleW = Math.max(0.0f, width - 72.0f);
+ putBounds(patches, "music:title:expanded:clip", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f);
+ putBounds(patches, "music:title:expanded", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f);
+ putBounds(patches, "music:artist:expanded:clip", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f);
+ putBounds(patches, "music:artist:expanded", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f);
+ putBounds(patches, "music:progress:bg", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f);
+ putBounds(patches, "music:progress:fill", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f);
+ putBounds(patches, "music:elapsed:expanded", rootX + mainX + 13.0f, rootY + 65.0f, 44.0f, 14.0f);
+ putBounds(patches, "music:total:expanded", rootX + mainX + width - 57.0f, rootY + 65.0f, 44.0f, 14.0f);
+
+ if (boolProp(props, "showShuffle", false)) {
+ IslandPatchOps.putControlBounds(patches, "music:shuffle", controls.shuffleX, controls.shuffleY, controls.controlSize);
+ }
+ IslandPatchOps.putControlBounds(patches, "music:prev", controls.prevX, controls.prevY, controls.controlSize);
+ IslandPatchOps.putControlBounds(patches, "music:play", controls.playX, controls.playY, controls.controlSize);
+ IslandPatchOps.putControlBounds(patches, "music:next", controls.nextX, controls.nextY, controls.controlSize);
+ if (boolProp(props, "showRepeat", false)) {
+ IslandPatchOps.putSvgControlBounds(patches, "music:repeat", controls.repeatX, controls.repeatY, controls.controlSize);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java
new file mode 100644
index 0000000..9a87d2b
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java
@@ -0,0 +1,139 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland;
+
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiBoundsPatchSet;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptColor;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptPatchSet;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps;
+
+/**
+ * Примитивы патчинга UI-скрипта: текст, изображения, формы и bounds-патчи.
+ */
+public final class IslandPatchOps {
+
+ private IslandPatchOps() {
+ }
+
+ public static void putControlBounds(UiBoundsPatchSet patches,
+ String key,
+ float x,
+ float y,
+ float size) {
+ putBounds(patches, key + ":hover", x, y, size, size);
+ putBounds(patches, key, x, y + 4.0f, size, Math.max(0.0f, size - 4.0f));
+ }
+
+ public static void putSvgControlBounds(UiBoundsPatchSet patches,
+ String key,
+ float x,
+ float y,
+ float size) {
+ float iconSize = Math.max(0.0f, size * 0.74f);
+ putBounds(patches, key + ":hover", x, y, size, size);
+ putBounds(patches, key, x + (size - iconSize) * 0.5f, y + (size - iconSize) * 0.5f, iconSize, iconSize);
+ }
+
+ public static void putBounds(UiBoundsPatchSet patches,
+ String key,
+ float x,
+ float y,
+ float width,
+ float height) {
+ patches.put(key, x, y, width, height);
+ }
+
+ public static void patchControl(UiScriptPatchSet patches,
+ String key,
+ String icon,
+ String iconColor,
+ String accentSoft,
+ float expandedAlpha,
+ float hover) {
+ patchText(patches, key, icon, scaleHexAlpha(iconColor, expandedAlpha));
+ patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha));
+ }
+
+ public static void patchSvgControl(UiScriptPatchSet patches,
+ String key,
+ String asset,
+ String iconColor,
+ String accentSoft,
+ float expandedAlpha,
+ float hover) {
+ patchImage(patches, key, asset, scaleHexAlpha(iconColor, expandedAlpha));
+ patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha));
+ }
+
+ public static void patchText(UiScriptPatchSet patches,
+ String key,
+ String text,
+ String color) {
+ patches.text(key, text, color);
+ }
+
+ public static void patchClippedText(UiScriptPatchSet patches,
+ String key,
+ float measuredWidth,
+ float boxWidth,
+ float scrollTime,
+ float delay,
+ float speed,
+ float fadeWidth) {
+ patches.clippedText(key, measuredWidth, boxWidth, scrollTime, delay, speed, fadeWidth);
+ }
+
+ public static void patchImage(UiScriptPatchSet patches,
+ String key,
+ String asset,
+ String tint) {
+ patches.image(key, asset, tint);
+ }
+
+ public static void patchShape(UiScriptPatchSet patches,
+ String key,
+ Object... pairs) {
+ patches.props(key, pairs);
+ }
+
+ public static void patchShape(UiScriptPatchSet patches,
+ String key,
+ String prop,
+ Object value) {
+ patches.put(key, prop, value);
+ }
+
+ public static void patchShape(UiScriptPatchSet patches,
+ String key,
+ String propA,
+ Object valueA,
+ String propB,
+ Object valueB) {
+ patches.put(key, propA, valueA, propB, valueB);
+ }
+
+ public static String stringProp(UiScriptProps props, String key, String fallback) {
+ return props.string(key, fallback);
+ }
+
+ public static float numberProp(UiScriptProps props, String key, float fallback) {
+ return props.number(key, fallback);
+ }
+
+ public static boolean boolProp(UiScriptProps props, String key, boolean fallback) {
+ return props.bool(key, fallback);
+ }
+
+ public static float clamp01(float value) {
+ return Math.max(0.0f, Math.min(1.0f, value));
+ }
+
+ public static String scaleHexAlpha(String hex, float amount) {
+ return UiScriptColor.alpha(hex, amount);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java
new file mode 100644
index 0000000..35d35bb
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java
@@ -0,0 +1,127 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland;
+
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.CachedUiScriptRuntime;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptPatchSet;
+import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps;
+
+import java.util.Map;
+
+import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandPatchOps.*;
+
+/**
+ * Рантайм-патчи UI-скрипта Dynamic Island (свойства узлов по режимам) и структурная сигнатура.
+ */
+public final class IslandRuntimePatches {
+
+ private IslandRuntimePatches() {
+ }
+
+ public static long islandStructuralSignature(UiScriptProps props) {
+ long h = 0xcbf29ce484222325L;
+ h = CachedUiScriptRuntime.mix(h, stringProp(props, "mode", ""));
+ h = CachedUiScriptRuntime.mix(h, boolProp(props, "blur", false));
+ h = CachedUiScriptRuntime.mix(h, numberProp(props, "blurAlpha", 0.0f));
+ h = CachedUiScriptRuntime.mix(h, !stringProp(props, "artworkTexture", "").isEmpty());
+ h = CachedUiScriptRuntime.mix(h, "music".equals(stringProp(props, "mode", "")));
+ h = CachedUiScriptRuntime.mix(h, boolProp(props, "showShuffle", false));
+ h = CachedUiScriptRuntime.mix(h, boolProp(props, "showRepeat", false));
+ return h;
+ }
+
+ public static Map> islandRuntimePatches(UiScriptProps props) {
+ UiScriptPatchSet patches = UiScriptPatchSet.create(48);
+ float compactAlpha = numberProp(props, "compactAlpha", 0.0f);
+ float expandedAlpha = numberProp(props, "expandedAlpha", 0.0f);
+ float contextAlpha = numberProp(props, "contextAlpha", compactAlpha);
+ float width = numberProp(props, "width", 0.0f);
+ float mainWidth = numberProp(props, "mainWidth", width);
+ float progress = clamp01(numberProp(props, "progress", 0.0f));
+ String mode = stringProp(props, "mode", "");
+ boolean musicMode = "music".equals(mode);
+ boolean clickGuiMode = "clickgui".equals(mode);
+ boolean shellMode = musicMode || clickGuiMode;
+ boolean pvpMode = "pvp".equals(mode);
+ boolean timeMode = "time".equals(mode);
+ String primary = stringProp(props, "textPrimary", "#FFFFFFFF");
+ String secondary = stringProp(props, "textSecondary", "#99FFFFFF");
+ String muted = stringProp(props, "textMuted", "#66FFFFFF");
+ String accent = stringProp(props, "accent", "#FFFFFFFF");
+ String accentSoft = stringProp(props, "accentSoft", "#33FFFFFF");
+
+ if (shellMode) {
+ float shellBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * numberProp(props, "alpha", 0.0f);
+ patchShape(patches, "dynamic-island", "renderRadius", props.get("radius"), "renderBlurAlpha", shellBlurAlpha);
+ patchShape(patches, "shell:blur", "blurAlpha", shellBlurAlpha, "radius", props.get("radius"));
+ patchShape(patches, "shell:shadow", "fill", props.get("shadow"), "radius", props.get("radius"));
+ patchShape(patches, "shell:fill", "startColor", props.get("fillTop"), "endColor", props.get("fillBottom"), "radius", props.get("radius"));
+ patchShape(patches, "shell:tint", "startColor", props.get("tintTop"), "endColor", props.get("tintBottom"), "radius", props.get("radius"));
+ patchShape(patches, "shell:stroke", "stroke", props.get("stroke"), "radius", props.get("radius"));
+ }
+
+ if (timeMode) {
+ patchShape(patches, "time:box", "fill", props.get("timeFill"), "stroke", props.get("timeStroke"));
+ patchText(patches, "time", stringProp(props, "time", ""), primary);
+ }
+
+ if (pvpMode) {
+ patchShape(patches, "pvp:chip", "fill", props.get("pvpFill"), "stroke", props.get("pvpStroke"));
+ patchText(patches, "pvp:timer", stringProp(props, "pvpTimer", "0s"), primary);
+ }
+
+ if (musicMode || clickGuiMode) {
+ float contextBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * contextAlpha;
+ patchShape(patches, "context:time:blur", "blurAlpha", contextBlurAlpha, "radius", 10.0f);
+ patchShape(patches, "context:time:box", "fill", scaleHexAlpha(stringProp(props, "contextFill", "#00000000"), contextAlpha), "stroke", scaleHexAlpha(stringProp(props, "contextStroke", "#00000000"), contextAlpha));
+ patchText(patches, "context:time", stringProp(props, "time", ""), scaleHexAlpha(primary, contextAlpha));
+ float pvpContextAlpha = boolProp(props, "pvpContextVisible", false) ? contextAlpha : 0.0f;
+ patchShape(patches, "context:pvp:blur", "blurAlpha", numberProp(props, "blurAlpha", 0.0f) * pvpContextAlpha, "radius", 10.0f);
+ patchShape(patches, "context:pvp:box", "fill", scaleHexAlpha(stringProp(props, "pvpFill", "#00000000"), pvpContextAlpha), "stroke", scaleHexAlpha(stringProp(props, "pvpStroke", "#00000000"), pvpContextAlpha));
+ patchText(patches, "context:pvp", stringProp(props, "pvpTimer", ""), scaleHexAlpha(primary, pvpContextAlpha));
+ }
+
+ if (musicMode) {
+ patchText(patches, "music:elapsed:compact", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, compactAlpha));
+ patchText(patches, "music:title:compact", stringProp(props, "title", ""), scaleHexAlpha(primary, compactAlpha));
+ patchClippedText(patches, "music:title:compact", numberProp(props, "titleWidthCompact", 0.0f), Math.max(0.0f, mainWidth - 124.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 15.0f);
+ patchShape(patches, "music:divider", "fill", scaleHexAlpha(muted, 0.4f * compactAlpha));
+ patchShape(patches, "wave:0", "fill", scaleHexAlpha(accent, 0.75f * compactAlpha));
+ patchShape(patches, "wave:1", "fill", scaleHexAlpha(accent, 0.83f * compactAlpha));
+ patchShape(patches, "wave:2", "fill", scaleHexAlpha(accent, 0.91f * compactAlpha));
+ patchImage(patches, "artwork:compact", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", compactAlpha));
+ patchShape(patches, "artwork:compact:fallback", "fill", scaleHexAlpha(accent, 0.92f * compactAlpha), "stroke", scaleHexAlpha("#33FFFFFF", compactAlpha));
+ }
+
+ if (musicMode) {
+ patchText(patches, "music:title:expanded", stringProp(props, "title", ""), scaleHexAlpha(primary, expandedAlpha));
+ patchClippedText(patches, "music:title:expanded", numberProp(props, "titleWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 16.0f);
+ patchText(patches, "music:artist:expanded", stringProp(props, "artist", ""), scaleHexAlpha(secondary, expandedAlpha));
+ patchClippedText(patches, "music:artist:expanded", numberProp(props, "artistWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.4f, 14.0f, 16.0f);
+ patchShape(patches, "music:progress:bg", "fill", scaleHexAlpha(stringProp(props, "progressBg", "#00FFFFFF"), expandedAlpha));
+ patchShape(patches, "music:progress:fill",
+ "fill", scaleHexAlpha(accent, expandedAlpha),
+ "renderWidth", Math.max(0.0f, mainWidth - 26.0f) * progress);
+ patchText(patches, "music:elapsed:expanded", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, expandedAlpha));
+ patchText(patches, "music:total:expanded", stringProp(props, "total", "0:00"), scaleHexAlpha(secondary, expandedAlpha));
+ patchImage(patches, "artwork:expanded", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", expandedAlpha));
+ patchShape(patches, "artwork:expanded:fallback", "fill", scaleHexAlpha(accent, 0.92f * expandedAlpha), "stroke", scaleHexAlpha("#33FFFFFF", expandedAlpha));
+ if (boolProp(props, "showShuffle", false)) {
+ patchControl(patches, "music:shuffle", stringProp(props, "iconShuffle", ""), stringProp(props, "shuffleColor", primary), accentSoft, expandedAlpha, numberProp(props, "shuffleHover", 0.0f));
+ }
+ patchControl(patches, "music:prev", stringProp(props, "iconPrev", ""), primary, accentSoft, expandedAlpha, numberProp(props, "prevHover", 0.0f));
+ patchControl(patches, "music:play", stringProp(props, "iconPlay", ""), primary, accentSoft, expandedAlpha, numberProp(props, "playHover", 0.0f));
+ patchControl(patches, "music:next", stringProp(props, "iconNext", ""), primary, accentSoft, expandedAlpha, numberProp(props, "nextHover", 0.0f));
+ if (boolProp(props, "showRepeat", false)) {
+ patchSvgControl(patches, "music:repeat", stringProp(props, "repeatAsset", "repeat-off"), stringProp(props, "repeatColor", "#99000000"), accentSoft, expandedAlpha, numberProp(props, "repeatHover", 0.0f));
+ }
+ }
+
+ return patches.asMap();
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java
new file mode 100644
index 0000000..efca081
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java
@@ -0,0 +1,199 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland;
+
+import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.misc.ClickGui;
+import dev.loki.lovisual.mixins.accessors.BossHealthOverlayAccessor;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.render.engine.renderer.Renderer2D;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+import dev.loki.lovisual.util.media.MediaSessionService;
+import dev.loki.lovisual.util.media.RepeatMode;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.components.BossHealthOverlay;
+import net.minecraft.client.gui.components.LerpingBossEvent;
+import net.minecraft.client.gui.screens.Screen;
+
+import java.time.LocalTime;
+import java.util.Map;
+import java.util.UUID;
+
+/** Чистые утилиты Dynamic Island: форматирование, иконки, цвета медиа, замеры, экраны, fallback-рендер. */
+public final class IslandUiUtil {
+
+ public static final int BASE_SHADOW = 0xFF000000;
+ public static final int BASE_STROKE = 0xFFFFFFFF;
+
+ private IslandUiUtil() {
+ }
+
+ public static int shuffleColor(MediaSessionService.Snapshot snapshot) {
+ if (snapshot == null || !snapshot.supportsShuffle()) {
+ return 0x99000000;
+ }
+ return snapshot.isShuffleActive() ? 0xFFFFFFFF : 0x7AB7BEC8;
+ }
+
+ public static int repeatColor(MediaSessionService.Snapshot snapshot) {
+ if (snapshot == null || !snapshot.supportsRepeat()) {
+ return 0x99000000;
+ }
+ RepeatMode mode = snapshot.repeatMode();
+ return mode == RepeatMode.ALL || mode == RepeatMode.ONE ? 0xFFFFFFFF : 0x7AB7BEC8;
+ }
+
+ public static String repeatSvg(MediaSessionService.Snapshot snapshot) {
+ if (snapshot == null || !snapshot.supportsRepeat()) {
+ return "repeat-off";
+ }
+ return snapshot.repeatMode() == RepeatMode.ONE ? "repeat-1" : "repeat";
+ }
+
+ public static boolean clickGuiEnabled() {
+ ClickGui clickGui = Modules.get(ClickGui.class);
+ return clickGui != null && clickGui.isEnabled();
+ }
+
+ public static boolean isSettingsLikeScreen(Screen screen) {
+ if (screen == null) return false;
+ String name = screen.getClass().getName();
+ String simple = screen.getClass().getSimpleName();
+ if (name.contains(".gui.screen.option.") || name.contains(".gui.screen.pack.")) return true;
+ return simple.contains("Options")
+ || simple.contains("Option")
+ || simple.contains("Controls")
+ || simple.contains("Keybind")
+ || simple.contains("Video")
+ || simple.contains("Sound")
+ || simple.contains("Language")
+ || simple.contains("Accessibility")
+ || simple.contains("Skin")
+ || simple.contains("Pack")
+ || simple.contains("Datapack")
+ || simple.contains("Customize")
+ || simple.contains("Telemetry")
+ || simple.contains("Credits");
+ }
+
+ public static String formatClock(LocalTime time) {
+ return String.format("%02d:%02d", time.getHour(), time.getMinute());
+ }
+
+ public static String formatSecondsOnly(float seconds) {
+ return Math.max(0L, (long) Math.ceil(Math.max(0f, seconds))) + "s";
+ }
+
+ public static String formatShortTime(float seconds) {
+ return formatShortTime((long) Math.ceil(Math.max(0f, seconds)));
+ }
+
+ public static String formatShortTime(long seconds) {
+ long sec = Math.max(0L, seconds);
+ long m = sec / 60L;
+ long s = sec % 60L;
+ return String.format("%d:%02d", m, s);
+ }
+
+ public static String iconString(int codepoint) {
+ return new String(Character.toChars(codepoint));
+ }
+
+ public static String hex(int argb) {
+ return String.format("#%08X", argb);
+ }
+
+ public static float clamp(float value, float min, float max) {
+ return Math.max(min, Math.min(max, value));
+ }
+
+ public static boolean hit(float mx, float my, float x, float y, float w, float h) {
+ return mx >= x && mx <= x + w && my >= y && my <= y + h;
+ }
+
+ public static float measureWidth(TextRenderer renderer, String text, float scale) {
+ if (renderer == null || text == null || text.isEmpty()) return 0f;
+ renderer.begin(scale, true, false);
+ try {
+ return (float) renderer.getWidth(text, false);
+ } finally {
+ renderer.end();
+ }
+ }
+
+ public static float measureHeight(TextRenderer renderer, float scale) {
+ if (renderer == null) return 0f;
+ renderer.begin(scale, true, false);
+ try {
+ return (float) renderer.getHeight(false);
+ } finally {
+ renderer.end();
+ }
+ }
+
+ public static float reservedTimeWidth(TextRenderer renderer, String text, float scale) {
+ return measureWidth(renderer, reserveTimeTemplate(text), scale);
+ }
+
+ public static String reserveTimeTemplate(String text) {
+ if (text == null || text.isEmpty()) return "0:00";
+ StringBuilder out = new StringBuilder(text.length());
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ out.append(Character.isDigit(c) ? '8' : c);
+ }
+ return out.toString();
+ }
+
+ public static void drawFallback(Renderer2D renderer,
+ float drawX,
+ float drawY,
+ float drawWidth,
+ float drawHeight,
+ float radius,
+ float alpha,
+ int accent,
+ int fillTop) {
+ renderer.roundedRectShadow(drawX, drawY, drawWidth, drawHeight, radius, 8f, 14f,
+ HudRenderUtil.scaleAlpha(BASE_SHADOW, 0.28f * alpha));
+ renderer.roundedRect(drawX, drawY, drawWidth, drawHeight, radius, 1.1f,
+ HudRenderUtil.scaleAlpha(fillTop, 0.88f * alpha));
+ renderer.roundedRectStroke(drawX, drawY, drawWidth, drawHeight, radius, 1.1f, 1f,
+ HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(BASE_STROKE, accent, 0.18f), 0.18f * alpha));
+ }
+
+ public static float bossBarOffset(Minecraft mc, int logicalScreenH, float topY) {
+ if (mc == null || mc.gui == null || mc.getWindow() == null) return 0f;
+ BossHealthOverlay bossBarHud = mc.gui.hud.getBossOverlay();
+ if (bossBarHud == null) return 0f;
+ Map bars;
+ try {
+ bars = ((BossHealthOverlayAccessor) bossBarHud).getBars();
+ } catch (Throwable ignored) {
+ return 0f;
+ }
+ if (bars == null || bars.isEmpty()) return 0f;
+
+ int scaledH = Math.max(1, mc.getWindow().getGuiScaledHeight());
+ int y = 12;
+ int rows = 0;
+ for (LerpingBossEvent ignored : bars.values()) {
+ rows++;
+ y += 19;
+ if (y >= scaledH / 3) break;
+ }
+ if (rows <= 0) return 0f;
+
+ float bossBottomScaled = 17f + 19f * (rows - 1);
+ float hudScale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight());
+ float windowScale = (float) mc.getWindow().getGuiScale();
+ float bossBottomLogical = (bossBottomScaled + 8f) * windowScale / Math.max(0.001f, hudScale);
+ return Math.max(0f, Math.min(logicalScreenH * 0.30f, bossBottomLogical) - topY);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java
new file mode 100644
index 0000000..637fec8
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java
@@ -0,0 +1,123 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls;
+
+import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandUiUtil;
+import dev.loki.lovisual.render.engine.animation.AnimationUtility;
+import dev.loki.lovisual.render.engine.math.HudScale;
+import dev.loki.lovisual.render.engine.text.TextRenderer;
+import dev.loki.lovisual.util.media.MediaSessionService;
+import net.minecraft.client.Minecraft;
+
+/**
+ * Геометрия и hover-состояния ряда медиа-контролов Dynamic Island.
+ */
+public final class IslandControls {
+
+ public static final float EXPANDED_PAD_X = 13f;
+ public static final float CONTROL_SIZE = 27f;
+ public static final float CONTROL_GAP = 7f;
+ public static final float CONTROL_ROW_GAP_Y = -5f;
+
+ public float shuffleX;
+ public float shuffleY;
+ public float prevX;
+ public float prevY;
+ public float playX;
+ public float playY;
+ public float nextX;
+ public float nextY;
+ public float repeatX;
+ public float repeatY;
+ public float controlSize;
+ public float shuffleHover;
+ public float prevHover;
+ public float playHover;
+ public float nextHover;
+ public float repeatHover;
+
+ public void updateLayout(float drawX, float drawY, float width, TextRenderer valueRenderer,
+ MediaSessionService.Snapshot snapshot) {
+ float progressX = drawX + EXPANDED_PAD_X;
+ float progressW = width - EXPANDED_PAD_X * 2f;
+ float progressY = drawY + 52f;
+ float timeH = IslandUiUtil.measureHeight(valueRenderer, 0.80f);
+
+ controlSize = CONTROL_SIZE;
+ float controlGap = CONTROL_GAP;
+ boolean showShuffle = snapshot != null && snapshot.supportsShuffle();
+ boolean showRepeat = snapshot != null && snapshot.supportsRepeat();
+ int activeControls = 3 + (showShuffle ? 1 : 0) + (showRepeat ? 1 : 0);
+ float totalControlsW = controlSize * activeControls + controlGap * Math.max(0, activeControls - 1);
+ float controlsX = drawX + (width - totalControlsW) * 0.5f;
+ float controlsY = progressY + 5f + timeH + CONTROL_ROW_GAP_Y;
+ float cursorX = controlsX;
+
+ if (showShuffle) {
+ shuffleX = cursorX;
+ shuffleY = controlsY;
+ cursorX += controlSize + controlGap;
+ } else {
+ shuffleX = shuffleY = 0f;
+ }
+ prevX = cursorX;
+ prevY = controlsY;
+ cursorX += controlSize + controlGap;
+ playX = cursorX;
+ playY = controlsY;
+ cursorX += controlSize + controlGap;
+ nextX = cursorX;
+ nextY = controlsY;
+ cursorX += controlSize + controlGap;
+ if (showRepeat) {
+ repeatX = cursorX;
+ repeatY = controlsY;
+ } else {
+ repeatX = repeatY = 0f;
+ }
+ }
+
+ public void updateHover(boolean chatOpen, Minecraft mc, MediaSessionService.Snapshot snapshot) {
+ if (!chatOpen || controlSize <= 0f || mc == null || mc.getWindow() == null) {
+ shuffleHover = AnimationUtility.approach(shuffleHover, 0f, 0.25f);
+ prevHover = AnimationUtility.approach(prevHover, 0f, 0.25f);
+ playHover = AnimationUtility.approach(playHover, 0f, 0.25f);
+ nextHover = AnimationUtility.approach(nextHover, 0f, 0.25f);
+ repeatHover = AnimationUtility.approach(repeatHover, 0f, 0.25f);
+ return;
+ }
+
+ int fbw = mc.getWindow().getWidth();
+ int fbh = mc.getWindow().getHeight();
+ float uiScale = HudScale.scale(fbw, fbh);
+ float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale);
+ float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale);
+ boolean showShuffle = snapshot != null && snapshot.supportsShuffle();
+ boolean showRepeat = snapshot != null && snapshot.supportsRepeat();
+
+ shuffleHover = AnimationUtility.approach(shuffleHover, showShuffle && IslandUiUtil.hit(mx, my, shuffleX, shuffleY, controlSize, controlSize) ? 1f : 0f, 0.25f);
+ prevHover = AnimationUtility.approach(prevHover, IslandUiUtil.hit(mx, my, prevX, prevY, controlSize, controlSize) ? 1f : 0f, 0.25f);
+ playHover = AnimationUtility.approach(playHover, IslandUiUtil.hit(mx, my, playX, playY, controlSize, controlSize) ? 1f : 0f, 0.25f);
+ nextHover = AnimationUtility.approach(nextHover, IslandUiUtil.hit(mx, my, nextX, nextY, controlSize, controlSize) ? 1f : 0f, 0.25f);
+ repeatHover = AnimationUtility.approach(repeatHover, showRepeat && IslandUiUtil.hit(mx, my, repeatX, repeatY, controlSize, controlSize) ? 1f : 0f, 0.25f);
+ }
+
+ public void reset() {
+ shuffleX = shuffleY = 0f;
+ prevX = prevY = 0f;
+ playX = playY = 0f;
+ nextX = nextY = 0f;
+ repeatX = repeatY = 0f;
+ controlSize = 0f;
+ shuffleHover = 0f;
+ prevHover = 0f;
+ playHover = 0f;
+ nextHover = 0f;
+ repeatHover = 0f;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java
index 4cac46a..8c8d0fb 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java
@@ -20,22 +20,21 @@ import dev.loki.lovisual.config.common.CommonBooleanGroupSchema;
import dev.loki.lovisual.config.common.CommonSettingSchema;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.gui.clickgui.settings.*;
+import dev.loki.lovisual.features.module.module.ModuleBinds;
+import dev.loki.lovisual.features.module.module.ModuleConfigValues;
+import dev.loki.lovisual.features.module.module.ModuleNames;
+import dev.loki.lovisual.features.module.module.ModuleSettingValues;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.Lifecycle;
-import dev.loki.lovisual.util.input.KeyManager;
import dev.loki.lovisual.util.logging.DebugLog;
-import net.minecraft.client.resources.language.I18n;
-
import java.util.*;
import java.util.function.Supplier;
public abstract class Module implements ConfigObject, ConfigNameProvider, SettingOwner, ConfigValueAliasProvider {
- private static final String SETTING_SHOW_IN_MODULE_LIST = "show_in_module_list";
- private static final String LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES = "show_in_active_modules";
protected final List settings = new ArrayList<>();
private final String name;
private final String displayName;
@@ -45,7 +44,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
private final BooleanValue enabledValue;
private final EnumValue activationSourceValue =
new EnumValue<>("activation_source", ModuleActivationSource.NONE, ModuleActivationSource.class);
- private final BooleanValue moduleListVisibleValue = new BooleanValue(SETTING_SHOW_IN_MODULE_LIST, true);
+ private final BooleanValue moduleListVisibleValue = new BooleanValue(ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST, true);
private final List declaredSettingDefs = new ArrayList<>();
private final Map, SettingDef> declaredSettingDefsByValue = new IdentityHashMap<>();
private final KeyBindSetting keyBindSetting;
@@ -61,7 +60,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
this.name = info.id().toLowerCase(Locale.ROOT);
this.displayName = info.displayName();
- this.aliases = normalizeAliases(info.aliases());
+ this.aliases = ModuleNames.normalizeAliases(info.aliases());
this.description = info.description();
this.category = info.category();
this.enabledValue = new BooleanValue("enabled", info.enabledByDefault());
@@ -89,75 +88,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
private static KeyBindSetting createKeyBindSetting() {
- return new KeyBindSetting(new KeyBindValue("bind", "NONE"));
- }
-
- private static List normalizeAliases(String[] aliases) {
- if (aliases == null || aliases.length == 0) return List.of();
-
- List out = new ArrayList<>(aliases.length);
- for (String alias : aliases) {
- if (alias == null || alias.isBlank()) continue;
- out.add(alias.trim());
- }
- return List.copyOf(out);
- }
-
- private static String stripOwnerPrefix(String value, String owner) {
- if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value;
-
- String normalizedOwner = normalizePrefix(owner);
- String lowerValue = value.toLowerCase(java.util.Locale.ROOT);
- if (!lowerValue.startsWith(normalizedOwner)) return value;
-
- String tail = value.substring(normalizedOwner.length());
- if (tail.isEmpty()) return value;
-
- char first = tail.charAt(0);
- if (first == '_' || first == '-' || first == '.') {
- tail = tail.substring(1);
- } else if (!Character.isUpperCase(first)) {
- return value;
- }
-
- return camelToSnake(tail);
- }
-
- private static String normalizePrefix(String value) {
- StringBuilder out = new StringBuilder(value.length());
- for (int i = 0; i < value.length(); i++) {
- char c = Character.toLowerCase(value.charAt(i));
- if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
- out.append(c);
- }
- }
- return out.toString();
- }
-
- private static String camelToSnake(String value) {
- if (value == null || value.isEmpty()) return value;
-
- StringBuilder out = new StringBuilder(value.length() + 4);
- char previous = 0;
- for (int i = 0; i < value.length(); i++) {
- char c = value.charAt(i);
- if (c == '-' || c == '.' || c == ' ') {
- c = '_';
- }
- if (c == '_') {
- if (!out.isEmpty() && out.charAt(out.length() - 1) != '_') {
- out.append('_');
- }
- previous = '_';
- continue;
- }
- if (Character.isUpperCase(c) && i > 0 && previous != '_' && !Character.isUpperCase(previous)) {
- out.append('_');
- }
- out.append(Character.toLowerCase(c));
- previous = c;
- }
- return out.toString();
+ return ModuleBinds.createKeyBindSetting();
}
private static ModuleActivationSource normalizeActivationSource(boolean state, ModuleActivationSource source) {
@@ -192,9 +123,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final BooleanValue bool(String configName, String settingId, boolean def) {
- BooleanValue value = new BooleanValue(configName, def);
- declare(value, SettingDef.bool(settingId, value));
- return value;
+ return ModuleSettingValues.bool(configName, settingId, def, this::declare);
}
protected final NumberValue num(String configName, int def, int min, int max) {
@@ -202,9 +131,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final NumberValue num(String configName, String settingId, int def, int min, int max) {
- NumberValue value = new NumberValue<>(configName, def, min, max);
- declare(value, SettingDef.number(settingId, value));
- return value;
+ return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare);
}
protected final NumberValue num(String configName, long def, long min, long max) {
@@ -212,9 +139,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final NumberValue num(String configName, String settingId, long def, long min, long max) {
- NumberValue value = new NumberValue<>(configName, def, min, max);
- declare(value, SettingDef.number(settingId, value));
- return value;
+ return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare);
}
protected final NumberValue num(String configName, float def, float min, float max) {
@@ -222,9 +147,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final NumberValue num(String configName, String settingId, float def, float min, float max) {
- NumberValue value = new NumberValue<>(configName, def, min, max);
- declare(value, SettingDef.number(settingId, value));
- return value;
+ return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare);
}
protected final NumberValue num(String configName, double def, double min, double max) {
@@ -232,9 +155,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final NumberValue num(String configName, String settingId, double def, double min, double max) {
- NumberValue value = new NumberValue<>(configName, def, min, max);
- declare(value, SettingDef.number(settingId, value));
- return value;
+ return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare);
}
protected final ModeValue mode(String configName, String def, String... options) {
@@ -242,9 +163,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final ModeValue modeSetting(String configName, String settingId, String def, String... options) {
- ModeValue value = new ModeValue(configName, def, options);
- declare(value, SettingDef.mode(settingId, value));
- return value;
+ return ModuleSettingValues.modeSetting(configName, settingId, def, this::declare, options);
}
protected final > EnumValue enumMode(String configName, E def) {
@@ -252,10 +171,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final > EnumValue enumSetting(String configName, String settingId, E def) {
- Class enumClass = def.getDeclaringClass();
- EnumValue value = new EnumValue<>(configName, def, enumClass);
- declare(value, SettingDef.mode(settingId, value));
- return value;
+ return ModuleSettingValues.enumSetting(configName, settingId, def, this::declare);
}
@Deprecated(forRemoval = false)
@@ -265,9 +181,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
@Deprecated(forRemoval = false)
protected final > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass) {
- EnumValue value = new EnumValue<>(configName, def, enumClass);
- declare(value, SettingDef.mode(settingId, value));
- return value;
+ return ModuleSettingValues.enumSetting(configName, settingId, def, enumClass, this::declare);
}
@SafeVarargs
@@ -277,9 +191,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
@SafeVarargs
protected final > EnumValue enumSetting(String configName, String settingId, E def, E... options) {
- EnumValue value = new EnumValue<>(configName, def, options);
- declare(value, SettingDef.mode(settingId, value));
- return value;
+ return ModuleSettingValues.enumSetting(configName, settingId, def, this::declare, options);
}
protected final RGBAColorValue color(String configName, String defHex) {
@@ -287,9 +199,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final RGBAColorValue color(String configName, String settingId, String defHex) {
- RGBAColorValue value = new RGBAColorValue(configName, defHex);
- declare(value, SettingDef.color(settingId, value));
- return value;
+ return ModuleSettingValues.color(configName, settingId, defHex, this::declare);
}
protected final RGBColorValue colorNoAlpha(String configName, String defHex) {
@@ -297,9 +207,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final RGBColorValue colorNoAlpha(String configName, String settingId, String defHex) {
- RGBColorValue value = new RGBColorValue(configName, defHex);
- declare(value, SettingDef.colorNoAlpha(settingId, value));
- return value;
+ return ModuleSettingValues.colorNoAlpha(configName, settingId, defHex, this::declare);
}
protected final StringValue text(String configName, String def) {
@@ -307,9 +215,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final StringValue text(String configName, String settingId, String def) {
- StringValue value = new StringValue(configName, def);
- declare(value, SettingDef.text(settingId, value));
- return value;
+ return ModuleSettingValues.text(configName, settingId, def, this::declare);
}
protected final SetValue textList(String configName) {
@@ -321,15 +227,11 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode) {
- SetValue value = new SetValue(configName);
- declare(value, SettingDef.textList(settingId, value, pickerMode));
- return value;
+ return ModuleSettingValues.textList(configName, settingId, pickerMode, this::declare);
}
protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, Set defaults) {
- SetValue value = new SetValue(configName, defaults);
- declare(value, SettingDef.textList(settingId, value, pickerMode));
- return value;
+ return ModuleSettingValues.textList(configName, settingId, pickerMode, defaults, this::declare);
}
protected final ItemIdSetValue itemList(String configName) {
@@ -341,9 +243,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final ItemIdSetValue itemList(String configName, String settingId, TextListSetting.PickerMode pickerMode) {
- ItemIdSetValue value = new ItemIdSetValue(configName);
- declare(value, SettingDef.textList(settingId, value, pickerMode));
- return value;
+ return ModuleSettingValues.itemList(configName, settingId, pickerMode, this::declare);
}
protected final BooleanMapValue group(String configName, Map defaults) {
@@ -351,9 +251,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final BooleanMapValue group(String configName, String settingId, Map defaults) {
- BooleanMapValue value = new BooleanMapValue(configName, defaults);
- declare(value, SettingDef.group(settingId, value));
- return value;
+ return ModuleSettingValues.group(configName, settingId, defaults, this::declare);
}
protected final KeyBindValue bind(String configName, String def, BindMode mode) {
@@ -361,9 +259,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final KeyBindValue bind(String configName, String settingId, String def, BindMode mode) {
- KeyBindValue value = new KeyBindValue(configName, def);
- declare(value, SettingDef.bind(settingId, value, mode));
- return value;
+ return ModuleSettingValues.bind(configName, settingId, def, mode, this::declare);
}
protected final BooleanMapValue groupCommon(String configName,
@@ -520,12 +416,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
protected final BooleanMapValue protocolHeuristics(String configName,
String settingId,
CommonBooleanGroupSchema schema) {
- Map defaults = schema == null ? Map.of() : schema.defaults();
- BooleanMapValue value = new BooleanMapValue(configName, defaults);
- SettingDef def = SettingDef.protocolHeuristics(settingId, value);
- if (schema != null) def.common(schema);
- declare(value, def);
- return value;
+ return ModuleSettingValues.protocolHeuristics(configName, settingId, schema, this::declare);
}
protected final > EnumValue enumCommon(String configName,
@@ -744,17 +635,13 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
private String settingIdFromConfigName(String configName) {
- if (configName == null || configName.isBlank()) return configName;
-
- String tail = stripOwnerPrefix(configName, name());
- tail = stripOwnerPrefix(tail, getClass().getSimpleName());
- return tail;
+ return ModuleNames.settingId(configName, name(), getClass().getSimpleName());
}
@Override
public List getLegacyValueNames(String currentValueName) {
- if (SETTING_SHOW_IN_MODULE_LIST.equals(currentValueName)) {
- return List.of(LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES);
+ if (ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST.equals(currentValueName)) {
+ return List.of(ModuleConfigValues.LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES);
}
return List.of();
}
@@ -786,10 +673,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final FunctionBindSetting action(String name, String defaultKey, BindMode mode) {
- FunctionBindSetting setting = new FunctionBindSetting(name, defaultKey, mode);
- actions.put(name, setting);
- settings.add(setting);
- return setting;
+ return ModuleBinds.registerAction(actions, settings, name, defaultKey, mode);
}
protected final FunctionBindSetting action(String name, String defaultKey) {
@@ -805,22 +689,15 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
protected final boolean isActionPressedOnce(String name) {
- if (!isEnabled()) return false;
- FunctionBindSetting setting = actions.get(name);
- if (setting == null) return false;
- return KeyManager.wasPressed(setting.getBindingName());
+ return ModuleBinds.isPressedOnce(isEnabled(), actions.get(name));
}
protected final boolean isActionHeld(String name) {
- if (!isEnabled()) return false;
- FunctionBindSetting setting = actions.get(name);
- if (setting == null) return false;
- return KeyManager.isHeld(setting.getBindingName());
+ return ModuleBinds.isHeld(isEnabled(), actions.get(name));
}
protected final String getActionKey(String name) {
- FunctionBindSetting setting = actions.get(name);
- return setting == null ? "NONE" : setting.get();
+ return ModuleBinds.actionKey(actions.get(name));
}
public boolean isShownInModuleList() {
@@ -830,10 +707,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
public void setShownInModuleList(boolean value) {
if (moduleListVisibleValue.get() == value) return;
moduleListVisibleValue.set(value);
- try {
- ConfigSerializer.requestSave(this);
- } catch (Throwable ignored) {
- }
+ ModuleConfigValues.requestSaveQuietly(this);
}
public void toggleShownInModuleList() {
@@ -872,10 +746,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
public final void setActivationSource(ModuleActivationSource source) {
activationSource = normalizeActivationSource(enabled, source);
activationSourceValue.set(activationSource);
- try {
- ConfigSerializer.requestSave(this);
- } catch (Throwable ignored) {
- }
+ ModuleConfigValues.requestSaveQuietly(this);
}
public boolean isEnabledFromKeybind() {
@@ -896,7 +767,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
public String getDisplayName() {
- return translateFirst(displayName, "module." + name(), "module." + name() + ".name");
+ return ModuleNames.translateFirst(displayName, "module." + name(), "module." + name() + ".name");
}
public List getAliases() {
@@ -904,24 +775,13 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
public String getDescription() {
- return translateFirst(description, "module." + name() + ".description");
+ return ModuleNames.translateFirst(description, "module." + name() + ".description");
}
public ModuleCategory getCategory() {
return category;
}
- private static String translateFirst(String fallback, String... keys) {
- for (String key : keys) {
- if (key == null || key.isBlank()) continue;
- String translated = I18n.get(key);
- if (!translated.equals(key)) {
- return translated;
- }
- }
- return fallback == null ? "" : fallback;
- }
-
public final void toggle() {
setEnabled(!enabled, ModuleActivationSource.MANUAL);
}
@@ -944,10 +804,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
activationSource = normalizedSource;
enabledValue.set(enabled);
activationSourceValue.set(activationSource);
- try {
- ConfigSerializer.requestSave(this);
- } catch (Throwable ignored) {
- }
+ ModuleConfigValues.requestSaveQuietly(this);
return;
}
@@ -975,10 +832,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
// Persist change
enabledValue.set(enabled);
activationSourceValue.set(activationSource);
- try {
- ConfigSerializer.requestSave(this);
- } catch (Throwable ignored) {
- }
+ ModuleConfigValues.requestSaveQuietly(this);
}
public final void setEnabledFromConfig(boolean state) {
@@ -1084,64 +938,21 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
@Override
public List> getConfigValues() {
- Map> map = new LinkedHashMap<>();
-
- map.put(enabledValue.getName(), enabledValue);
- map.put(activationSourceValue.getName(), activationSourceValue);
- map.put(moduleListVisibleValue.getName(), moduleListVisibleValue);
-
- for (Setting s : settings) {
- ConfigValue> v = s.getConfigValue();
- if (v != null) {
- map.put(v.getName(), v);
- }
- }
-
- return new ArrayList<>(map.values());
+ return ModuleConfigValues.orderedValues(enabledValue, activationSourceValue, moduleListVisibleValue, settings);
}
/**
* Legacy support: apply values from the old "settings" map saved by previous versions.
*/
public void applyLegacySettings(Object json) {
- if (json == null) return;
-
- if (json instanceof de.marhali.json5.Json5Object obj) {
- for (var e : obj.asMap().entrySet()) {
- applySetting(e.getKey(), e.getValue());
- }
- } else if (json instanceof Map, ?> map) {
- for (var e : map.entrySet()) {
- if (e.getKey() instanceof String k) {
- applySetting(k, e.getValue());
- }
- }
- }
- }
-
- private void applySetting(String id, Object data) {
- if (SETTING_SHOW_IN_MODULE_LIST.equals(id) || LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES.equals(id)) {
- moduleListVisibleValue.fromJson(data);
- return;
- }
- for (Setting s : settings) {
- if (s.getId().equals(id)) {
- s.load(data);
- break;
- }
- }
+ ModuleConfigValues.applyLegacy(json, moduleListVisibleValue, settings);
}
/**
* Re-applies values to settings to trigger any side effects in Setting.load().
*/
public void syncSettingsAfterLoad() {
- for (Setting s : settings) {
- ConfigValue> v = s.getConfigValue();
- if (v != null) {
- s.load(v.toJson());
- }
- }
+ ModuleConfigValues.syncAfterLoad(settings);
}
public void applyConfiguredEnabledState() {
@@ -1173,9 +984,6 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
}
public void saveConfig() {
- try {
- ConfigSerializer.requestSave(this);
- } catch (Throwable ignored) {
- }
+ ModuleConfigValues.requestSaveQuietly(this);
}
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java
new file mode 100644
index 0000000..dcf9790
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java
@@ -0,0 +1,57 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.module;
+
+import dev.loki.lovisual.config.values.BindMode;
+import dev.loki.lovisual.config.values.KeyBindValue;
+import dev.loki.lovisual.features.gui.clickgui.settings.FunctionBindSetting;
+import dev.loki.lovisual.features.gui.clickgui.settings.KeyBindSetting;
+import dev.loki.lovisual.features.gui.clickgui.settings.Setting;
+import dev.loki.lovisual.util.input.KeyManager;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Keybind and action helpers for modules: the shared key bind setting creation, action
+ * registration into the module's settings and action query helpers.
+ */
+public final class ModuleBinds {
+
+ private ModuleBinds() {
+ }
+
+ public static KeyBindSetting createKeyBindSetting() {
+ return new KeyBindSetting(new KeyBindValue("bind", "NONE"));
+ }
+
+ public static FunctionBindSetting registerAction(Map actions,
+ List settings,
+ String name,
+ String defaultKey,
+ BindMode mode) {
+ FunctionBindSetting setting = new FunctionBindSetting(name, defaultKey, mode);
+ actions.put(name, setting);
+ settings.add(setting);
+ return setting;
+ }
+
+ public static boolean isPressedOnce(boolean enabled, FunctionBindSetting setting) {
+ if (!enabled || setting == null) return false;
+ return KeyManager.wasPressed(setting.getBindingName());
+ }
+
+ public static boolean isHeld(boolean enabled, FunctionBindSetting setting) {
+ if (!enabled || setting == null) return false;
+ return KeyManager.isHeld(setting.getBindingName());
+ }
+
+ public static String actionKey(FunctionBindSetting setting) {
+ return setting == null ? "NONE" : setting.get();
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java
new file mode 100644
index 0000000..9736c3f
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java
@@ -0,0 +1,106 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.module;
+
+import de.marhali.json5.Json5Object;
+import dev.loki.lovisual.config.ConfigObject;
+import dev.loki.lovisual.config.ConfigSerializer;
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.config.values.ConfigValue;
+import dev.loki.lovisual.config.values.EnumValue;
+import dev.loki.lovisual.features.gui.clickgui.settings.Setting;
+import dev.loki.lovisual.features.module.ModuleActivationSource;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Config plumbing for modules: the ordered config value collection, legacy settings
+ * application, post-load value sync and the quiet save request used across Module.
+ */
+public final class ModuleConfigValues {
+
+ public static final String SETTING_SHOW_IN_MODULE_LIST = "show_in_module_list";
+ public static final String LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES = "show_in_active_modules";
+
+ private ModuleConfigValues() {
+ }
+
+ public static List> orderedValues(BooleanValue enabledValue,
+ EnumValue activationSourceValue,
+ BooleanValue moduleListVisibleValue,
+ List settings) {
+ Map> map = new LinkedHashMap<>();
+
+ map.put(enabledValue.getName(), enabledValue);
+ map.put(activationSourceValue.getName(), activationSourceValue);
+ map.put(moduleListVisibleValue.getName(), moduleListVisibleValue);
+
+ for (Setting s : settings) {
+ ConfigValue> v = s.getConfigValue();
+ if (v != null) {
+ map.put(v.getName(), v);
+ }
+ }
+
+ return new ArrayList<>(map.values());
+ }
+
+ /**
+ * Legacy support: apply values from the old "settings" map saved by previous versions.
+ */
+ public static void applyLegacy(Object json, BooleanValue moduleListVisibleValue, List settings) {
+ if (json == null) return;
+
+ if (json instanceof Json5Object obj) {
+ for (var e : obj.asMap().entrySet()) {
+ applySetting(e.getKey(), e.getValue(), moduleListVisibleValue, settings);
+ }
+ } else if (json instanceof Map, ?> map) {
+ for (var e : map.entrySet()) {
+ if (e.getKey() instanceof String k) {
+ applySetting(k, e.getValue(), moduleListVisibleValue, settings);
+ }
+ }
+ }
+ }
+
+ private static void applySetting(String id, Object data, BooleanValue moduleListVisibleValue, List settings) {
+ if (SETTING_SHOW_IN_MODULE_LIST.equals(id) || LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES.equals(id)) {
+ moduleListVisibleValue.fromJson(data);
+ return;
+ }
+ for (Setting s : settings) {
+ if (s.getId().equals(id)) {
+ s.load(data);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Re-applies values to settings to trigger any side effects in Setting.load().
+ */
+ public static void syncAfterLoad(List settings) {
+ for (Setting s : settings) {
+ ConfigValue> v = s.getConfigValue();
+ if (v != null) {
+ s.load(v.toJson());
+ }
+ }
+ }
+
+ public static void requestSaveQuietly(ConfigObject module) {
+ try {
+ ConfigSerializer.requestSave(module);
+ } catch (Throwable ignored) {
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java
new file mode 100644
index 0000000..acf0de9
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java
@@ -0,0 +1,111 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.module;
+
+import net.minecraft.client.resources.language.I18n;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Name and identity helpers for modules: alias normalization, setting id derivation
+ * from config names and the i18n-first display/description translation lookup.
+ */
+public final class ModuleNames {
+
+ private ModuleNames() {
+ }
+
+ public static List normalizeAliases(String[] aliases) {
+ if (aliases == null || aliases.length == 0) return List.of();
+
+ List out = new ArrayList<>(aliases.length);
+ for (String alias : aliases) {
+ if (alias == null || alias.isBlank()) continue;
+ out.add(alias.trim());
+ }
+ return List.copyOf(out);
+ }
+
+ public static String stripOwnerPrefix(String value, String owner) {
+ if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value;
+
+ String normalizedOwner = normalizePrefix(owner);
+ String lowerValue = value.toLowerCase(Locale.ROOT);
+ if (!lowerValue.startsWith(normalizedOwner)) return value;
+
+ String tail = value.substring(normalizedOwner.length());
+ if (tail.isEmpty()) return value;
+
+ char first = tail.charAt(0);
+ if (first == '_' || first == '-' || first == '.') {
+ tail = tail.substring(1);
+ } else if (!Character.isUpperCase(first)) {
+ return value;
+ }
+
+ return camelToSnake(tail);
+ }
+
+ private static String normalizePrefix(String value) {
+ StringBuilder out = new StringBuilder(value.length());
+ for (int i = 0; i < value.length(); i++) {
+ char c = Character.toLowerCase(value.charAt(i));
+ if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ private static String camelToSnake(String value) {
+ if (value == null || value.isEmpty()) return value;
+
+ StringBuilder out = new StringBuilder(value.length() + 4);
+ char previous = 0;
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '-' || c == '.' || c == ' ') {
+ c = '_';
+ }
+ if (c == '_') {
+ if (!out.isEmpty() && out.charAt(out.length() - 1) != '_') {
+ out.append('_');
+ }
+ previous = '_';
+ continue;
+ }
+ if (Character.isUpperCase(c) && i > 0 && previous != '_' && !Character.isUpperCase(previous)) {
+ out.append('_');
+ }
+ out.append(Character.toLowerCase(c));
+ previous = c;
+ }
+ return out.toString();
+ }
+
+ public static String settingId(String configName, String ownerId, String ownerSimpleName) {
+ if (configName == null || configName.isBlank()) return configName;
+
+ String tail = stripOwnerPrefix(configName, ownerId);
+ tail = stripOwnerPrefix(tail, ownerSimpleName);
+ return tail;
+ }
+
+ public static String translateFirst(String fallback, String... keys) {
+ for (String key : keys) {
+ if (key == null || key.isBlank()) continue;
+ String translated = I18n.get(key);
+ if (!translated.equals(key)) {
+ return translated;
+ }
+ }
+ return fallback == null ? "" : fallback;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java
new file mode 100644
index 0000000..51091bf
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java
@@ -0,0 +1,156 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.module;
+
+import dev.loki.lovisual.config.SettingDef;
+import dev.loki.lovisual.config.common.CommonBooleanGroupSchema;
+import dev.loki.lovisual.config.values.BindMode;
+import dev.loki.lovisual.config.values.BooleanMapValue;
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.config.values.ConfigValue;
+import dev.loki.lovisual.config.values.EnumValue;
+import dev.loki.lovisual.config.values.ItemIdSetValue;
+import dev.loki.lovisual.config.values.KeyBindValue;
+import dev.loki.lovisual.config.values.ModeValue;
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.config.values.RGBAColorValue;
+import dev.loki.lovisual.config.values.RGBColorValue;
+import dev.loki.lovisual.config.values.SetValue;
+import dev.loki.lovisual.config.values.StringValue;
+import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/**
+ * Factory helpers for module setting values. Every factory creates the config value,
+ * builds its {@link SettingDef} and hands both to the caller's declare callback so the
+ * owning module can register them exactly as before.
+ */
+public final class ModuleSettingValues {
+
+ private ModuleSettingValues() {
+ }
+
+ public static BooleanValue bool(String configName, String settingId, boolean def, BiConsumer, SettingDef> declare) {
+ BooleanValue value = new BooleanValue(configName, def);
+ declare.accept(value, SettingDef.bool(settingId, value));
+ return value;
+ }
+
+ public static NumberValue num(String configName, String settingId, int def, int min, int max, BiConsumer, SettingDef> declare) {
+ NumberValue value = new NumberValue<>(configName, def, min, max);
+ declare.accept(value, SettingDef.number(settingId, value));
+ return value;
+ }
+
+ public static NumberValue num(String configName, String settingId, long def, long min, long max, BiConsumer, SettingDef> declare) {
+ NumberValue value = new NumberValue<>(configName, def, min, max);
+ declare.accept(value, SettingDef.number(settingId, value));
+ return value;
+ }
+
+ public static NumberValue num(String configName, String settingId, float def, float min, float max, BiConsumer, SettingDef> declare) {
+ NumberValue value = new NumberValue<>(configName, def, min, max);
+ declare.accept(value, SettingDef.number(settingId, value));
+ return value;
+ }
+
+ public static NumberValue num(String configName, String settingId, double def, double min, double max, BiConsumer, SettingDef> declare) {
+ NumberValue value = new NumberValue<>(configName, def, min, max);
+ declare.accept(value, SettingDef.number(settingId, value));
+ return value;
+ }
+
+ public static ModeValue modeSetting(String configName, String settingId, String def, BiConsumer, SettingDef> declare, String... options) {
+ ModeValue value = new ModeValue(configName, def, options);
+ declare.accept(value, SettingDef.mode(settingId, value));
+ return value;
+ }
+
+ public static > EnumValue enumSetting(String configName, String settingId, E def, BiConsumer, SettingDef> declare) {
+ Class enumClass = def.getDeclaringClass();
+ EnumValue value = new EnumValue<>(configName, def, enumClass);
+ declare.accept(value, SettingDef.mode(settingId, value));
+ return value;
+ }
+
+ public static > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass, BiConsumer, SettingDef> declare) {
+ EnumValue value = new EnumValue<>(configName, def, enumClass);
+ declare.accept(value, SettingDef.mode(settingId, value));
+ return value;
+ }
+
+ @SafeVarargs
+ public static > EnumValue enumSetting(String configName, String settingId, E def, BiConsumer, SettingDef> declare, E... options) {
+ EnumValue value = new EnumValue<>(configName, def, options);
+ declare.accept(value, SettingDef.mode(settingId, value));
+ return value;
+ }
+
+ public static RGBAColorValue color(String configName, String settingId, String defHex, BiConsumer, SettingDef> declare) {
+ RGBAColorValue value = new RGBAColorValue(configName, defHex);
+ declare.accept(value, SettingDef.color(settingId, value));
+ return value;
+ }
+
+ public static RGBColorValue colorNoAlpha(String configName, String settingId, String defHex, BiConsumer, SettingDef> declare) {
+ RGBColorValue value = new RGBColorValue(configName, defHex);
+ declare.accept(value, SettingDef.colorNoAlpha(settingId, value));
+ return value;
+ }
+
+ public static StringValue text(String configName, String settingId, String def, BiConsumer, SettingDef> declare) {
+ StringValue value = new StringValue(configName, def);
+ declare.accept(value, SettingDef.text(settingId, value));
+ return value;
+ }
+
+ public static SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, BiConsumer, SettingDef> declare) {
+ SetValue value = new SetValue(configName);
+ declare.accept(value, SettingDef.textList(settingId, value, pickerMode));
+ return value;
+ }
+
+ public static SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, Set defaults, BiConsumer, SettingDef> declare) {
+ SetValue value = new SetValue(configName, defaults);
+ declare.accept(value, SettingDef.textList(settingId, value, pickerMode));
+ return value;
+ }
+
+ public static ItemIdSetValue itemList(String configName, String settingId, TextListSetting.PickerMode pickerMode, BiConsumer, SettingDef> declare) {
+ ItemIdSetValue value = new ItemIdSetValue(configName);
+ declare.accept(value, SettingDef.textList(settingId, value, pickerMode));
+ return value;
+ }
+
+ public static BooleanMapValue group(String configName, String settingId, Map defaults, BiConsumer, SettingDef> declare) {
+ BooleanMapValue value = new BooleanMapValue(configName, defaults);
+ declare.accept(value, SettingDef.group(settingId, value));
+ return value;
+ }
+
+ public static KeyBindValue bind(String configName, String settingId, String def, BindMode mode, BiConsumer, SettingDef> declare) {
+ KeyBindValue value = new KeyBindValue(configName, def);
+ declare.accept(value, SettingDef.bind(settingId, value, mode));
+ return value;
+ }
+
+ public static BooleanMapValue protocolHeuristics(String configName,
+ String settingId,
+ CommonBooleanGroupSchema schema,
+ BiConsumer, SettingDef> declare) {
+ Map defaults = schema == null ? Map.of() : schema.defaults();
+ BooleanMapValue value = new BooleanMapValue(configName, defaults);
+ SettingDef def = SettingDef.protocolHeuristics(settingId, value);
+ if (schema != null) def.common(schema);
+ declare.accept(value, def);
+ return value;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java
new file mode 100644
index 0000000..1caea00
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java
@@ -0,0 +1,53 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat;
+
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.features.module.Notifier;
+import dev.loki.lovisual.features.relations.CategoryService;
+import dev.loki.lovisual.util.protection.NotifyThrottle;
+import net.minecraft.util.Util;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.Player;
+
+/**
+ * Cancels attacks against players marked as friends (including the
+ * Bedwars self team) and shows a throttled warning instead.
+ */
+@ModuleInfo(
+ id = "nofrienddamage",
+ displayName = "No Friend Damage",
+ aliases = {"nofrienddamage", "nfd"},
+ category = ModuleCategory.COMBAT,
+ description = "module.nofrienddamage.description"
+)
+public final class NoFriendDamage extends Module {
+
+ private static final long WARN_INTERVAL_MS = 2000L;
+
+ private final NotifyThrottle warnThrottle = new NotifyThrottle(WARN_INTERVAL_MS);
+
+ /**
+ * Called from the client attack hook (MultiPlayerGameMode.attack).
+ *
+ * @return true when the attack must be cancelled
+ */
+ public boolean shouldBlockAttack(Entity target) {
+ if (!isEnabled() || !(target instanceof Player targetPlayer)) return false;
+ if (!CategoryService.isFriend(targetPlayer)) return false;
+ notifyBlocked(targetPlayer);
+ return true;
+ }
+
+ private void notifyBlocked(Player targetPlayer) {
+ if (!warnThrottle.tryAcquire(Util.getMillis())) return;
+ Notifier.warning("Blocked attack on friend: " + targetPlayer.getGameProfile().name());
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java
index 02d964d..1afa626 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java
@@ -16,38 +16,34 @@ import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
-import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.Entity;
-import net.minecraft.world.entity.ExperienceOrb;
import net.minecraft.world.entity.LivingEntity;
-import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.item.PrimedTnt;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
-import net.minecraft.world.entity.projectile.Projectile;
-import net.minecraft.world.entity.projectile.ProjectileUtil;
+import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball;
+import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge;
import net.minecraft.world.entity.projectile.arrow.AbstractArrow;
import net.minecraft.world.entity.projectile.arrow.ThrownTrident;
-import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball;
-import net.minecraft.world.entity.projectile.hurtingprojectile.SmallFireball;
-import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge;
-import net.minecraft.world.entity.projectile.throwableitemprojectile.*;
-import net.minecraft.world.item.BowItem;
-import net.minecraft.world.item.CrossbowItem;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.AbstractThrownPotion;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
-import net.minecraft.world.level.ClipContext;
import net.minecraft.world.phys.AABB;
-import net.minecraft.world.phys.EntityHitResult;
-import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import org.joml.Quaternionf;
-import org.joml.Vector3f;
import dev.loki.lovisual.config.values.BooleanMapValue;
import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.config.values.RGBColorValue;
-import dev.loki.lovisual.features.module.*;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsAim;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TimerPlate;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult;
import dev.loki.lovisual.mixins.accessors.PersistentProjectileEntityAccessor;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.TextureStorage;
@@ -68,6 +64,15 @@ import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addBillboardQuad;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addGradientLine;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addImpactDecal;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addImpactRing;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.multiplyAlpha;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.withAlpha;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_ENTITY_COLOR;
+import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_SELF_COLOR;
+
//todo Description
@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT)
public class Predictions extends Module {
@@ -87,8 +92,6 @@ public class Predictions extends Module {
private static final String SETTING_TNT_COLOR = "tnt_color";
private static final String SETTING_PROJECTILES = "projectiles";
- private static final int HIT_SELF_COLOR = 0xFFFF4444;
- private static final int HIT_ENTITY_COLOR = 0xFFFFCC44;
private static final int GLOW_SUBSTEPS = 10;
private static final float GLOW_CORE_SIZE = 0.085f;
private static final float GLOW_SOFT_SIZE = 0.16f;
@@ -125,20 +128,10 @@ public class Predictions extends Module {
private static final float TRAJECTORY_LINE_WIDTH = 1.35f;
private static final float TRAJECTORY_LINE_ALPHA_START = 0.16f;
private static final float TRAJECTORY_LINE_ALPHA_END = 0.78f;
- private static final int IMPACT_RING_SEGMENTS = 64;
+ private static final float IMPACT_DECAL_RADIUS = 0.34f;
private static final float IMPACT_RING_RADIUS = 0.30f;
- private static final float IMPACT_RING_CROSS_RADIUS = 0.18f;
private static final float IMPACT_RING_LINE_WIDTH = 1.2f;
private static final float IMPACT_RING_ALPHA = 0.88f;
- private static final float IMPACT_DECAL_RADIUS = 0.34f;
- private static final float IMPACT_DECAL_EDGE_SOFTNESS = 0.085f;
- private static final float IMPACT_DECAL_STROKE_WIDTH = 0.10f;
- private static final float IMPACT_DECAL_PATTERN_SCALE = 4.25f;
- private static final float IMPACT_DECAL_FILL_ALPHA = 0.48f;
- private static final float IMPACT_DECAL_STROKE_ALPHA = 0.92f;
- private static final float IMPACT_DECAL_PATTERN_STRENGTH = 0.10f;
- private static final float IMPACT_DECAL_QUAD_PADDING = 1.18f;
- private static final double IMPACT_DECAL_OFFSET = 0.004;
private final Minecraft mc = Minecraft.getInstance();
@@ -177,301 +170,6 @@ public class Predictions extends Module {
private ItemStack tntTimerIcon = ItemStack.EMPTY;
private final List timerPlates = new ArrayList<>();
- private static boolean canHitPredictedEntity(Entity projectile, Entity entity, AABB collisionBox) {
- if (projectile == null || entity == null || !entity.isAlive() || !entity.canBeHitByProjectile()) {
- return false;
- }
-
- if (projectile instanceof Projectile projectileEntity) {
- Entity owner = projectileEntity.getOwner();
- if (owner != null && !hasPredictedLeftOwner(owner, entity, collisionBox)) {
- return !owner.isPassengerOfSameVehicle(entity);
- }
- }
-
- return true;
- }
-
- private static boolean hasPredictedLeftOwner(Entity owner, Entity entity, AABB collisionBox) {
- if (owner == null || collisionBox == null) {
- return true;
- }
- return owner.getRootVehicle()
- .getSelfAndPassengers()
- .filter(Entity::isPickable)
- .noneMatch(passenger -> collisionBox.intersects(passenger.getBoundingBox()));
- }
-
- private static void addGradientLine(MeshBuilder mesh, Vec3 from, Vec3 to, int startColor, int endColor) {
- if (mesh == null || from == null || to == null) return;
- mesh.ensureLineCapacity();
- int i1 = mesh.vec3(from.x, from.y, from.z).color(new RenderColor(startColor)).next();
- int i2 = mesh.vec3(to.x, to.y, to.z).color(new RenderColor(endColor)).next();
- mesh.line(i1, i2);
- }
-
- private static void addImpactRing(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) {
- if (mesh == null || center == null) return;
-
- Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6
- ? new Vec3(0.0, 1.0, 0.0)
- : normal.normalize();
- Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0);
- Vec3 tangent = useNormal.cross(axis);
- if (tangent.lengthSqr() < 1.0E-6) {
- tangent = new Vec3(1.0, 0.0, 0.0);
- } else {
- tangent = tangent.normalize();
- }
- Vec3 bitangent = useNormal.cross(tangent).normalize();
-
- for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) {
- double angle0 = (Math.PI * 2.0 * i) / IMPACT_RING_SEGMENTS;
- double angle1 = (Math.PI * 2.0 * (i + 1)) / IMPACT_RING_SEGMENTS;
- Vec3 p0 = center
- .add(tangent.scale(Math.cos(angle0) * radius))
- .add(bitangent.scale(Math.sin(angle0) * radius));
- Vec3 p1 = center
- .add(tangent.scale(Math.cos(angle1) * radius))
- .add(bitangent.scale(Math.sin(angle1) * radius));
- addGradientLine(mesh, p0, p1, argb, argb);
- }
-
- Vec3 crossTangent = tangent.scale(IMPACT_RING_CROSS_RADIUS);
- Vec3 crossBitangent = bitangent.scale(IMPACT_RING_CROSS_RADIUS);
- addGradientLine(mesh, center.subtract(crossTangent), center.add(crossTangent), argb, argb);
- addGradientLine(mesh, center.subtract(crossBitangent), center.add(crossBitangent), argb, argb);
- }
-
- private static void addImpactDecal(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) {
- if (mesh == null || center == null || radius <= 0.0f) {
- return;
- }
-
- Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6
- ? new Vec3(0.0, 1.0, 0.0)
- : normal.normalize();
- Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0);
- Vec3 tangent = useNormal.cross(axis);
- if (tangent.lengthSqr() < 1.0E-6) {
- tangent = new Vec3(1.0, 0.0, 0.0);
- } else {
- tangent = tangent.normalize();
- }
- Vec3 bitangent = useNormal.cross(tangent).normalize();
- Vec3 offsetCenter = center.add(useNormal.scale(IMPACT_DECAL_OFFSET));
- float quadRadius = radius * IMPACT_DECAL_QUAD_PADDING;
- float sdfRadius = 1.0f / IMPACT_DECAL_QUAD_PADDING;
- Vec3 tangentScaled = tangent.scale(quadRadius);
- Vec3 bitangentScaled = bitangent.scale(quadRadius);
-
- mesh.ensureQuadCapacity();
- int i1 = mesh.vec3(offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).x,
- offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).y,
- offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).z)
- .vec2(0.0, 0.0)
- .color(new RenderColor(argb))
- .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
- .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
- .next();
- int i2 = mesh.vec3(offsetCenter.add(tangentScaled).subtract(bitangentScaled).x,
- offsetCenter.add(tangentScaled).subtract(bitangentScaled).y,
- offsetCenter.add(tangentScaled).subtract(bitangentScaled).z)
- .vec2(1.0, 0.0)
- .color(new RenderColor(argb))
- .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
- .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
- .next();
- int i3 = mesh.vec3(offsetCenter.add(tangentScaled).add(bitangentScaled).x,
- offsetCenter.add(tangentScaled).add(bitangentScaled).y,
- offsetCenter.add(tangentScaled).add(bitangentScaled).z)
- .vec2(1.0, 1.0)
- .color(new RenderColor(argb))
- .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
- .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
- .next();
- int i4 = mesh.vec3(offsetCenter.subtract(tangentScaled).add(bitangentScaled).x,
- offsetCenter.subtract(tangentScaled).add(bitangentScaled).y,
- offsetCenter.subtract(tangentScaled).add(bitangentScaled).z)
- .vec2(0.0, 1.0)
- .color(new RenderColor(argb))
- .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
- .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
- .next();
- mesh.quad(i1, i2, i3, i4);
- }
-
- private static Vec3 resolveImpactNormal(HitResult hit, Vec3 velocity) {
- if (hit instanceof net.minecraft.world.phys.BlockHitResult blockHit) {
- return Vec3.atLowerCornerOf(blockHit.getDirection().getUnitVec3i());
- }
- if (velocity != null && velocity.lengthSqr() > 1.0E-6) {
- return velocity.normalize();
- }
- return new Vec3(0.0, 1.0, 0.0);
- }
-
- private static double partialStepProgress(Vec3 from, Vec3 hitPos, double segmentLengthSq) {
- if (from == null || hitPos == null || segmentLengthSq <= 1.0E-6) {
- return 1.0;
- }
- return Mth.clamp(from.distanceTo(hitPos) / Math.sqrt(segmentLengthSq), 0.0, 1.0);
- }
-
- private static Vec3 resolveEntityImpactNormal(AABB box, Vec3 hitPos, Vec3 fallbackVelocity) {
- if (box == null || hitPos == null) {
- return resolveImpactNormal(null, fallbackVelocity);
- }
-
- double dxMin = Math.abs(hitPos.x - box.minX);
- double dxMax = Math.abs(box.maxX - hitPos.x);
- double dyMin = Math.abs(hitPos.y - box.minY);
- double dyMax = Math.abs(box.maxY - hitPos.y);
- double dzMin = Math.abs(hitPos.z - box.minZ);
- double dzMax = Math.abs(box.maxZ - hitPos.z);
-
- double best = dxMin;
- Vec3 normal = new Vec3(-1.0, 0.0, 0.0);
- if (dxMax < best) {
- best = dxMax;
- normal = new Vec3(1.0, 0.0, 0.0);
- }
- if (dyMin < best) {
- best = dyMin;
- normal = new Vec3(0.0, -1.0, 0.0);
- }
- if (dyMax < best) {
- best = dyMax;
- normal = new Vec3(0.0, 1.0, 0.0);
- }
- if (dzMin < best) {
- best = dzMin;
- normal = new Vec3(0.0, 0.0, -1.0);
- }
- if (dzMax < best) {
- normal = new Vec3(0.0, 0.0, 1.0);
- }
- return normal;
- }
-
- private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz,
- float size, Quaternionf camRot, int argb) {
- Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot).mul(size);
- Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot).mul(size);
-
- double p1x = cx - right.x() - up.x();
- double p1y = cy - right.y() - up.y();
- double p1z = cz - right.z() - up.z();
- double p2x = cx + right.x() - up.x();
- double p2y = cy + right.y() - up.y();
- double p2z = cz + right.z() - up.z();
- double p3x = cx + right.x() + up.x();
- double p3y = cy + right.y() + up.y();
- double p3z = cz + right.z() + up.z();
- double p4x = cx - right.x() + up.x();
- double p4y = cy - right.y() + up.y();
- double p4z = cz - right.z() + up.z();
-
- mesh.ensureQuadCapacity();
- int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0.0, 1.0).color(new RenderColor(argb)).next();
- int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(argb)).next();
- int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(argb)).next();
- int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(argb)).next();
- mesh.quad(i1, i2, i3, i4);
- }
-
- private static void addFilledBox(Renderer3D renderer, AABB box, int argb) {
- int a = (argb >>> 24) & 0xFF;
- int r = (argb >>> 16) & 0xFF;
- int g = (argb >>> 8) & 0xFF;
- int b = argb & 0xFF;
-
- double minX = box.minX;
- double minY = box.minY;
- double minZ = box.minZ;
- double maxX = box.maxX;
- double maxY = box.maxY;
- double maxZ = box.maxZ;
-
- renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
- renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a);
- renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
- renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a);
- renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a);
- renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
- }
-
- private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) {
- int a = (argb >>> 24) & 0xFF;
- int r = (argb >>> 16) & 0xFF;
- int g = (argb >>> 8) & 0xFF;
- int b = argb & 0xFF;
-
- double minX = box.minX;
- double minY = box.minY;
- double minZ = box.minZ;
- double maxX = box.maxX;
- double maxY = box.maxY;
- double maxZ = box.maxZ;
-
- renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a);
- renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a);
- renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
- renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a);
-
- renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a);
- renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a);
- renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
- renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
-
- renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a);
- renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a);
- renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a);
- renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a);
- }
-
- private static int multiplyAlpha(int argb, float alphaMultiplier) {
- int baseAlpha = (argb >>> 24) & 0xFF;
- int alpha = Math.max(0, Math.min(255, Math.round(baseAlpha * Math.max(0.0f, alphaMultiplier))));
- return (argb & 0x00FFFFFF) | (alpha << 24);
- }
-
- private static int withAlpha(int argb, int alpha) {
- return (argb & 0x00FFFFFF) | (Math.max(0, Math.min(255, alpha)) << 24);
- }
-
- private static Vec3 advanceTickVelocity(Entity entity, Vec3 velocity) {
- return velocity.scale(0.99).add(0.0, -entity.getGravity(), 0.0);
- }
-
- private static Kind kindFromParams(ItemStack stack) {
- if (stack == null) return Kind.ARROW;
- if (stack.is(Items.TRIDENT)) return Kind.TRIDENT;
- if (stack.is(Items.SPLASH_POTION)) return Kind.POTION_SPLASH;
- if (stack.is(Items.LINGERING_POTION)) return Kind.POTION_LINGERING;
- if (stack.is(Items.SNOWBALL)) return Kind.SNOWBALL;
- if (stack.is(Items.EGG)) return Kind.EGG;
- if (stack.is(Items.FIREWORK_ROCKET)) return Kind.FIREWORK;
- if (stack.is(Items.WIND_CHARGE)) return Kind.WIND_CHARGE;
- if (stack.is(Items.FIRE_CHARGE)) return Kind.FIREBALL;
- if (stack.is(Items.ENDER_PEARL)) return Kind.PEARL_ENTITY;
- return Kind.ARROW;
- }
-
- private static Kind kindFromProjectile(Entity entity) {
- if (entity instanceof ThrownTrident) return Kind.TRIDENT;
- if (entity instanceof AbstractThrownPotion potion) {
- return potion.getItem().is(Items.LINGERING_POTION) ? Kind.POTION_LINGERING : Kind.POTION_SPLASH;
- }
- if (entity instanceof Snowball) return Kind.SNOWBALL;
- if (entity instanceof ThrownEgg) return Kind.EGG;
- if (entity instanceof FireworkRocketEntity) return Kind.FIREWORK;
- if (entity instanceof WindCharge) return Kind.WIND_CHARGE;
- if (entity instanceof Fireball) return Kind.FIREBALL;
- if (entity instanceof ThrownEnderpearl) return Kind.PEARL_ENTITY;
- return Kind.ARROW;
- }
-
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
@@ -524,8 +222,8 @@ public class Predictions extends Module {
List results = showPlate ? simulateProjectiles(tickDelta) : Collections.emptyList();
if (showPlate && !results.isEmpty()) {
for (TrajectoryResult res : results) {
- if (res.points.isEmpty()) continue;
- Vec3 end = res.points.getLast();
+ if (res.points().isEmpty()) continue;
+ Vec3 end = res.points().getLast();
Vec3 screen = ScreenProjection.worldToScreen(end, tickDelta);
if (screen == null) continue;
drawTimerPlate(renderer, textRenderer, res, (float) screen.x, (float) screen.y);
@@ -567,6 +265,16 @@ public class Predictions extends Module {
}
}
+ private TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta) {
+ return PredictionsSimulation.simulateEntity(e, kind, tickDelta,
+ maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue);
+ }
+
+ private AimPrediction computeAimPrediction(Player player, float tickDelta) {
+ return PredictionsAim.computeAimPrediction(player, tickDelta,
+ maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue);
+ }
+
private List simulateProjectiles(float tickDelta) {
ClientLevel world = mc.level;
if (world == null) return Collections.emptyList();
@@ -664,268 +372,6 @@ public class Predictions extends Module {
return out;
}
- private TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta) {
- ClientLevel world = mc.level;
- if (world == null) return null;
-
- Vec3 pos = e.position();
- Vec3 vel = e.getDeltaMovement();
-
- List points = new ArrayList<>();
- points.add(pos);
-
- double t = 0.0;
- int baseMaxTicks = Math.max(1, maxSimTicks.get());
- int maxTicks = kind == Kind.PEARL_ENTITY
- ? Math.max(1, pearlMaxSimTicks.get())
- : baseMaxTicks;
- int hitColor = 0;
- long impactTick = world.getGameTime();
- Vec3 impactPos = null;
- Vec3 impactNormal = null;
- Entity hitEntity = null;
- boolean hitBlock = false;
-
- for (int i = 0; i < maxTicks; i++) {
- Vec3 segmentStart = pos;
- Vec3 simulatedVelocity = advanceTickVelocity(e, vel);
- Vec3 simulatedPos = pos.add(simulatedVelocity);
- double segmentLengthSq = segmentStart.distanceToSqr(simulatedPos);
-
- HitResult hit = world.clip(new ClipContext(
- segmentStart,
- simulatedPos,
- ClipContext.Block.COLLIDER,
- ClipContext.Fluid.NONE,
- e
- ));
-
- EntityCollision entityCollision = getEntityCollision(e, segmentStart, simulatedPos);
- boolean hasBlockHit = hit.getType() != HitResult.Type.MISS;
- boolean useEntityHit = entityCollision != null
- && (!hasBlockHit || segmentStart.distanceToSqr(entityCollision.hitPos()) <= segmentStart.distanceToSqr(hit.getLocation()));
-
- if (useEntityHit) {
- hitColor = (entityCollision.entity() == mc.player) ? HIT_SELF_COLOR : HIT_ENTITY_COLOR;
- Vec3 entityHitPos = entityCollision.hitPos();
- t += partialStepProgress(segmentStart, entityHitPos, segmentLengthSq);
- points.add(entityHitPos);
- impactTick = world.getGameTime() + (long) Math.ceil(t);
- impactPos = entityHitPos;
- impactNormal = entityCollision.hitNormal();
- hitEntity = entityCollision.entity();
- break;
- } else if (hasBlockHit) {
- Vec3 blockHitPos = hit.getLocation();
- t += partialStepProgress(segmentStart, blockHitPos, segmentLengthSq);
- points.add(blockHitPos);
- impactTick = world.getGameTime() + (long) Math.ceil(t);
- impactPos = blockHitPos;
- impactNormal = resolveImpactNormal(hit, simulatedVelocity);
- hitBlock = true;
- break;
- }
-
- pos = simulatedPos;
- vel = simulatedVelocity;
- t += 1.0;
- points.add(pos);
- impactTick = world.getGameTime() + (long) Math.ceil(t);
- }
-
- ItemStack icon = switch (kind) {
- case PEARL_ENTITY -> new ItemStack(Items.ENDER_PEARL);
- case TRIDENT -> new ItemStack(Items.TRIDENT);
- case FIREBALL -> new ItemStack(Items.FIRE_CHARGE);
- case WIND_CHARGE -> new ItemStack(Items.WIND_CHARGE);
- case FIREWORK -> new ItemStack(Items.FIREWORK_ROCKET);
- case SNOWBALL -> new ItemStack(Items.SNOWBALL);
- case EGG -> new ItemStack(Items.EGG);
- case POTION_SPLASH -> new ItemStack(Items.SPLASH_POTION);
- case POTION_LINGERING -> new ItemStack(Items.LINGERING_POTION);
- default -> new ItemStack(Items.ARROW);
- };
-
- int color = hitColor;
- if (color == 0 && kind == Kind.PEARL_ENTITY && pearlColorToggle.get()) {
- color = pearlColorValue.getArgb();
- }
-
- return new TrajectoryResult(points, t, impactTick, icon, color, kind, impactPos, impactNormal, hitEntity, hitBlock);
- }
-
- private EntityCollision getEntityCollision(Entity movingEntity, Vec3 startPos, Vec3 endPos) {
- Vec3 direction = endPos.subtract(startPos);
- if (direction.lengthSqr() == 0.0) {
- return null;
- }
-
- AABB projectileBox = movingEntity.getBoundingBox().move(startPos.subtract(movingEntity.position()));
- AABB collisionBox = projectileBox.expandTowards(direction).inflate(0.5);
- EntityHitResult hitResult = ProjectileUtil.getEntityHitResult(
- movingEntity,
- startPos,
- endPos,
- collisionBox,
- entity -> canHitPredictedEntity(movingEntity, entity, collisionBox)
- && entity instanceof LivingEntity
- && !(entity instanceof ItemEntity)
- && !(entity instanceof ExperienceOrb)
- && entity != movingEntity,
- direction.lengthSqr()
- );
- if (hitResult == null) {
- return null;
- }
-
- return new EntityCollision(
- hitResult.getEntity(),
- hitResult.getLocation(),
- resolveEntityImpactNormal(hitResult.getEntity().getBoundingBox(), hitResult.getLocation(), direction)
- );
- }
-
- private AimPrediction computeAimPrediction(Player player, float tickDelta) {
- Projectile projectile = createHeldProjectile(player, tickDelta);
- if (projectile == null) {
- return null;
- }
-
- Kind kind = kindFromProjectile(projectile);
- TrajectoryResult result = simulateEntity(projectile, kind, tickDelta);
- if (result == null || result.points().isEmpty()) {
- return null;
- }
-
- Vec3 hitPos = result.impactPos() != null ? result.impactPos() : result.points().getLast();
- Entity hitEntity = result.hitEntity();
- Vec3 anchorPos;
- if (hitEntity instanceof LivingEntity living) {
- anchorPos = living.getPosition(tickDelta).add(0.0, living.getBbHeight() * 0.5, 0.0);
- } else if (hitEntity != null) {
- anchorPos = hitEntity.getBoundingBox().getCenter();
- } else {
- anchorPos = hitPos;
- }
-
- return new AimPrediction(hitEntity, hitPos, anchorPos, result.impactNormal(), result.iconStack(), result);
- }
-
- private Projectile createHeldProjectile(Player player, float tickDelta) {
- if (mc.level == null) {
- return null;
- }
-
- InteractionHand hand = null;
- ItemStack handStack = ItemStack.EMPTY;
-
- ItemStack mainHand = player.getItemInHand(InteractionHand.MAIN_HAND);
- if (isAimUsable(player, mainHand, InteractionHand.MAIN_HAND)) {
- hand = InteractionHand.MAIN_HAND;
- handStack = mainHand;
- } else {
- ItemStack offHand = player.getItemInHand(InteractionHand.OFF_HAND);
- if (isAimUsable(player, offHand, InteractionHand.OFF_HAND)) {
- hand = InteractionHand.OFF_HAND;
- handStack = offHand;
- }
- }
-
- if (hand == null || handStack.isEmpty()) {
- return null;
- }
-
- Projectile projectile = null;
- float speed = 0.0f;
- float roll = 0.0f;
-
- if (handStack.is(Items.ENDER_PEARL)) {
- projectile = new ThrownEnderpearl(mc.level, player, handStack);
- speed = 1.5f;
- } else if (handStack.is(Items.SNOWBALL)) {
- projectile = new Snowball(mc.level, player, handStack);
- speed = 1.5f;
- } else if (handStack.is(Items.EGG)) {
- projectile = new ThrownEgg(mc.level, player, handStack);
- speed = 1.5f;
- } else if (handStack.is(Items.SPLASH_POTION) || handStack.is(Items.LINGERING_POTION)) {
- projectile = handStack.is(Items.LINGERING_POTION)
- ? new ThrownLingeringPotion(mc.level, player, handStack)
- : new ThrownSplashPotion(mc.level, player, handStack);
- speed = 0.5f;
- roll = -20.0f;
- } else if (handStack.is(Items.EXPERIENCE_BOTTLE)) {
- projectile = new ThrownExperienceBottle(mc.level, player, handStack);
- speed = 0.7f;
- roll = -20.0f;
- } else if (handStack.is(Items.WIND_CHARGE)) {
- projectile = new WindCharge(player, mc.level, player.position().x, player.getEyePosition().y, player.position().z);
- speed = 1.5f;
- } else if (handStack.is(Items.FIRE_CHARGE)) {
- projectile = new SmallFireball(mc.level, player, Vec3.ZERO);
- speed = 1.0f;
- } else if (handStack.is(Items.TRIDENT) && player.isUsingItem() && player.getUsedItemHand() == hand) {
- projectile = new ThrownTrident(mc.level, player, handStack);
- speed = 2.5f;
- } else if (handStack.is(Items.BOW) && player.isUsingItem() && player.getUsedItemHand() == hand) {
- float pull = BowItem.getPowerForTime(player.getTicksUsingItem());
- if (pull <= 0.0f) {
- return null;
- }
- ItemStack arrowStack = new ItemStack(Items.ARROW);
- projectile = new net.minecraft.world.entity.projectile.arrow.Arrow(mc.level, player, arrowStack, handStack);
- speed = 3.0f * pull;
- } else if (handStack.getItem() instanceof CrossbowItem && CrossbowItem.isCharged(handStack)) {
- ItemStack arrowStack = new ItemStack(Items.ARROW);
- projectile = new net.minecraft.world.entity.projectile.arrow.Arrow(mc.level, player, arrowStack, handStack);
- speed = 3.15f;
- }
-
- if (projectile == null) {
- return null;
- }
-
- Vec3 eye = player.getEyePosition(tickDelta);
- projectile.absSnapTo(eye.x, eye.y, eye.z);
- setHeldProjectileVelocity(projectile, player, player.getXRot(), player.getYRot(), roll, speed);
- return projectile;
- }
-
- private void setHeldProjectileVelocity(Projectile projectile, Entity shooter, float pitch, float yaw, float roll, float speed) {
- float x = -Mth.sin(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f));
- float y = -Mth.sin((pitch + roll) * ((float) Math.PI / 180.0f));
- float z = Mth.cos(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f));
-
- Vec3 velocity = new Vec3(x, y, z).normalize().scale(speed);
- projectile.setDeltaMovement(velocity);
-
- Vec3 movement = shooter.getKnownMovement();
- projectile.setDeltaMovement(projectile.getDeltaMovement().add(movement.x, shooter.onGround() ? 0.0 : movement.y, movement.z));
- }
-
- private boolean isAimUsable(Player player, ItemStack stack, InteractionHand hand) {
- if (stack == null || stack.isEmpty()) {
- return false;
- }
-
- if (stack.is(Items.BOW) || stack.is(Items.TRIDENT)) {
- return player.isUsingItem() && player.getUsedItemHand() == hand && player.getUseItem() == stack;
- }
-
- if (stack.is(Items.CROSSBOW)) {
- return net.minecraft.world.item.CrossbowItem.isCharged(stack);
- }
-
- return stack.is(Items.ENDER_PEARL)
- || stack.is(Items.SNOWBALL)
- || stack.is(Items.EGG)
- || stack.is(Items.SPLASH_POTION)
- || stack.is(Items.LINGERING_POTION)
- || stack.is(Items.EXPERIENCE_BOTTLE)
- || stack.is(Items.FIRE_CHARGE)
- || stack.is(Items.WIND_CHARGE);
- }
-
private boolean isKindDisabled(String key) {
// Ender pearls are always shown; caller must skip check for them.
return !kindToggles.get(key);
@@ -941,12 +387,12 @@ public class Predictions extends Module {
}
private void drawTimerPlate(Renderer2D renderer, TextRenderer textRenderer, TrajectoryResult res, float sx, float sy) {
- double seconds = res.flightTicks / 20.0;
+ double seconds = res.flightTicks() / 20.0;
if (seconds <= 0.0) return;
- ItemStack iconStack = useItemIcon(res.kind) ? resolveTimerItem(res.kind) : ItemStack.EMPTY;
- Identifier iconId = iconStack.isEmpty() ? resolveTimerIcon(res.kind) : null;
- drawTimerPlate(renderer, textRenderer, formatTime(seconds), sx, sy, res.argb, iconStack, iconId);
+ ItemStack iconStack = useItemIcon(res.kind()) ? resolveTimerItem(res.kind()) : ItemStack.EMPTY;
+ Identifier iconId = iconStack.isEmpty() ? resolveTimerIcon(res.kind()) : null;
+ drawTimerPlate(renderer, textRenderer, formatTime(seconds), sx, sy, res.argb(), iconStack, iconId);
}
private void drawTntTimers(Renderer2D renderer, TextRenderer textRenderer, float tickDelta) {
@@ -1300,64 +746,53 @@ public class Predictions extends Module {
}
}
- private enum Kind {
- PEARL_ENTITY,
- ARROW,
- TRIDENT,
- FIREBALL,
- WIND_CHARGE,
- FIREWORK,
- SNOWBALL,
- EGG,
- POTION_SPLASH,
- POTION_LINGERING
+ private static void addFilledBox(Renderer3D renderer, AABB box, int argb) {
+ int a = (argb >>> 24) & 0xFF;
+ int r = (argb >>> 16) & 0xFF;
+ int g = (argb >>> 8) & 0xFF;
+ int b = argb & 0xFF;
+
+ double minX = box.minX;
+ double minY = box.minY;
+ double minZ = box.minZ;
+ double maxX = box.maxX;
+ double maxY = box.maxY;
+ double maxZ = box.maxZ;
+
+ renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
+ renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a);
+ renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
+ renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a);
+ renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a);
+ renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
}
- private record AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack,
- TrajectoryResult trajectory) {
- private AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack,
- TrajectoryResult trajectory) {
- this.entity = entity;
- this.hitPos = hitPos;
- this.anchorPos = anchorPos;
- this.hitNormal = hitNormal;
- this.displayStack = displayStack == null ? ItemStack.EMPTY : displayStack;
- this.trajectory = trajectory;
- }
- }
+ private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) {
+ int a = (argb >>> 24) & 0xFF;
+ int r = (argb >>> 16) & 0xFF;
+ int g = (argb >>> 8) & 0xFF;
+ int b = argb & 0xFF;
- private record TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack,
- int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity,
- boolean hitBlock) {
- private TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack,
- int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity, boolean hitBlock) {
- this.points = points;
- this.flightTicks = flightTicks;
- this.impactTick = impactTick;
- this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack;
- this.argb = argb;
- this.kind = kind;
- this.impactPos = impactPos;
- this.impactNormal = impactNormal;
- this.hitEntity = hitEntity;
- this.hitBlock = hitBlock;
- }
- }
+ double minX = box.minX;
+ double minY = box.minY;
+ double minZ = box.minZ;
+ double maxX = box.maxX;
+ double maxY = box.maxY;
+ double maxZ = box.maxZ;
- private record EntityCollision(Entity entity, Vec3 hitPos, Vec3 hitNormal) {
- }
+ renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a);
+ renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a);
+ renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
+ renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a);
- private record TimerPlate(float textX, float textY, String text,
- float iconX, float iconY, float iconScale, ItemStack iconStack) {
- private TimerPlate(float textX, float textY, String text,
- float iconX, float iconY, float iconScale, ItemStack iconStack) {
- this.textX = textX;
- this.textY = textY;
- this.text = text;
- this.iconX = iconX;
- this.iconY = iconY;
- this.iconScale = iconScale;
- this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack;
- }
+ renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a);
+ renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a);
+ renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
+ renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
+
+ renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a);
+ renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a);
+ renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a);
+ renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a);
}
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java
index 0ea3fb4..7202813 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java
@@ -7,9 +7,6 @@
package dev.loki.lovisual.features.module.modules.combat;
-import dev.loki.lovisual.util.time.GameClock;
-
-import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
@@ -17,8 +14,6 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
-import org.joml.Quaternionf;
-import org.joml.Vector3f;
import dev.loki.lovisual.config.common.CommonSettingSchemas;
import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.ModeValue;
@@ -28,13 +23,16 @@ import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
import dev.loki.lovisual.features.module.WorldPhase;
+import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals;
+import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects;
+import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes;
+import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals.CrystalInstance;
import dev.loki.lovisual.features.relations.CategoryService;
import dev.loki.lovisual.mixininterface.IEntity;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.TextureStorage;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
-import dev.loki.lovisual.render.engine.math.RenderMath;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
@@ -44,6 +42,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.IntFunction;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderBlackhole;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderGhosts;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.drawOldTargetEsp;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.renderCaptureMark;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.renderVortex;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.setCaptureSpeedScale;
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.tickTargetEsp;
+
//todo Description
@ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.COMBAT)
public class TargetESP extends Module {
@@ -69,20 +75,9 @@ public class TargetESP extends Module {
private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed";
private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style";
private static final String SETTING_VORTEX_SPEED = "vortex_speed";
- private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY;
- private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE;
private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM;
- private static final float CRYSTAL_BASE_SIZE = 0.05f;
- private static final int CRYSTAL_SIDES = 8;
private static final float ALPHA_TICK_DT = 1.0f / 20.0f;
private static final float ALPHA_SNAP_EPSILON = 0.02f;
- private static float prevCircleStep;
- private static float circleStep;
- private static double captureValue = 1.0;
- private static double prevCaptureValue;
- private static double captureSpeed = 1.0;
- private static double captureSpeedScale = 1.0;
- private static boolean captureFlipSpeed;
private final Minecraft mc = Minecraft.getInstance();
private final ModeValue espMode = modeSetting(
"targetEspMode",
@@ -140,602 +135,6 @@ public class TargetESP extends Module {
private Entity lastCrystalTarget;
private float crystalRotationAngle;
- private static void tickTargetEsp() {
- prevCircleStep = circleStep;
- circleStep += 0.15f;
-
- prevCaptureValue = captureValue;
- captureValue += captureSpeed * captureSpeedScale;
- if (captureSpeed > 25.0) captureFlipSpeed = true;
- if (captureSpeed < -25.0) captureFlipSpeed = false;
- captureSpeed = captureFlipSpeed
- ? captureSpeed - 0.5 * captureSpeedScale
- : captureSpeed + 0.5 * captureSpeedScale;
- }
-
- private static void setCaptureSpeedScale(float scale) {
- if (scale <= 0f) {
- captureSpeedScale = 0.05;
- } else {
- captureSpeedScale = scale;
- }
- }
-
- private static void drawOldTargetEsp(Renderer3D renderer, Entity target, float tickDelta, IntFunction palette) {
- if (renderer == null || target == null) return;
- double cs = prevCircleStep + (circleStep - prevCircleStep) * tickDelta;
- double prevSinAnim = absSinAnimation(cs - 0.45f);
- double sinAnim = absSinAnimation(cs);
- Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
- double x = pos.x;
- double y = pos.y + prevSinAnim * target.getBbHeight();
- double z = pos.z;
- double nextY = pos.y + sinAnim * target.getBbHeight();
-
- MeshBuilder mesh = renderer.tris();
-
- final int segments = 96;
- final double step = Math.PI * 2.0 / segments;
- final double radius = target.getBbWidth() * 0.8;
-
- for (int i = 0; i < segments; i++) {
- double a0 = i * step;
- double a1 = (i + 1) * step;
- float cos = (float) (x + Math.cos(a0) * radius);
- float sin = (float) (z + Math.sin(a0) * radius);
- float cosNext = (float) (x + Math.cos(a1) * radius);
- float sinNext = (float) (z + Math.sin(a1) * radius);
-
- int idx0 = (int) Math.round(i * (360.0 / segments));
- int idx1 = (int) Math.round((i + 1) * (360.0 / segments));
- int cTop0 = applyOpacity(palette.apply(idx0), 170.0f / 255.0f);
- int cTop1 = applyOpacity(palette.apply(idx1), 170.0f / 255.0f);
- int cBottom0 = applyOpacity(palette.apply(idx0), 0.0f);
- int cBottom1 = applyOpacity(palette.apply(idx1), 0.0f);
-
- mesh.ensureQuadCapacity();
- int i1 = addVertex(mesh, cos, nextY, sin, cTop0);
- int i2 = addVertex(mesh, cosNext, nextY, sinNext, cTop1);
- int i3 = addVertex(mesh, cosNext, y, sinNext, cBottom1);
- int i4 = addVertex(mesh, cos, y, sin, cBottom0);
- mesh.quad(i1, i2, i3, i4);
- }
-
- }
-
- private static void renderGhosts(Renderer3D renderer,
- int espLength, int factor, float shaking, float amplitude,
- Entity target, float tickDelta, IntFunction palette) {
- Minecraft mc = Minecraft.getInstance();
- if (renderer == null || mc == null || mc.player == null) return;
-
- Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
- double tPosX = pos.x;
- double tPosY = pos.y;
- double tPosZ = pos.z;
- float iAge = (float) interpolate(target.tickCount - 1, target.tickCount, tickDelta);
-
- MeshBuilder mesh = renderer.batchTextured(
- LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
- GHOST_PARTICLE_TEXTURE,
- Renderer3D.DepthMode.MAIN
- );
- if (mesh == null) return;
-
- for (int j = 0; j < 3; j++) {
- for (int i = 0; i <= espLength; i++) {
- double radians = Math.toRadians((((float) i / 1.5f + iAge) * factor + (j * 120)) % (factor * 360));
- double sinQuad = Math.sin(Math.toRadians(iAge * 2.5f + i * (j + 1)) * amplitude) / shaking;
-
- float offset = ((float) i / espLength);
- int color = applyOpacity(palette.apply((int) (180 * offset)), offset);
- float scale = Math.max(0.24f * (offset), 0.2f);
-
- double cx = tPosX + Math.cos(radians) * target.getBbWidth();
- double cy = tPosY + 1 + sinQuad;
- double cz = tPosZ + Math.sin(radians) * target.getBbWidth();
-
- addBillboardQuad(mesh, cx, cy, cz, scale, color);
- }
- }
- }
-
- private static final int VORTEX_HELIX_STEPS = 28;
- private static final int VORTEX_RING_SEGMENTS = 24;
-
- private static void renderVortex(Renderer3D renderer,
- LivingEntity target,
- float tickDelta,
- IntFunction palette,
- float spinSpeed) {
- if (renderer == null || target == null) return;
-
- Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
- float radius = target.getBbWidth() * 0.7f;
- float height = target.getBbHeight();
- double time = (GameClock.millis() / 1000.0) * spinSpeed;
-
- float prevWidth = RenderState.lineWidth;
- RenderState.lineWidth = 1.4f;
- MeshBuilder mesh;
- try {
- mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
- } finally {
- RenderState.lineWidth = prevWidth;
- }
- if (mesh == null) return;
-
- for (int strand = 0; strand < 2; strand++) {
- double strandOffset = strand * Math.PI;
- int prevIndex = -1;
- for (int i = 0; i <= VORTEX_HELIX_STEPS; i++) {
- double t = (double) i / VORTEX_HELIX_STEPS;
- double angle = time * 2.4 + t * Math.PI * 3.0 + strandOffset;
- double r = radius * (0.85 + 0.15 * Math.sin(t * Math.PI));
- double x = pos.x + Math.cos(angle) * r;
- double y = pos.y + t * height;
- double z = pos.z + Math.sin(angle) * r;
- int color = applyOpacity(palette.apply((int) (t * 180)), (float) (1.0 - t * 0.35));
-
- int index = addVertex(mesh, x, y, z, color);
- if (prevIndex >= 0) {
- mesh.line(prevIndex, index);
- }
- prevIndex = index;
- }
- }
-
- float[] ringHeights = {0.05f, height * 0.5f, height * 0.95f};
- float[] ringSpeeds = {0.4f, -0.55f, 0.35f};
- for (int ring = 0; ring < ringHeights.length; ring++) {
- double ringY = pos.y + ringHeights[ring];
- double ringRotation = time * ringSpeeds[ring];
- int ringColor = applyOpacity(palette.apply(60), 0.4f);
- int prevIndex = -1;
- int firstIndex = -1;
- for (int i = 0; i <= VORTEX_RING_SEGMENTS; i++) {
- double angle = ringRotation + (i % VORTEX_RING_SEGMENTS) * 2.0 * Math.PI / VORTEX_RING_SEGMENTS;
- double x = pos.x + Math.cos(angle) * radius;
- double z = pos.z + Math.sin(angle) * radius;
- int index = addVertex(mesh, x, ringY, z, ringColor);
- if (firstIndex < 0) {
- firstIndex = index;
- }
- if (prevIndex >= 0) {
- mesh.line(prevIndex, index);
- }
- prevIndex = index;
- }
- }
- }
-
- private static void renderCaptureMark(Renderer3D renderer,
- Entity target,
- float tickDelta,
- IntFunction palette) {
- if (renderer == null) return;
- Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
- double tPosX = pos.x;
- double tPosY = pos.y;
- double tPosZ = pos.z;
-
- MeshBuilder mesh = renderer.batchTextured(
- LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
- CAPTURE_MARK_TEXTURE,
- Renderer3D.DepthMode.MAIN
- );
- if (mesh == null) return;
-
- float angle = interpolateFloat((float) prevCaptureValue, (float) captureValue, tickDelta);
- float size = 0.75f;
-
- double cy = tPosY + target.getEyeHeight(target.getPose()) / 2f;
-
- addRotatedBillboard(mesh, tPosX, cy, tPosZ, size, angle, palette);
- }
-
- private static final float[][] BODY_HOLES = {
- {0.0f, 0.60f}, // голова
- {-0.48f, 0.36f}, // левая рука
- {0.48f, 0.36f}, // правая рука
- {0.0f, 0.05f}, // грудь
- {-0.30f, -0.42f},// левая нога
- {0.30f, -0.42f} // правая нога
- };
-
- private static void renderBlackhole(Renderer3D renderer,
- LivingEntity target,
- float tickDelta,
- IntFunction palette,
- float size,
- float spinSpeed,
- float alpha,
- String style) {
- if (renderer == null || target == null) return;
- Minecraft mc = Minecraft.getInstance();
- if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return;
- Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
- Vec3 baseCenter = new Vec3(pos.x, pos.y + target.getBbHeight() * 0.5, pos.z);
-
- // Камера-зависимый billboard-базис: правый/верх в мировых координатах
- Camera cam = mc.gameRenderer.mainCamera();
- float yaw = (float) Math.toRadians(cam.yRot());
- float pitch = (float) Math.toRadians(cam.xRot());
- Vec3 fwd = new Vec3(-Math.sin(yaw) * Math.cos(pitch), -Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch)).normalize();
- Vec3 rgt = new Vec3(0.0, 1.0, 0.0).cross(fwd);
- if (rgt.lengthSqr() < 1.0E-6) {
- rgt = new Vec3(1.0, 0.0, 0.0);
- } else {
- rgt = rgt.normalize();
- }
- Vec3 upp = fwd.cross(rgt).normalize();
- Vector3f right = new Vector3f((float) rgt.x, (float) rgt.y, (float) rgt.z);
- Vector3f up = new Vector3f((float) upp.x, (float) upp.y, (float) upp.z);
-
- MeshBuilder diskMesh = renderer.batch(
- LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH,
- Renderer3D.DepthMode.PRE_DEPTH
- );
- MeshBuilder coreMesh = renderer.batch(
- LoVisualRenderPipelines.WORLD_COLORED_DEPTH,
- Renderer3D.DepthMode.PRE_DEPTH
- );
- if (diskMesh == null || coreMesh == null) return;
-
- float time = (GameClock.millis() % 100000L) / 1000.0f;
-
- if ("Body".equals(style)) {
- float holeSize = size * 0.34f;
- for (float[] off : BODY_HOLES) {
- Vec3 center = new Vec3(
- baseCenter.x + rgt.x * off[0] + upp.x * off[1],
- baseCenter.y + rgt.y * off[0] + upp.y * off[1],
- baseCenter.z + rgt.z * off[0] + upp.z * off[1]
- );
- appendBlackhole(diskMesh, coreMesh, center, right, up, holeSize, time, spinSpeed, palette, alpha);
- }
- } else {
- appendBlackhole(diskMesh, coreMesh, baseCenter, right, up, size, time, spinSpeed, palette, alpha);
- }
- }
-
- private static void appendBlackhole(MeshBuilder diskMesh,
- MeshBuilder coreMesh,
- Vec3 center,
- Vector3f right,
- Vector3f up,
- float size,
- float time,
- float spinSpeed,
- IntFunction palette,
- float alpha) {
- float horizon = 0.35f * size;
- int rings = 4;
- int segs = 64;
-
- // аккреционный диск: вращающаяся спираль на аддитивном бленде
- for (int ring = 0; ring < rings; ring++) {
- float r0 = horizon + (size - horizon) * (ring / (float) rings);
- float r1 = horizon + (size - horizon) * ((ring + 1) / (float) rings);
- for (int s = 0; s < segs; s++) {
- float a0 = (float) (Math.PI * 2.0 * s / segs);
- float a1 = (float) (Math.PI * 2.0 * (s + 1) / segs);
- int v0 = addBlackholeVertex(diskMesh, center, right, up, r0, a0, horizon, size, time, spinSpeed, palette);
- int v1 = addBlackholeVertex(diskMesh, center, right, up, r1, a0, horizon, size, time, spinSpeed, palette);
- int v2 = addBlackholeVertex(diskMesh, center, right, up, r1, a1, horizon, size, time, spinSpeed, palette);
- int v3 = addBlackholeVertex(diskMesh, center, right, up, r0, a1, horizon, size, time, spinSpeed, palette);
- diskMesh.triangle(v0, v1, v2);
- diskMesh.triangle(v0, v2, v3);
- }
- }
-
- // чёрное ядро закрывает центр диска
- int coreArgb = applyOpacity(0xFF080808, alpha);
- int coreCenter = addBlackholeCoreVertex(coreMesh, center, right, up, 0f, 0f, coreArgb);
- int coreSegs = 32;
- for (int s = 0; s < coreSegs; s++) {
- float a0 = (float) (Math.PI * 2.0 * s / coreSegs);
- float a1 = (float) (Math.PI * 2.0 * (s + 1) / coreSegs);
- int c0 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a0, coreArgb);
- int c1 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a1, coreArgb);
- coreMesh.triangle(coreCenter, c1, c0);
- }
- }
-
- private static int addBlackholeVertex(MeshBuilder mesh,
- Vec3 center,
- Vector3f right,
- Vector3f up,
- float r,
- float angle,
- float horizon,
- float size,
- float time,
- float spinSpeed,
- IntFunction palette) {
- float dist = r / size;
- float rot = time * 2.5f * spinSpeed + 1.3f / (dist + 0.05f);
- float ca = (float) Math.cos(rot);
- float sa = (float) Math.sin(rot);
- float ux = dist * (float) Math.cos(angle);
- float uy = dist * (float) Math.sin(angle);
- float rx = ux * ca - uy * sa;
- float ry = ux * sa + uy * ca;
- float spiral = Math.abs((float) Math.sin(rx * 14.0f + ry * 4.0f) * 0.5f + 0.5f);
- float falloff = (float) Math.pow(horizon / Math.max(dist, 0.001f), 2.2);
- float outerFade = smoothstep(1.0f, horizon, dist);
- float brightness = falloff * (0.55f + 0.45f * spiral) * outerFade;
-
- int color = palette.apply((int) ((Math.toDegrees(angle) + time * 90f * spinSpeed) % 360f));
- color = applyOpacity(color, brightness);
-
- float wx = (float) center.x + right.x() * ux * size + up.x() * uy * size;
- float wy = (float) center.y + right.y() * ux * size + up.y() * uy * size;
- float wz = (float) center.z + right.z() * ux * size + up.z() * uy * size;
- return addVertex(mesh, wx, wy, wz, color);
- }
-
- private static int addBlackholeCoreVertex(MeshBuilder mesh,
- Vec3 center,
- Vector3f right,
- Vector3f up,
- float r,
- float angle,
- int argb) {
- float ux = r * (float) Math.cos(angle);
- float uy = r * (float) Math.sin(angle);
- float wx = (float) center.x + right.x() * ux + up.x() * uy;
- float wy = (float) center.y + right.y() * ux + up.y() * uy;
- float wz = (float) center.z + right.z() * ux + up.z() * uy;
- return addVertex(mesh, wx, wy, wz, argb);
- }
-
- private static float smoothstep(float edge0, float edge1, float x) {
- float t = (x - edge0) / (edge1 - edge0);
- t = Math.max(0f, Math.min(1f, t));
- return t * t * (3f - 2f * t);
- }
-
- private static void appendCrystalGeometry(MeshBuilder mesh,
- Vec3 targetPos,
- CrystalInstance crystal,
- float orbitYawRad,
- float scale,
- int argb,
- boolean filled) {
- if (mesh == null || crystal == null || targetPos == null) {
- return;
- }
-
- float s = CRYSTAL_BASE_SIZE * scale;
- float hPrism = s;
- float hPyramid = s * 1.5f;
-
- Vector3f[] topVertices = new Vector3f[CRYSTAL_SIDES];
- Vector3f[] bottomVertices = new Vector3f[CRYSTAL_SIDES];
- for (int i = 0; i < CRYSTAL_SIDES; i++) {
- float angle = (float) (2.0 * Math.PI * i / CRYSTAL_SIDES);
- float x = (float) (s * Math.cos(angle));
- float z = (float) (s * Math.sin(angle));
- topVertices[i] = new Vector3f(x, hPrism * 0.5f, z);
- bottomVertices[i] = new Vector3f(x, -hPrism * 0.5f, z);
- }
-
- Vector3f topPeak = new Vector3f(0.0f, hPrism * 0.5f + hPyramid, 0.0f);
- Vector3f bottomPeak = new Vector3f(0.0f, -hPrism * 0.5f - hPyramid, 0.0f);
-
- if (filled) {
- for (int i = 0; i < CRYSTAL_SIDES; i++) {
- Vector3f v1 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos);
- Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
- Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
- Vector3f v4 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos);
- addTriangle(mesh, v1, v2, v3, argb);
- addTriangle(mesh, v1, v3, v4, argb);
- }
- for (int i = 0; i < CRYSTAL_SIDES; i++) {
- Vector3f v1 = transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos);
- Vector3f v2 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos);
- Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
- addTriangle(mesh, v1, v2, v3, argb);
- }
- for (int i = 0; i < CRYSTAL_SIDES; i++) {
- Vector3f v1 = transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos);
- Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
- Vector3f v3 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos);
- addTriangle(mesh, v1, v2, v3, argb);
- }
- return;
- }
-
- for (int i = 0; i < CRYSTAL_SIDES; i++) {
- Vector3f bottomA = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos);
- Vector3f bottomB = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
- Vector3f topA = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos);
- Vector3f topB = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
- addLine(mesh, bottomA, bottomB, argb);
- addLine(mesh, topA, topB, argb);
- addLine(mesh, bottomA, topA, argb);
- addLine(mesh, transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos), topA, argb);
- addLine(mesh, transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos), bottomA, argb);
- }
- }
-
- private static Vector3f transformCrystalVertex(Vector3f vertex,
- CrystalInstance crystal,
- float orbitYawRad,
- Vec3 targetPos) {
- Vector3f transformed = new Vector3f(vertex).mul(crystal.pulsation());
- transformed.rotateX((float) Math.toRadians(crystal.rotation.x));
- transformed.rotateY((float) Math.toRadians(crystal.rotation.y + crystal.selfRotationDeg()));
- transformed.rotateZ((float) Math.toRadians(crystal.rotation.z));
- transformed.add((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z);
- transformed.rotateY(orbitYawRad);
- transformed.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z);
- return transformed;
- }
-
- private static void appendBloomSphere(MeshBuilder mesh,
- Vec3 targetPos,
- CrystalInstance crystal,
- float orbitYawRad,
- int argb) {
- if (mesh == null || crystal == null || targetPos == null) {
- return;
- }
-
- Vector3f center = new Vector3f((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z);
- center.rotateY(orbitYawRad);
- center.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z);
-
- float size = CRYSTAL_BASE_SIZE * 13.0f;
- Quaternionf camRot = RenderState.cameraRotation;
- float yaw = orbitYawRad;
- int bloomColor = applyOpacity(argb, (0.4f * 25.0f) / 255.0f);
-
- for (int i = 0; i < 6; i++) {
- float angle = (360.0f / 6.0f) * i;
- Quaternionf rotation = new Quaternionf()
- .rotateY((float) Math.toRadians(angle))
- .rotateY(yaw)
- .mul(camRot);
- addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor);
- }
-
- for (int i = 0; i < 6; i++) {
- float angle = (360.0f / 6.0f) * i;
- Quaternionf rotation = new Quaternionf()
- .rotateX((float) Math.toRadians(90.0f))
- .rotateY((float) Math.toRadians(angle))
- .rotateY(yaw)
- .mul(camRot);
- addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor);
- }
- }
-
- private static void addTexturedBillboard(MeshBuilder mesh,
- double cx, double cy, double cz,
- float size, Quaternionf rotation, int argb) {
- Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(rotation).mul(size * 0.5f);
- Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(rotation).mul(size * 0.5f);
- addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb);
- }
-
- private static void addTriangle(MeshBuilder mesh, Vector3f v1, Vector3f v2, Vector3f v3, int argb) {
- mesh.ensureTriCapacity();
- int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb);
- int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb);
- int i3 = addVertex(mesh, v3.x, v3.y, v3.z, argb);
- mesh.triangle(i1, i2, i3);
- }
-
- private static void addLine(MeshBuilder mesh, Vector3f v1, Vector3f v2, int argb) {
- mesh.ensureLineCapacity();
- int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb);
- int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb);
- mesh.line(i1, i2);
- }
-
- private static void addRotatedBillboard(MeshBuilder mesh, double cx, double cy, double cz,
- float size, float angle, IntFunction palette) {
- Quaternionf camRot = RenderState.cameraRotation;
- Vector3f right = new Vector3f(1, 0, 0).rotate(camRot);
- Vector3f up = new Vector3f(0, 1, 0).rotate(camRot);
-
- Vector3f r = new Vector3f(right).mul(size);
- Vector3f u = new Vector3f(up).mul(size);
-
- float sin = Mth.sin((float) Math.toRadians(angle));
- float cos = Mth.cos((float) Math.toRadians(angle));
-
- Vector3f rRot = new Vector3f(
- r.x() * cos - u.x() * sin,
- r.y() * cos - u.y() * sin,
- r.z() * cos - u.z() * sin
- );
- Vector3f uRot = new Vector3f(
- r.x() * sin + u.x() * cos,
- r.y() * sin + u.y() * cos,
- r.z() * sin + u.z() * cos
- );
-
- addBillboardVertices(mesh, cx, cy, cz, rRot, uRot,
- palette.apply(90),
- palette.apply(0),
- palette.apply(180),
- palette.apply(270));
- }
-
- private static void addBillboardQuad(MeshBuilder mesh,
- double cx, double cy, double cz,
- float size, int argb) {
- Quaternionf camRot = RenderState.cameraRotation;
- Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
- Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size);
- addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb);
- }
-
- private static void addBillboardVertices(MeshBuilder mesh, double cx, double cy, double cz,
- Vector3f right, Vector3f up,
- int c1, int c2, int c3, int c4) {
- double p1x = cx - right.x() - up.x();
- double p1y = cy - right.y() - up.y();
- double p1z = cz - right.z() - up.z();
- double p2x = cx + right.x() - up.x();
- double p2y = cy + right.y() - up.y();
- double p2z = cz + right.z() - up.z();
- double p3x = cx + right.x() + up.x();
- double p3y = cy + right.y() + up.y();
- double p3z = cz + right.z() + up.z();
- double p4x = cx - right.x() + up.x();
- double p4y = cy - right.y() + up.y();
- double p4z = cz - right.z() + up.z();
-
- mesh.ensureQuadCapacity();
- int i1 = addVertex(mesh, p1x, p1y, p1z, 0, 1, c1);
- int i2 = addVertex(mesh, p2x, p2y, p2z, 1, 1, c2);
- int i3 = addVertex(mesh, p3x, p3y, p3z, 1, 0, c3);
- int i4 = addVertex(mesh, p4x, p4y, p4z, 0, 0, c4);
- mesh.quad(i1, i2, i3, i4);
- }
-
- private static int applyOpacity(int argb, float opacity) {
- opacity = Math.min(1f, Math.max(0f, opacity));
- int baseA = (argb >>> 24) & 0xFF;
- int a = Mth.clamp((int) (baseA * opacity), 0, 255);
- return (a << 24) | (argb & 0x00FFFFFF);
- }
-
- private static int addVertex(MeshBuilder mesh, double x, double y, double z, int argb) {
- int a = (argb >>> 24) & 0xFF;
- int r = (argb >>> 16) & 0xFF;
- int g = (argb >>> 8) & 0xFF;
- int b = argb & 0xFF;
- mesh.vec3(x, y, z).color(r, g, b, a);
- return mesh.next();
- }
-
- private static int addVertex(MeshBuilder mesh, double x, double y, double z, double u, double v, int argb) {
- int a = (argb >>> 24) & 0xFF;
- int r = (argb >>> 16) & 0xFF;
- int g = (argb >>> 8) & 0xFF;
- int b = argb & 0xFF;
- mesh.vec3(x, y, z).vec2(u, v).color(r, g, b, a);
- return mesh.next();
- }
-
- private static double absSinAnimation(double input) {
- return Math.abs(1 + Math.sin(input)) / 2;
- }
-
- private static double interpolate(double oldValue, double newValue, double interpolationValue) {
- return (oldValue + (newValue - oldValue) * interpolationValue);
- }
-
- private static float interpolateFloat(float oldValue, float newValue, double interpolationValue) {
- return (float) interpolate(oldValue, newValue, interpolationValue);
- }
-
@Override
public void onDisable() {
target = null;
@@ -1024,38 +423,4 @@ public class TargetESP extends Module {
default -> AnimatedRenderColors.Mode.STATIC;
};
}
-
- private static final class CrystalInstance {
- private final Vec3 position;
- private final Vec3 rotation;
- private final float rotationSpeed;
-
- private CrystalInstance(Vec3 position, Vec3 rotation) {
- this.position = position;
- this.rotation = rotation;
- this.rotationSpeed = 0.5f + (float) (Math.random() * 1.5f);
- }
-
- private float selfRotationDeg() {
- return (GameClock.millis() % 36000L) / 100.0f * rotationSpeed;
- }
-
- private float pulsation() {
- return 1.0f + (float) (Math.sin(GameClock.millis() / 500.0) * 0.1f);
- }
-
- private void append(MeshBuilder additiveMesh,
- MeshBuilder fillMesh,
- MeshBuilder lineMesh,
- MeshBuilder bloomMesh,
- Vec3 targetPos,
- float orbitYawRad,
- int baseColor) {
- appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.2f), true);
- appendCrystalGeometry(fillMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.3f), true);
- appendCrystalGeometry(lineMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.8f), false);
- appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.2f, applyOpacity(baseColor, 0.3f), true);
- appendBloomSphere(bloomMesh, targetPos, this, orbitYawRad, baseColor);
- }
- }
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java
new file mode 100644
index 0000000..b2be6d9
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java
@@ -0,0 +1,190 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.predictions;
+
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.config.values.RGBColorValue;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.util.Mth;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.entity.projectile.Projectile;
+import net.minecraft.world.entity.projectile.arrow.Arrow;
+import net.minecraft.world.entity.projectile.arrow.ThrownTrident;
+import net.minecraft.world.entity.projectile.hurtingprojectile.SmallFireball;
+import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.*;
+import net.minecraft.world.item.BowItem;
+import net.minecraft.world.item.CrossbowItem;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Held-item aim prediction for the Predictions module: builds the hypothetical
+ * projectile for the item in hand and simulates its trajectory.
+ */
+public final class PredictionsAim {
+ private PredictionsAim() {
+ }
+
+ public static AimPrediction computeAimPrediction(Player player, float tickDelta,
+ NumberValue maxSimTicks,
+ NumberValue pearlMaxSimTicks,
+ BooleanValue pearlColorToggle,
+ RGBColorValue pearlColorValue) {
+ Projectile projectile = createHeldProjectile(player, tickDelta);
+ if (projectile == null) {
+ return null;
+ }
+
+ Kind kind = PredictionsModel.kindFromProjectile(projectile);
+ TrajectoryResult result = PredictionsSimulation.simulateEntity(projectile, kind, tickDelta,
+ maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue);
+ if (result == null || result.points().isEmpty()) {
+ return null;
+ }
+
+ Vec3 hitPos = result.impactPos() != null ? result.impactPos() : result.points().getLast();
+ Entity hitEntity = result.hitEntity();
+ Vec3 anchorPos;
+ if (hitEntity instanceof LivingEntity living) {
+ anchorPos = living.getPosition(tickDelta).add(0.0, living.getBbHeight() * 0.5, 0.0);
+ } else if (hitEntity != null) {
+ anchorPos = hitEntity.getBoundingBox().getCenter();
+ } else {
+ anchorPos = hitPos;
+ }
+
+ return new AimPrediction(hitEntity, hitPos, anchorPos, result.impactNormal(), result.iconStack(), result);
+ }
+
+ private static Projectile createHeldProjectile(Player player, float tickDelta) {
+ Minecraft mc = Minecraft.getInstance();
+ ClientLevel world = mc.level;
+ if (world == null) {
+ return null;
+ }
+
+ InteractionHand hand = null;
+ ItemStack handStack = ItemStack.EMPTY;
+
+ ItemStack mainHand = player.getItemInHand(InteractionHand.MAIN_HAND);
+ if (isAimUsable(player, mainHand, InteractionHand.MAIN_HAND)) {
+ hand = InteractionHand.MAIN_HAND;
+ handStack = mainHand;
+ } else {
+ ItemStack offHand = player.getItemInHand(InteractionHand.OFF_HAND);
+ if (isAimUsable(player, offHand, InteractionHand.OFF_HAND)) {
+ hand = InteractionHand.OFF_HAND;
+ handStack = offHand;
+ }
+ }
+
+ if (hand == null || handStack.isEmpty()) {
+ return null;
+ }
+
+ Projectile projectile = null;
+ float speed = 0.0f;
+ float roll = 0.0f;
+
+ if (handStack.is(Items.ENDER_PEARL)) {
+ projectile = new ThrownEnderpearl(world, player, handStack);
+ speed = 1.5f;
+ } else if (handStack.is(Items.SNOWBALL)) {
+ projectile = new Snowball(world, player, handStack);
+ speed = 1.5f;
+ } else if (handStack.is(Items.EGG)) {
+ projectile = new ThrownEgg(world, player, handStack);
+ speed = 1.5f;
+ } else if (handStack.is(Items.SPLASH_POTION) || handStack.is(Items.LINGERING_POTION)) {
+ projectile = handStack.is(Items.LINGERING_POTION)
+ ? new ThrownLingeringPotion(world, player, handStack)
+ : new ThrownSplashPotion(world, player, handStack);
+ speed = 0.5f;
+ roll = -20.0f;
+ } else if (handStack.is(Items.EXPERIENCE_BOTTLE)) {
+ projectile = new ThrownExperienceBottle(world, player, handStack);
+ speed = 0.7f;
+ roll = -20.0f;
+ } else if (handStack.is(Items.WIND_CHARGE)) {
+ projectile = new WindCharge(player, world, player.position().x, player.getEyePosition().y, player.position().z);
+ speed = 1.5f;
+ } else if (handStack.is(Items.FIRE_CHARGE)) {
+ projectile = new SmallFireball(world, player, Vec3.ZERO);
+ speed = 1.0f;
+ } else if (handStack.is(Items.TRIDENT) && player.isUsingItem() && player.getUsedItemHand() == hand) {
+ projectile = new ThrownTrident(world, player, handStack);
+ speed = 2.5f;
+ } else if (handStack.is(Items.BOW) && player.isUsingItem() && player.getUsedItemHand() == hand) {
+ float pull = BowItem.getPowerForTime(player.getTicksUsingItem());
+ if (pull <= 0.0f) {
+ return null;
+ }
+ ItemStack arrowStack = new ItemStack(Items.ARROW);
+ projectile = new Arrow(world, player, arrowStack, handStack);
+ speed = 3.0f * pull;
+ } else if (handStack.getItem() instanceof CrossbowItem && CrossbowItem.isCharged(handStack)) {
+ ItemStack arrowStack = new ItemStack(Items.ARROW);
+ projectile = new Arrow(world, player, arrowStack, handStack);
+ speed = 3.15f;
+ }
+
+ if (projectile == null) {
+ return null;
+ }
+
+ Vec3 eye = player.getEyePosition(tickDelta);
+ projectile.absSnapTo(eye.x, eye.y, eye.z);
+ setHeldProjectileVelocity(projectile, player, player.getXRot(), player.getYRot(), roll, speed);
+ return projectile;
+ }
+
+ private static void setHeldProjectileVelocity(Projectile projectile, Entity shooter, float pitch, float yaw, float roll, float speed) {
+ float x = -Mth.sin(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f));
+ float y = -Mth.sin((pitch + roll) * ((float) Math.PI / 180.0f));
+ float z = Mth.cos(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f));
+
+ Vec3 velocity = new Vec3(x, y, z).normalize().scale(speed);
+ projectile.setDeltaMovement(velocity);
+
+ Vec3 movement = shooter.getKnownMovement();
+ projectile.setDeltaMovement(projectile.getDeltaMovement().add(movement.x, shooter.onGround() ? 0.0 : movement.y, movement.z));
+ }
+
+ private static boolean isAimUsable(Player player, ItemStack stack, InteractionHand hand) {
+ if (stack == null || stack.isEmpty()) {
+ return false;
+ }
+
+ if (stack.is(Items.BOW) || stack.is(Items.TRIDENT)) {
+ return player.isUsingItem() && player.getUsedItemHand() == hand && player.getUseItem() == stack;
+ }
+
+ if (stack.is(Items.CROSSBOW)) {
+ return net.minecraft.world.item.CrossbowItem.isCharged(stack);
+ }
+
+ return stack.is(Items.ENDER_PEARL)
+ || stack.is(Items.SNOWBALL)
+ || stack.is(Items.EGG)
+ || stack.is(Items.SPLASH_POTION)
+ || stack.is(Items.LINGERING_POTION)
+ || stack.is(Items.EXPERIENCE_BOTTLE)
+ || stack.is(Items.FIRE_CHARGE)
+ || stack.is(Items.WIND_CHARGE);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java
new file mode 100644
index 0000000..6aade55
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java
@@ -0,0 +1,194 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.predictions;
+
+import dev.loki.lovisual.render.engine.color.RenderColor;
+import dev.loki.lovisual.render.engine.renderer.Renderer3D;
+import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.phys.HitResult;
+import net.minecraft.world.phys.Vec3;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+
+/**
+ * Low level mesh primitives, impact shapes and small vector/color math shared by
+ * the Predictions renderers.
+ */
+public final class PredictionsGeometry {
+ private PredictionsGeometry() {
+ }
+
+ private static final int IMPACT_RING_SEGMENTS = 64;
+ private static final float IMPACT_RING_CROSS_RADIUS = 0.18f;
+ private static final float IMPACT_DECAL_EDGE_SOFTNESS = 0.085f;
+ private static final float IMPACT_DECAL_STROKE_WIDTH = 0.10f;
+ private static final float IMPACT_DECAL_PATTERN_SCALE = 4.25f;
+ private static final float IMPACT_DECAL_FILL_ALPHA = 0.48f;
+ private static final float IMPACT_DECAL_STROKE_ALPHA = 0.92f;
+ private static final float IMPACT_DECAL_PATTERN_STRENGTH = 0.10f;
+ private static final float IMPACT_DECAL_QUAD_PADDING = 1.18f;
+ private static final double IMPACT_DECAL_OFFSET = 0.004;
+
+ public static void addGradientLine(MeshBuilder mesh, Vec3 from, Vec3 to, int startColor, int endColor) {
+ if (mesh == null || from == null || to == null) return;
+ mesh.ensureLineCapacity();
+ int i1 = mesh.vec3(from.x, from.y, from.z).color(new RenderColor(startColor)).next();
+ int i2 = mesh.vec3(to.x, to.y, to.z).color(new RenderColor(endColor)).next();
+ mesh.line(i1, i2);
+ }
+
+ public static void addImpactRing(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) {
+ if (mesh == null || center == null) return;
+
+ Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6
+ ? new Vec3(0.0, 1.0, 0.0)
+ : normal.normalize();
+ Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0);
+ Vec3 tangent = useNormal.cross(axis);
+ if (tangent.lengthSqr() < 1.0E-6) {
+ tangent = new Vec3(1.0, 0.0, 0.0);
+ } else {
+ tangent = tangent.normalize();
+ }
+ Vec3 bitangent = useNormal.cross(tangent).normalize();
+
+ for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) {
+ double angle0 = (Math.PI * 2.0 * i) / IMPACT_RING_SEGMENTS;
+ double angle1 = (Math.PI * 2.0 * (i + 1)) / IMPACT_RING_SEGMENTS;
+ Vec3 p0 = center
+ .add(tangent.scale(Math.cos(angle0) * radius))
+ .add(bitangent.scale(Math.sin(angle0) * radius));
+ Vec3 p1 = center
+ .add(tangent.scale(Math.cos(angle1) * radius))
+ .add(bitangent.scale(Math.sin(angle1) * radius));
+ addGradientLine(mesh, p0, p1, argb, argb);
+ }
+
+ Vec3 crossTangent = tangent.scale(IMPACT_RING_CROSS_RADIUS);
+ Vec3 crossBitangent = bitangent.scale(IMPACT_RING_CROSS_RADIUS);
+ addGradientLine(mesh, center.subtract(crossTangent), center.add(crossTangent), argb, argb);
+ addGradientLine(mesh, center.subtract(crossBitangent), center.add(crossBitangent), argb, argb);
+ }
+
+ public static void addImpactDecal(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) {
+ if (mesh == null || center == null || radius <= 0.0f) {
+ return;
+ }
+
+ Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6
+ ? new Vec3(0.0, 1.0, 0.0)
+ : normal.normalize();
+ Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0);
+ Vec3 tangent = useNormal.cross(axis);
+ if (tangent.lengthSqr() < 1.0E-6) {
+ tangent = new Vec3(1.0, 0.0, 0.0);
+ } else {
+ tangent = tangent.normalize();
+ }
+ Vec3 bitangent = useNormal.cross(tangent).normalize();
+ Vec3 offsetCenter = center.add(useNormal.scale(IMPACT_DECAL_OFFSET));
+ float quadRadius = radius * IMPACT_DECAL_QUAD_PADDING;
+ float sdfRadius = 1.0f / IMPACT_DECAL_QUAD_PADDING;
+ Vec3 tangentScaled = tangent.scale(quadRadius);
+ Vec3 bitangentScaled = bitangent.scale(quadRadius);
+
+ mesh.ensureQuadCapacity();
+ int i1 = mesh.vec3(offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).x,
+ offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).y,
+ offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).z)
+ .vec2(0.0, 0.0)
+ .color(new RenderColor(argb))
+ .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
+ .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
+ .next();
+ int i2 = mesh.vec3(offsetCenter.add(tangentScaled).subtract(bitangentScaled).x,
+ offsetCenter.add(tangentScaled).subtract(bitangentScaled).y,
+ offsetCenter.add(tangentScaled).subtract(bitangentScaled).z)
+ .vec2(1.0, 0.0)
+ .color(new RenderColor(argb))
+ .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
+ .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
+ .next();
+ int i3 = mesh.vec3(offsetCenter.add(tangentScaled).add(bitangentScaled).x,
+ offsetCenter.add(tangentScaled).add(bitangentScaled).y,
+ offsetCenter.add(tangentScaled).add(bitangentScaled).z)
+ .vec2(1.0, 1.0)
+ .color(new RenderColor(argb))
+ .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
+ .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
+ .next();
+ int i4 = mesh.vec3(offsetCenter.subtract(tangentScaled).add(bitangentScaled).x,
+ offsetCenter.subtract(tangentScaled).add(bitangentScaled).y,
+ offsetCenter.subtract(tangentScaled).add(bitangentScaled).z)
+ .vec2(0.0, 1.0)
+ .color(new RenderColor(argb))
+ .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE)
+ .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f)
+ .next();
+ mesh.quad(i1, i2, i3, i4);
+ }
+
+ public static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz,
+ float size, Quaternionf camRot, int argb) {
+ Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot).mul(size);
+ Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot).mul(size);
+
+ double p1x = cx - right.x() - up.x();
+ double p1y = cy - right.y() - up.y();
+ double p1z = cz - right.z() - up.z();
+ double p2x = cx + right.x() - up.x();
+ double p2y = cy + right.y() - up.y();
+ double p2z = cz + right.z() - up.z();
+ double p3x = cx + right.x() + up.x();
+ double p3y = cy + right.y() + up.y();
+ double p3z = cz + right.z() + up.z();
+ double p4x = cx - right.x() + up.x();
+ double p4y = cy - right.y() + up.y();
+ double p4z = cz - right.z() + up.z();
+
+ mesh.ensureQuadCapacity();
+ int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0.0, 1.0).color(new RenderColor(argb)).next();
+ int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(argb)).next();
+ int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(argb)).next();
+ int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(argb)).next();
+ mesh.quad(i1, i2, i3, i4);
+ }
+
+ public static int multiplyAlpha(int argb, float alphaMultiplier) {
+ int baseAlpha = (argb >>> 24) & 0xFF;
+ int alpha = Math.max(0, Math.min(255, Math.round(baseAlpha * Math.max(0.0f, alphaMultiplier))));
+ return (argb & 0x00FFFFFF) | (alpha << 24);
+ }
+
+ public static int withAlpha(int argb, int alpha) {
+ return (argb & 0x00FFFFFF) | (Math.max(0, Math.min(255, alpha)) << 24);
+ }
+
+ public static double partialStepProgress(Vec3 from, Vec3 hitPos, double segmentLengthSq) {
+ if (from == null || hitPos == null || segmentLengthSq <= 1.0E-6) {
+ return 1.0;
+ }
+ return Mth.clamp(from.distanceTo(hitPos) / Math.sqrt(segmentLengthSq), 0.0, 1.0);
+ }
+
+ public static Vec3 resolveImpactNormal(HitResult hit, Vec3 velocity) {
+ if (hit instanceof net.minecraft.world.phys.BlockHitResult blockHit) {
+ return Vec3.atLowerCornerOf(blockHit.getDirection().getUnitVec3i());
+ }
+ if (velocity != null && velocity.lengthSqr() > 1.0E-6) {
+ return velocity.normalize();
+ }
+ return new Vec3(0.0, 1.0, 0.0);
+ }
+
+ public static Vec3 advanceTickVelocity(Entity entity, Vec3 velocity) {
+ return velocity.scale(0.99).add(0.0, -entity.getGravity(), 0.0);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java
new file mode 100644
index 0000000..ee33f05
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java
@@ -0,0 +1,157 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.predictions;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.projectile.FireworkRocketEntity;
+import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball;
+import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge;
+import net.minecraft.world.entity.projectile.arrow.ThrownTrident;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.AbstractThrownPotion;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg;
+import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.List;
+
+/**
+ * Shared value types and item/entity classification helpers for the Predictions module.
+ */
+public final class PredictionsModel {
+ private PredictionsModel() {
+ }
+
+ public enum Kind {
+ PEARL_ENTITY,
+ ARROW,
+ TRIDENT,
+ FIREBALL,
+ WIND_CHARGE,
+ FIREWORK,
+ SNOWBALL,
+ EGG,
+ POTION_SPLASH,
+ POTION_LINGERING
+ }
+
+ public record TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack,
+ int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity,
+ boolean hitBlock) {
+ public TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack,
+ int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity, boolean hitBlock) {
+ this.points = points;
+ this.flightTicks = flightTicks;
+ this.impactTick = impactTick;
+ this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack;
+ this.argb = argb;
+ this.kind = kind;
+ this.impactPos = impactPos;
+ this.impactNormal = impactNormal;
+ this.hitEntity = hitEntity;
+ this.hitBlock = hitBlock;
+ }
+ }
+
+ public record AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack,
+ TrajectoryResult trajectory) {
+ public AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack,
+ TrajectoryResult trajectory) {
+ this.entity = entity;
+ this.hitPos = hitPos;
+ this.anchorPos = anchorPos;
+ this.hitNormal = hitNormal;
+ this.displayStack = displayStack == null ? ItemStack.EMPTY : displayStack;
+ this.trajectory = trajectory;
+ }
+ }
+
+ public record EntityCollision(Entity entity, Vec3 hitPos, Vec3 hitNormal) {
+ }
+
+ public record TimerPlate(float textX, float textY, String text,
+ float iconX, float iconY, float iconScale, ItemStack iconStack) {
+ public TimerPlate(float textX, float textY, String text,
+ float iconX, float iconY, float iconScale, ItemStack iconStack) {
+ this.textX = textX;
+ this.textY = textY;
+ this.text = text;
+ this.iconX = iconX;
+ this.iconY = iconY;
+ this.iconScale = iconScale;
+ this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack;
+ }
+ }
+
+ public static Kind kindFromParams(ItemStack stack) {
+ if (stack == null) return Kind.ARROW;
+ if (stack.is(Items.TRIDENT)) return Kind.TRIDENT;
+ if (stack.is(Items.SPLASH_POTION)) return Kind.POTION_SPLASH;
+ if (stack.is(Items.LINGERING_POTION)) return Kind.POTION_LINGERING;
+ if (stack.is(Items.SNOWBALL)) return Kind.SNOWBALL;
+ if (stack.is(Items.EGG)) return Kind.EGG;
+ if (stack.is(Items.FIREWORK_ROCKET)) return Kind.FIREWORK;
+ if (stack.is(Items.WIND_CHARGE)) return Kind.WIND_CHARGE;
+ if (stack.is(Items.FIRE_CHARGE)) return Kind.FIREBALL;
+ if (stack.is(Items.ENDER_PEARL)) return Kind.PEARL_ENTITY;
+ return Kind.ARROW;
+ }
+
+ public static Kind kindFromProjectile(Entity entity) {
+ if (entity instanceof ThrownTrident) return Kind.TRIDENT;
+ if (entity instanceof AbstractThrownPotion potion) {
+ return potion.getItem().is(Items.LINGERING_POTION) ? Kind.POTION_LINGERING : Kind.POTION_SPLASH;
+ }
+ if (entity instanceof Snowball) return Kind.SNOWBALL;
+ if (entity instanceof ThrownEgg) return Kind.EGG;
+ if (entity instanceof FireworkRocketEntity) return Kind.FIREWORK;
+ if (entity instanceof WindCharge) return Kind.WIND_CHARGE;
+ if (entity instanceof Fireball) return Kind.FIREBALL;
+ if (entity instanceof ThrownEnderpearl) return Kind.PEARL_ENTITY;
+ return Kind.ARROW;
+ }
+
+ public static Vec3 resolveEntityImpactNormal(AABB box, Vec3 hitPos, Vec3 fallbackVelocity) {
+ if (box == null || hitPos == null) {
+ return PredictionsGeometry.resolveImpactNormal(null, fallbackVelocity);
+ }
+
+ double dxMin = Math.abs(hitPos.x - box.minX);
+ double dxMax = Math.abs(box.maxX - hitPos.x);
+ double dyMin = Math.abs(hitPos.y - box.minY);
+ double dyMax = Math.abs(box.maxY - hitPos.y);
+ double dzMin = Math.abs(hitPos.z - box.minZ);
+ double dzMax = Math.abs(box.maxZ - hitPos.z);
+
+ double best = dxMin;
+ Vec3 normal = new Vec3(-1.0, 0.0, 0.0);
+ if (dxMax < best) {
+ best = dxMax;
+ normal = new Vec3(1.0, 0.0, 0.0);
+ }
+ if (dyMin < best) {
+ best = dyMin;
+ normal = new Vec3(0.0, -1.0, 0.0);
+ }
+ if (dyMax < best) {
+ best = dyMax;
+ normal = new Vec3(0.0, 1.0, 0.0);
+ }
+ if (dzMin < best) {
+ best = dzMin;
+ normal = new Vec3(0.0, 0.0, -1.0);
+ }
+ if (dzMax < best) {
+ normal = new Vec3(0.0, 0.0, 1.0);
+ }
+ return normal;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java
new file mode 100644
index 0000000..e626dbd
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java
@@ -0,0 +1,196 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.predictions;
+
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.config.values.RGBColorValue;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.EntityCollision;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind;
+import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.ExperienceOrb;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.entity.projectile.Projectile;
+import net.minecraft.world.entity.projectile.ProjectileUtil;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.ClipContext;
+import net.minecraft.world.phys.AABB;
+import net.minecraft.world.phys.EntityHitResult;
+import net.minecraft.world.phys.HitResult;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tick-by-tick projectile trajectory simulation and entity collision resolution
+ * for the Predictions module.
+ */
+public final class PredictionsSimulation {
+ private PredictionsSimulation() {
+ }
+
+ public static final int HIT_SELF_COLOR = 0xFFFF4444;
+ public static final int HIT_ENTITY_COLOR = 0xFFFFCC44;
+
+ public static TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta,
+ NumberValue maxSimTicks,
+ NumberValue pearlMaxSimTicks,
+ BooleanValue pearlColorToggle,
+ RGBColorValue pearlColorValue) {
+ Minecraft mc = Minecraft.getInstance();
+ ClientLevel world = mc.level;
+ if (world == null) return null;
+
+ Vec3 pos = e.position();
+ Vec3 vel = e.getDeltaMovement();
+
+ List points = new ArrayList<>();
+ points.add(pos);
+
+ double t = 0.0;
+ int baseMaxTicks = Math.max(1, maxSimTicks.get());
+ int maxTicks = kind == Kind.PEARL_ENTITY
+ ? Math.max(1, pearlMaxSimTicks.get())
+ : baseMaxTicks;
+ int hitColor = 0;
+ long impactTick = world.getGameTime();
+ Vec3 impactPos = null;
+ Vec3 impactNormal = null;
+ Entity hitEntity = null;
+ boolean hitBlock = false;
+
+ for (int i = 0; i < maxTicks; i++) {
+ Vec3 segmentStart = pos;
+ Vec3 simulatedVelocity = PredictionsGeometry.advanceTickVelocity(e, vel);
+ Vec3 simulatedPos = pos.add(simulatedVelocity);
+ double segmentLengthSq = segmentStart.distanceToSqr(simulatedPos);
+
+ HitResult hit = world.clip(new ClipContext(
+ segmentStart,
+ simulatedPos,
+ ClipContext.Block.COLLIDER,
+ ClipContext.Fluid.NONE,
+ e
+ ));
+
+ EntityCollision entityCollision = getEntityCollision(e, segmentStart, simulatedPos);
+ boolean hasBlockHit = hit.getType() != HitResult.Type.MISS;
+ boolean useEntityHit = entityCollision != null
+ && (!hasBlockHit || segmentStart.distanceToSqr(entityCollision.hitPos()) <= segmentStart.distanceToSqr(hit.getLocation()));
+
+ if (useEntityHit) {
+ hitColor = (entityCollision.entity() == mc.player) ? HIT_SELF_COLOR : HIT_ENTITY_COLOR;
+ Vec3 entityHitPos = entityCollision.hitPos();
+ t += PredictionsGeometry.partialStepProgress(segmentStart, entityHitPos, segmentLengthSq);
+ points.add(entityHitPos);
+ impactTick = world.getGameTime() + (long) Math.ceil(t);
+ impactPos = entityHitPos;
+ impactNormal = entityCollision.hitNormal();
+ hitEntity = entityCollision.entity();
+ break;
+ } else if (hasBlockHit) {
+ Vec3 blockHitPos = hit.getLocation();
+ t += PredictionsGeometry.partialStepProgress(segmentStart, blockHitPos, segmentLengthSq);
+ points.add(blockHitPos);
+ impactTick = world.getGameTime() + (long) Math.ceil(t);
+ impactPos = blockHitPos;
+ impactNormal = PredictionsGeometry.resolveImpactNormal(hit, simulatedVelocity);
+ hitBlock = true;
+ break;
+ }
+
+ pos = simulatedPos;
+ vel = simulatedVelocity;
+ t += 1.0;
+ points.add(pos);
+ impactTick = world.getGameTime() + (long) Math.ceil(t);
+ }
+
+ ItemStack icon = switch (kind) {
+ case PEARL_ENTITY -> new ItemStack(Items.ENDER_PEARL);
+ case TRIDENT -> new ItemStack(Items.TRIDENT);
+ case FIREBALL -> new ItemStack(Items.FIRE_CHARGE);
+ case WIND_CHARGE -> new ItemStack(Items.WIND_CHARGE);
+ case FIREWORK -> new ItemStack(Items.FIREWORK_ROCKET);
+ case SNOWBALL -> new ItemStack(Items.SNOWBALL);
+ case EGG -> new ItemStack(Items.EGG);
+ case POTION_SPLASH -> new ItemStack(Items.SPLASH_POTION);
+ case POTION_LINGERING -> new ItemStack(Items.LINGERING_POTION);
+ default -> new ItemStack(Items.ARROW);
+ };
+
+ int color = hitColor;
+ if (color == 0 && kind == Kind.PEARL_ENTITY && pearlColorToggle.get()) {
+ color = pearlColorValue.getArgb();
+ }
+
+ return new TrajectoryResult(points, t, impactTick, icon, color, kind, impactPos, impactNormal, hitEntity, hitBlock);
+ }
+
+ private static EntityCollision getEntityCollision(Entity movingEntity, Vec3 startPos, Vec3 endPos) {
+ Vec3 direction = endPos.subtract(startPos);
+ if (direction.lengthSqr() == 0.0) {
+ return null;
+ }
+
+ AABB projectileBox = movingEntity.getBoundingBox().move(startPos.subtract(movingEntity.position()));
+ AABB collisionBox = projectileBox.expandTowards(direction).inflate(0.5);
+ EntityHitResult hitResult = ProjectileUtil.getEntityHitResult(
+ movingEntity,
+ startPos,
+ endPos,
+ collisionBox,
+ entity -> canHitPredictedEntity(movingEntity, entity, collisionBox)
+ && entity instanceof LivingEntity
+ && !(entity instanceof ItemEntity)
+ && !(entity instanceof ExperienceOrb)
+ && entity != movingEntity,
+ direction.lengthSqr()
+ );
+ if (hitResult == null) {
+ return null;
+ }
+
+ return new EntityCollision(
+ hitResult.getEntity(),
+ hitResult.getLocation(),
+ PredictionsModel.resolveEntityImpactNormal(hitResult.getEntity().getBoundingBox(), hitResult.getLocation(), direction)
+ );
+ }
+
+ private static boolean canHitPredictedEntity(Entity projectile, Entity entity, AABB collisionBox) {
+ if (projectile == null || entity == null || !entity.isAlive() || !entity.canBeHitByProjectile()) {
+ return false;
+ }
+
+ if (projectile instanceof Projectile projectileEntity) {
+ Entity owner = projectileEntity.getOwner();
+ if (owner != null && !hasPredictedLeftOwner(owner, entity, collisionBox)) {
+ return !owner.isPassengerOfSameVehicle(entity);
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean hasPredictedLeftOwner(Entity owner, Entity entity, AABB collisionBox) {
+ if (owner == null || collisionBox == null) {
+ return true;
+ }
+ return owner.getRootVehicle()
+ .getSelfAndPassengers()
+ .filter(Entity::isPickable)
+ .noneMatch(passenger -> collisionBox.intersects(passenger.getBoundingBox()));
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java
new file mode 100644
index 0000000..ceb7f94
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java
@@ -0,0 +1,179 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.targetesp;
+
+import dev.loki.lovisual.render.engine.RenderState;
+import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
+import dev.loki.lovisual.util.time.GameClock;
+import net.minecraft.world.phys.Vec3;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*;
+
+/**
+ * Crystal orbit geometry for the TargetESP "crystals" mode.
+ */
+public final class TargetEspCrystals {
+ private TargetEspCrystals() {
+ }
+
+ private static final float CRYSTAL_BASE_SIZE = 0.05f;
+ private static final int CRYSTAL_SIDES = 8;
+
+ public static final class CrystalInstance {
+ private final Vec3 position;
+ private final Vec3 rotation;
+ private final float rotationSpeed;
+
+ public CrystalInstance(Vec3 position, Vec3 rotation) {
+ this.position = position;
+ this.rotation = rotation;
+ this.rotationSpeed = 0.5f + (float) (Math.random() * 1.5f);
+ }
+
+ private float selfRotationDeg() {
+ return (GameClock.millis() % 36000L) / 100.0f * rotationSpeed;
+ }
+
+ private float pulsation() {
+ return 1.0f + (float) (Math.sin(GameClock.millis() / 500.0) * 0.1f);
+ }
+
+ public void append(MeshBuilder additiveMesh,
+ MeshBuilder fillMesh,
+ MeshBuilder lineMesh,
+ MeshBuilder bloomMesh,
+ Vec3 targetPos,
+ float orbitYawRad,
+ int baseColor) {
+ appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.2f), true);
+ appendCrystalGeometry(fillMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.3f), true);
+ appendCrystalGeometry(lineMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.8f), false);
+ appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.2f, applyOpacity(baseColor, 0.3f), true);
+ appendBloomSphere(bloomMesh, targetPos, this, orbitYawRad, baseColor);
+ }
+ }
+
+ private static void appendCrystalGeometry(MeshBuilder mesh,
+ Vec3 targetPos,
+ CrystalInstance crystal,
+ float orbitYawRad,
+ float scale,
+ int argb,
+ boolean filled) {
+ if (mesh == null || crystal == null || targetPos == null) {
+ return;
+ }
+
+ float s = CRYSTAL_BASE_SIZE * scale;
+ float hPrism = s;
+ float hPyramid = s * 1.5f;
+
+ Vector3f[] topVertices = new Vector3f[CRYSTAL_SIDES];
+ Vector3f[] bottomVertices = new Vector3f[CRYSTAL_SIDES];
+ for (int i = 0; i < CRYSTAL_SIDES; i++) {
+ float angle = (float) (2.0 * Math.PI * i / CRYSTAL_SIDES);
+ float x = (float) (s * Math.cos(angle));
+ float z = (float) (s * Math.sin(angle));
+ topVertices[i] = new Vector3f(x, hPrism * 0.5f, z);
+ bottomVertices[i] = new Vector3f(x, -hPrism * 0.5f, z);
+ }
+
+ Vector3f topPeak = new Vector3f(0.0f, hPrism * 0.5f + hPyramid, 0.0f);
+ Vector3f bottomPeak = new Vector3f(0.0f, -hPrism * 0.5f - hPyramid, 0.0f);
+
+ if (filled) {
+ for (int i = 0; i < CRYSTAL_SIDES; i++) {
+ Vector3f v1 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos);
+ Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
+ Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
+ Vector3f v4 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos);
+ addTriangle(mesh, v1, v2, v3, argb);
+ addTriangle(mesh, v1, v3, v4, argb);
+ }
+ for (int i = 0; i < CRYSTAL_SIDES; i++) {
+ Vector3f v1 = transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos);
+ Vector3f v2 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos);
+ Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
+ addTriangle(mesh, v1, v2, v3, argb);
+ }
+ for (int i = 0; i < CRYSTAL_SIDES; i++) {
+ Vector3f v1 = transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos);
+ Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
+ Vector3f v3 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos);
+ addTriangle(mesh, v1, v2, v3, argb);
+ }
+ return;
+ }
+
+ for (int i = 0; i < CRYSTAL_SIDES; i++) {
+ Vector3f bottomA = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos);
+ Vector3f bottomB = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
+ Vector3f topA = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos);
+ Vector3f topB = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos);
+ addLine(mesh, bottomA, bottomB, argb);
+ addLine(mesh, topA, topB, argb);
+ addLine(mesh, bottomA, topA, argb);
+ addLine(mesh, transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos), topA, argb);
+ addLine(mesh, transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos), bottomA, argb);
+ }
+ }
+
+ private static Vector3f transformCrystalVertex(Vector3f vertex,
+ CrystalInstance crystal,
+ float orbitYawRad,
+ Vec3 targetPos) {
+ Vector3f transformed = new Vector3f(vertex).mul(crystal.pulsation());
+ transformed.rotateX((float) Math.toRadians(crystal.rotation.x));
+ transformed.rotateY((float) Math.toRadians(crystal.rotation.y + crystal.selfRotationDeg()));
+ transformed.rotateZ((float) Math.toRadians(crystal.rotation.z));
+ transformed.add((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z);
+ transformed.rotateY(orbitYawRad);
+ transformed.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z);
+ return transformed;
+ }
+
+ private static void appendBloomSphere(MeshBuilder mesh,
+ Vec3 targetPos,
+ CrystalInstance crystal,
+ float orbitYawRad,
+ int argb) {
+ if (mesh == null || crystal == null || targetPos == null) {
+ return;
+ }
+
+ Vector3f center = new Vector3f((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z);
+ center.rotateY(orbitYawRad);
+ center.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z);
+
+ float size = CRYSTAL_BASE_SIZE * 13.0f;
+ Quaternionf camRot = RenderState.cameraRotation;
+ float yaw = orbitYawRad;
+ int bloomColor = applyOpacity(argb, (0.4f * 25.0f) / 255.0f);
+
+ for (int i = 0; i < 6; i++) {
+ float angle = (360.0f / 6.0f) * i;
+ Quaternionf rotation = new Quaternionf()
+ .rotateY((float) Math.toRadians(angle))
+ .rotateY(yaw)
+ .mul(camRot);
+ addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor);
+ }
+
+ for (int i = 0; i < 6; i++) {
+ float angle = (360.0f / 6.0f) * i;
+ Quaternionf rotation = new Quaternionf()
+ .rotateX((float) Math.toRadians(90.0f))
+ .rotateY((float) Math.toRadians(angle))
+ .rotateY(yaw)
+ .mul(camRot);
+ addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java
new file mode 100644
index 0000000..cc1bfb9
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java
@@ -0,0 +1,178 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.targetesp;
+
+import dev.loki.lovisual.render.engine.TextureStorage;
+import dev.loki.lovisual.render.engine.math.RenderMath;
+import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
+import dev.loki.lovisual.render.engine.renderer.Renderer3D;
+import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
+import dev.loki.lovisual.util.time.GameClock;
+import net.minecraft.client.Camera;
+import net.minecraft.client.Minecraft;
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.phys.Vec3;
+import org.joml.Vector3f;
+
+import java.util.function.IntFunction;
+
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*;
+
+public final class TargetEspEffects {
+ private TargetEspEffects() {
+ }
+
+ private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY;
+
+ private static final float[][] BODY_HOLES = {
+ {0.0f, 0.60f}, // голова
+ {-0.48f, 0.36f}, // левая рука
+ {0.48f, 0.36f}, // правая рука
+ {0.0f, 0.05f}, // грудь
+ {-0.30f, -0.42f},// левая нога
+ {0.30f, -0.42f} // правая нога
+ };
+
+ public static void renderGhosts(Renderer3D renderer,
+ int espLength, int factor, float shaking, float amplitude,
+ Entity target, float tickDelta, IntFunction palette) {
+ Minecraft mc = Minecraft.getInstance();
+ if (renderer == null || mc == null || mc.player == null) return;
+
+ Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
+ double tPosX = pos.x;
+ double tPosY = pos.y;
+ double tPosZ = pos.z;
+ float iAge = (float) interpolate(target.tickCount - 1, target.tickCount, tickDelta);
+
+ MeshBuilder mesh = renderer.batchTextured(
+ LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
+ GHOST_PARTICLE_TEXTURE,
+ Renderer3D.DepthMode.MAIN
+ );
+ if (mesh == null) return;
+
+ for (int j = 0; j < 3; j++) {
+ for (int i = 0; i <= espLength; i++) {
+ double radians = Math.toRadians((((float) i / 1.5f + iAge) * factor + (j * 120)) % (factor * 360));
+ double sinQuad = Math.sin(Math.toRadians(iAge * 2.5f + i * (j + 1)) * amplitude) / shaking;
+
+ float offset = ((float) i / espLength);
+ int color = applyOpacity(palette.apply((int) (180 * offset)), offset);
+ float scale = Math.max(0.24f * (offset), 0.2f);
+
+ double cx = tPosX + Math.cos(radians) * target.getBbWidth();
+ double cy = tPosY + 1 + sinQuad;
+ double cz = tPosZ + Math.sin(radians) * target.getBbWidth();
+
+ addBillboardQuad(mesh, cx, cy, cz, scale, color);
+ }
+ }
+ }
+
+ public static void renderBlackhole(Renderer3D renderer,
+ LivingEntity target,
+ float tickDelta,
+ IntFunction palette,
+ float size,
+ float spinSpeed,
+ float alpha,
+ String style) {
+ if (renderer == null || target == null) return;
+ Minecraft mc = Minecraft.getInstance();
+ if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return;
+ Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
+ Vec3 baseCenter = new Vec3(pos.x, pos.y + target.getBbHeight() * 0.5, pos.z);
+
+ // Камера-зависимый billboard-базис: правый/верх в мировых координатах
+ Camera cam = mc.gameRenderer.mainCamera();
+ float yaw = (float) Math.toRadians(cam.yRot());
+ float pitch = (float) Math.toRadians(cam.xRot());
+ Vec3 fwd = new Vec3(-Math.sin(yaw) * Math.cos(pitch), -Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch)).normalize();
+ Vec3 rgt = new Vec3(0.0, 1.0, 0.0).cross(fwd);
+ if (rgt.lengthSqr() < 1.0E-6) {
+ rgt = new Vec3(1.0, 0.0, 0.0);
+ } else {
+ rgt = rgt.normalize();
+ }
+ Vec3 upp = fwd.cross(rgt).normalize();
+ Vector3f right = new Vector3f((float) rgt.x, (float) rgt.y, (float) rgt.z);
+ Vector3f up = new Vector3f((float) upp.x, (float) upp.y, (float) upp.z);
+
+ MeshBuilder diskMesh = renderer.batch(
+ LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH,
+ Renderer3D.DepthMode.PRE_DEPTH
+ );
+ MeshBuilder coreMesh = renderer.batch(
+ LoVisualRenderPipelines.WORLD_COLORED_DEPTH,
+ Renderer3D.DepthMode.PRE_DEPTH
+ );
+ if (diskMesh == null || coreMesh == null) return;
+
+ float time = (GameClock.millis() % 100000L) / 1000.0f;
+
+ if ("Body".equals(style)) {
+ float holeSize = size * 0.34f;
+ for (float[] off : BODY_HOLES) {
+ Vec3 center = new Vec3(
+ baseCenter.x + rgt.x * off[0] + upp.x * off[1],
+ baseCenter.y + rgt.y * off[0] + upp.y * off[1],
+ baseCenter.z + rgt.z * off[0] + upp.z * off[1]
+ );
+ appendBlackhole(diskMesh, coreMesh, center, right, up, holeSize, time, spinSpeed, palette, alpha);
+ }
+ } else {
+ appendBlackhole(diskMesh, coreMesh, baseCenter, right, up, size, time, spinSpeed, palette, alpha);
+ }
+ }
+
+ private static void appendBlackhole(MeshBuilder diskMesh,
+ MeshBuilder coreMesh,
+ Vec3 center,
+ Vector3f right,
+ Vector3f up,
+ float size,
+ float time,
+ float spinSpeed,
+ IntFunction palette,
+ float alpha) {
+ float horizon = 0.35f * size;
+ int rings = 4;
+ int segs = 64;
+
+ // аккреционный диск: вращающаяся спираль на аддитивном бленде
+ for (int ring = 0; ring < rings; ring++) {
+ float r0 = horizon + (size - horizon) * (ring / (float) rings);
+ float r1 = horizon + (size - horizon) * ((ring + 1) / (float) rings);
+ for (int s = 0; s < segs; s++) {
+ float a0 = (float) (Math.PI * 2.0 * s / segs);
+ float a1 = (float) (Math.PI * 2.0 * (s + 1) / segs);
+ int v0 = addBlackholeVertex(diskMesh, center, right, up, r0, a0, horizon, size, time, spinSpeed, palette);
+ int v1 = addBlackholeVertex(diskMesh, center, right, up, r1, a0, horizon, size, time, spinSpeed, palette);
+ int v2 = addBlackholeVertex(diskMesh, center, right, up, r1, a1, horizon, size, time, spinSpeed, palette);
+ int v3 = addBlackholeVertex(diskMesh, center, right, up, r0, a1, horizon, size, time, spinSpeed, palette);
+ diskMesh.triangle(v0, v1, v2);
+ diskMesh.triangle(v0, v2, v3);
+ }
+ }
+
+ // чёрное ядро закрывает центр диска
+ int coreArgb = applyOpacity(0xFF080808, alpha);
+ int coreCenter = addBlackholeCoreVertex(coreMesh, center, right, up, 0f, 0f, coreArgb);
+ int coreSegs = 32;
+ for (int s = 0; s < coreSegs; s++) {
+ float a0 = (float) (Math.PI * 2.0 * s / coreSegs);
+ float a1 = (float) (Math.PI * 2.0 * (s + 1) / coreSegs);
+ int c0 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a0, coreArgb);
+ int c1 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a1, coreArgb);
+ coreMesh.triangle(coreCenter, c1, c0);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java
new file mode 100644
index 0000000..9adb689
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java
@@ -0,0 +1,199 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.targetesp;
+
+import dev.loki.lovisual.render.engine.RenderState;
+import dev.loki.lovisual.render.engine.TextureStorage;
+import dev.loki.lovisual.render.engine.math.RenderMath;
+import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
+import dev.loki.lovisual.render.engine.renderer.Renderer3D;
+import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
+import dev.loki.lovisual.util.time.GameClock;
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.function.IntFunction;
+
+import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*;
+
+/**
+ * State-driven TargetESP modes: animation state, the legacy ring, ghost
+ * particles and the capture mark.
+ */
+public final class TargetEspModes {
+ private TargetEspModes() {
+ }
+
+ private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE;
+ private static final int VORTEX_HELIX_STEPS = 28;
+ private static final int VORTEX_RING_SEGMENTS = 24;
+
+ private static float prevCircleStep;
+ private static float circleStep;
+ private static double captureValue = 1.0;
+ private static double prevCaptureValue;
+ private static double captureSpeed = 1.0;
+ private static double captureSpeedScale = 1.0;
+ private static boolean captureFlipSpeed;
+
+ public static void tickTargetEsp() {
+ prevCircleStep = circleStep;
+ circleStep += 0.15f;
+
+ prevCaptureValue = captureValue;
+ captureValue += captureSpeed * captureSpeedScale;
+ if (captureSpeed > 25.0) captureFlipSpeed = true;
+ if (captureSpeed < -25.0) captureFlipSpeed = false;
+ captureSpeed = captureFlipSpeed
+ ? captureSpeed - 0.5 * captureSpeedScale
+ : captureSpeed + 0.5 * captureSpeedScale;
+ }
+
+ public static void setCaptureSpeedScale(float scale) {
+ if (scale <= 0f) {
+ captureSpeedScale = 0.05;
+ } else {
+ captureSpeedScale = scale;
+ }
+ }
+
+ public static void drawOldTargetEsp(Renderer3D renderer, Entity target, float tickDelta, IntFunction palette) {
+ if (renderer == null || target == null) return;
+ double cs = prevCircleStep + (circleStep - prevCircleStep) * tickDelta;
+ double prevSinAnim = absSinAnimation(cs - 0.45f);
+ double sinAnim = absSinAnimation(cs);
+ Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
+ double x = pos.x;
+ double y = pos.y + prevSinAnim * target.getBbHeight();
+ double z = pos.z;
+ double nextY = pos.y + sinAnim * target.getBbHeight();
+
+ MeshBuilder mesh = renderer.tris();
+
+ final int segments = 96;
+ final double step = Math.PI * 2.0 / segments;
+ final double radius = target.getBbWidth() * 0.8;
+
+ for (int i = 0; i < segments; i++) {
+ double a0 = i * step;
+ double a1 = (i + 1) * step;
+ float cos = (float) (x + Math.cos(a0) * radius);
+ float sin = (float) (z + Math.sin(a0) * radius);
+ float cosNext = (float) (x + Math.cos(a1) * radius);
+ float sinNext = (float) (z + Math.sin(a1) * radius);
+
+ int idx0 = (int) Math.round(i * (360.0 / segments));
+ int idx1 = (int) Math.round((i + 1) * (360.0 / segments));
+ int cTop0 = applyOpacity(palette.apply(idx0), 170.0f / 255.0f);
+ int cTop1 = applyOpacity(palette.apply(idx1), 170.0f / 255.0f);
+ int cBottom0 = applyOpacity(palette.apply(idx0), 0.0f);
+ int cBottom1 = applyOpacity(palette.apply(idx1), 0.0f);
+
+ mesh.ensureQuadCapacity();
+ int i1 = addVertex(mesh, cos, nextY, sin, cTop0);
+ int i2 = addVertex(mesh, cosNext, nextY, sinNext, cTop1);
+ int i3 = addVertex(mesh, cosNext, y, sinNext, cBottom1);
+ int i4 = addVertex(mesh, cos, y, sin, cBottom0);
+ mesh.quad(i1, i2, i3, i4);
+ }
+
+ }
+
+ public static void renderVortex(Renderer3D renderer,
+ LivingEntity target,
+ float tickDelta,
+ IntFunction palette,
+ float spinSpeed) {
+ if (renderer == null || target == null) return;
+
+ Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
+ float radius = target.getBbWidth() * 0.7f;
+ float height = target.getBbHeight();
+ double time = (GameClock.millis() / 1000.0) * spinSpeed;
+
+ float prevWidth = RenderState.lineWidth;
+ RenderState.lineWidth = 1.4f;
+ MeshBuilder mesh;
+ try {
+ mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
+ } finally {
+ RenderState.lineWidth = prevWidth;
+ }
+ if (mesh == null) return;
+
+ for (int strand = 0; strand < 2; strand++) {
+ double strandOffset = strand * Math.PI;
+ int prevIndex = -1;
+ for (int i = 0; i <= VORTEX_HELIX_STEPS; i++) {
+ double t = (double) i / VORTEX_HELIX_STEPS;
+ double angle = time * 2.4 + t * Math.PI * 3.0 + strandOffset;
+ double r = radius * (0.85 + 0.15 * Math.sin(t * Math.PI));
+ double x = pos.x + Math.cos(angle) * r;
+ double y = pos.y + t * height;
+ double z = pos.z + Math.sin(angle) * r;
+ int color = applyOpacity(palette.apply((int) (t * 180)), (float) (1.0 - t * 0.35));
+
+ int index = addVertex(mesh, x, y, z, color);
+ if (prevIndex >= 0) {
+ mesh.line(prevIndex, index);
+ }
+ prevIndex = index;
+ }
+ }
+
+ float[] ringHeights = {0.05f, height * 0.5f, height * 0.95f};
+ float[] ringSpeeds = {0.4f, -0.55f, 0.35f};
+ for (int ring = 0; ring < ringHeights.length; ring++) {
+ double ringY = pos.y + ringHeights[ring];
+ double ringRotation = time * ringSpeeds[ring];
+ int ringColor = applyOpacity(palette.apply(60), 0.4f);
+ int prevIndex = -1;
+ int firstIndex = -1;
+ for (int i = 0; i <= VORTEX_RING_SEGMENTS; i++) {
+ double angle = ringRotation + (i % VORTEX_RING_SEGMENTS) * 2.0 * Math.PI / VORTEX_RING_SEGMENTS;
+ double x = pos.x + Math.cos(angle) * radius;
+ double z = pos.z + Math.sin(angle) * radius;
+ int index = addVertex(mesh, x, ringY, z, ringColor);
+ if (firstIndex < 0) {
+ firstIndex = index;
+ }
+ if (prevIndex >= 0) {
+ mesh.line(prevIndex, index);
+ }
+ prevIndex = index;
+ }
+ }
+ }
+
+ public static void renderCaptureMark(Renderer3D renderer,
+ Entity target,
+ float tickDelta,
+ IntFunction palette) {
+ if (renderer == null) return;
+ Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
+ double tPosX = pos.x;
+ double tPosY = pos.y;
+ double tPosZ = pos.z;
+
+ MeshBuilder mesh = renderer.batchTextured(
+ LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
+ CAPTURE_MARK_TEXTURE,
+ Renderer3D.DepthMode.MAIN
+ );
+ if (mesh == null) return;
+
+ float angle = interpolateFloat((float) prevCaptureValue, (float) captureValue, tickDelta);
+ float size = 0.75f;
+
+ double cy = tPosY + target.getEyeHeight(target.getPose()) / 2f;
+
+ addRotatedBillboard(mesh, tPosX, cy, tPosZ, size, angle, palette);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java
new file mode 100644
index 0000000..5db1cf1
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java
@@ -0,0 +1,199 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.combat.targetesp;
+
+import dev.loki.lovisual.render.engine.RenderState;
+import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
+import net.minecraft.util.Mth;
+import net.minecraft.world.phys.Vec3;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+
+import java.util.function.IntFunction;
+
+public final class TargetEspRender {
+ private TargetEspRender() {
+ }
+
+ public static void addTriangle(MeshBuilder mesh, Vector3f v1, Vector3f v2, Vector3f v3, int argb) {
+ mesh.ensureTriCapacity();
+ int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb);
+ int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb);
+ int i3 = addVertex(mesh, v3.x, v3.y, v3.z, argb);
+ mesh.triangle(i1, i2, i3);
+ }
+
+ public static void addLine(MeshBuilder mesh, Vector3f v1, Vector3f v2, int argb) {
+ mesh.ensureLineCapacity();
+ int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb);
+ int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb);
+ mesh.line(i1, i2);
+ }
+
+ public static int addVertex(MeshBuilder mesh, double x, double y, double z, int argb) {
+ int a = (argb >>> 24) & 0xFF;
+ int r = (argb >>> 16) & 0xFF;
+ int g = (argb >>> 8) & 0xFF;
+ int b = argb & 0xFF;
+ mesh.vec3(x, y, z).color(r, g, b, a);
+ return mesh.next();
+ }
+
+ public static int addVertex(MeshBuilder mesh, double x, double y, double z, double u, double v, int argb) {
+ int a = (argb >>> 24) & 0xFF;
+ int r = (argb >>> 16) & 0xFF;
+ int g = (argb >>> 8) & 0xFF;
+ int b = argb & 0xFF;
+ mesh.vec3(x, y, z).vec2(u, v).color(r, g, b, a);
+ return mesh.next();
+ }
+
+ public static void addBillboardQuad(MeshBuilder mesh,
+ double cx, double cy, double cz,
+ float size, int argb) {
+ Quaternionf camRot = RenderState.cameraRotation;
+ Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
+ Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size);
+ addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb);
+ }
+
+ public static void addTexturedBillboard(MeshBuilder mesh,
+ double cx, double cy, double cz,
+ float size, Quaternionf rotation, int argb) {
+ Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(rotation).mul(size * 0.5f);
+ Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(rotation).mul(size * 0.5f);
+ addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb);
+ }
+
+ public static void addRotatedBillboard(MeshBuilder mesh, double cx, double cy, double cz,
+ float size, float angle, IntFunction palette) {
+ Quaternionf camRot = RenderState.cameraRotation;
+ Vector3f right = new Vector3f(1, 0, 0).rotate(camRot);
+ Vector3f up = new Vector3f(0, 1, 0).rotate(camRot);
+
+ Vector3f r = new Vector3f(right).mul(size);
+ Vector3f u = new Vector3f(up).mul(size);
+
+ float sin = Mth.sin((float) Math.toRadians(angle));
+ float cos = Mth.cos((float) Math.toRadians(angle));
+
+ Vector3f rRot = new Vector3f(
+ r.x() * cos - u.x() * sin,
+ r.y() * cos - u.y() * sin,
+ r.z() * cos - u.z() * sin
+ );
+ Vector3f uRot = new Vector3f(
+ r.x() * sin + u.x() * cos,
+ r.y() * sin + u.y() * cos,
+ r.z() * sin + u.z() * cos
+ );
+
+ addBillboardVertices(mesh, cx, cy, cz, rRot, uRot,
+ palette.apply(90),
+ palette.apply(0),
+ palette.apply(180),
+ palette.apply(270));
+ }
+
+ public static void addBillboardVertices(MeshBuilder mesh, double cx, double cy, double cz,
+ Vector3f right, Vector3f up,
+ int c1, int c2, int c3, int c4) {
+ double p1x = cx - right.x() - up.x();
+ double p1y = cy - right.y() - up.y();
+ double p1z = cz - right.z() - up.z();
+ double p2x = cx + right.x() - up.x();
+ double p2y = cy + right.y() - up.y();
+ double p2z = cz + right.z() - up.z();
+ double p3x = cx + right.x() + up.x();
+ double p3y = cy + right.y() + up.y();
+ double p3z = cz + right.z() + up.z();
+ double p4x = cx - right.x() + up.x();
+ double p4y = cy - right.y() + up.y();
+ double p4z = cz - right.z() + up.z();
+
+ mesh.ensureQuadCapacity();
+ int i1 = addVertex(mesh, p1x, p1y, p1z, 0, 1, c1);
+ int i2 = addVertex(mesh, p2x, p2y, p2z, 1, 1, c2);
+ int i3 = addVertex(mesh, p3x, p3y, p3z, 1, 0, c3);
+ int i4 = addVertex(mesh, p4x, p4y, p4z, 0, 0, c4);
+ mesh.quad(i1, i2, i3, i4);
+ }
+
+ public static int applyOpacity(int argb, float opacity) {
+ opacity = Math.min(1f, Math.max(0f, opacity));
+ int baseA = (argb >>> 24) & 0xFF;
+ int a = Mth.clamp((int) (baseA * opacity), 0, 255);
+ return (a << 24) | (argb & 0x00FFFFFF);
+ }
+
+ public static int addBlackholeVertex(MeshBuilder mesh,
+ Vec3 center,
+ Vector3f right,
+ Vector3f up,
+ float r,
+ float angle,
+ float horizon,
+ float size,
+ float time,
+ float spinSpeed,
+ IntFunction palette) {
+ float dist = r / size;
+ float rot = time * 2.5f * spinSpeed + 1.3f / (dist + 0.05f);
+ float ca = (float) Math.cos(rot);
+ float sa = (float) Math.sin(rot);
+ float ux = dist * (float) Math.cos(angle);
+ float uy = dist * (float) Math.sin(angle);
+ float rx = ux * ca - uy * sa;
+ float ry = ux * sa + uy * ca;
+ float spiral = Math.abs((float) Math.sin(rx * 14.0f + ry * 4.0f) * 0.5f + 0.5f);
+ float falloff = (float) Math.pow(horizon / Math.max(dist, 0.001f), 2.2);
+ float outerFade = smoothstep(1.0f, horizon, dist);
+ float brightness = falloff * (0.55f + 0.45f * spiral) * outerFade;
+
+ int color = palette.apply((int) ((Math.toDegrees(angle) + time * 90f * spinSpeed) % 360f));
+ color = applyOpacity(color, brightness);
+
+ float wx = (float) center.x + right.x() * ux * size + up.x() * uy * size;
+ float wy = (float) center.y + right.y() * ux * size + up.y() * uy * size;
+ float wz = (float) center.z + right.z() * ux * size + up.z() * uy * size;
+ return addVertex(mesh, wx, wy, wz, color);
+ }
+
+ public static int addBlackholeCoreVertex(MeshBuilder mesh,
+ Vec3 center,
+ Vector3f right,
+ Vector3f up,
+ float r,
+ float angle,
+ int argb) {
+ float ux = r * (float) Math.cos(angle);
+ float uy = r * (float) Math.sin(angle);
+ float wx = (float) center.x + right.x() * ux + up.x() * uy;
+ float wy = (float) center.y + right.y() * ux + up.y() * uy;
+ float wz = (float) center.z + right.z() * ux + up.z() * uy;
+ return addVertex(mesh, wx, wy, wz, argb);
+ }
+
+ public static double absSinAnimation(double input) {
+ return Math.abs(1 + Math.sin(input)) / 2;
+ }
+
+ public static double interpolate(double oldValue, double newValue, double interpolationValue) {
+ return (oldValue + (newValue - oldValue) * interpolationValue);
+ }
+
+ public static float interpolateFloat(float oldValue, float newValue, double interpolationValue) {
+ return (float) interpolate(oldValue, newValue, interpolationValue);
+ }
+
+ private static float smoothstep(float edge0, float edge1, float x) {
+ float t = (x - edge0) / (edge1 - edge0);
+ t = Math.max(0f, Math.min(1f, t));
+ return t * t * (3f - 2f * t);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java
new file mode 100644
index 0000000..e9b0e03
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java
@@ -0,0 +1,105 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.misc;
+
+import dev.loki.lovisual.config.values.ModeValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.features.module.Notifier;
+import net.minecraft.client.Minecraft;
+
+import java.util.UUID;
+
+/**
+ * Blocks forced server resource packs.
+ *
+ * Decline/Remove answer every push with a terminal
+ * {@code ServerboundResourcePackPacket.Action.DECLINED} before vanilla shows
+ * its prompt or starts a download; Remove additionally drops packs that are
+ * already applied.
+ */
+@ModuleInfo(
+ id = "noserverpack",
+ displayName = "No Server Pack",
+ aliases = {"noserverpack", "nopack"},
+ category = ModuleCategory.MISC
+)
+public class NoServerPack extends Module {
+ private static final String SETTING_ACTION = "action";
+
+ private final ModeValue action =
+ mode("noServerPackAction", SETTING_ACTION, Action.DECLINE.id, Action.REMOVE.id, Action.ASK.id);
+
+ private boolean clearedAppliedPacks;
+
+ public Action action() {
+ return Action.byId(action.get());
+ }
+
+ /**
+ * Packet-hook decision. Never touches the network or GUI from here.
+ *
+ * @return true when the push must be cancelled and answered with DECLINED
+ */
+ public boolean shouldDeclinePush() {
+ return shouldDeclinePush(isEnabled(), action.get());
+ }
+
+ /** Called from the packet hook after a push was answered with DECLINED. */
+ public void onPackDeclined(UUID packId) {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || packId == null) return;
+ mc.execute(() -> Notifier.info("Server resource pack declined (" + packId + ")"));
+ }
+
+ @Override
+ public void onTick() {
+ if (!isEnabled()) {
+ clearedAppliedPacks = false;
+ return;
+ }
+ if (action() == Action.REMOVE) {
+ clearAppliedPacksOnce();
+ } else {
+ clearedAppliedPacks = false;
+ }
+ }
+
+ private void clearAppliedPacksOnce() {
+ if (clearedAppliedPacks) return;
+ clearedAppliedPacks = true;
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.getDownloadedPackSource() == null) return;
+ mc.getDownloadedPackSource().popAll();
+ Notifier.info("Applied server packs removed");
+ }
+
+ static boolean shouldDeclinePush(boolean enabled, String mode) {
+ return enabled && Action.byId(mode) != Action.ASK;
+ }
+
+ public enum Action {
+ DECLINE("Decline"),
+ REMOVE("Remove"),
+ ASK("Ask");
+
+ public final String id;
+
+ Action(String id) {
+ this.id = id;
+ }
+
+ static Action byId(String id) {
+ for (Action value : values()) {
+ if (value.id.equals(id)) return value;
+ }
+ return DECLINE;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java
new file mode 100644
index 0000000..9f93a31
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java
@@ -0,0 +1,88 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.util.time.GameClock;
+import net.minecraft.client.Minecraft;
+
+/**
+ * Automatically presses the vanilla respawn path after death, once per death
+ * and only after a configurable delay.
+ */
+@ModuleInfo(
+ id = "autorespawn",
+ displayName = "Auto Respawn",
+ aliases = {"autorespawn", "respawn"},
+ category = ModuleCategory.PLAYER
+)
+public class AutoRespawn extends Module {
+ private static final String SETTING_DELAY = "delay";
+
+ private final NumberValue delay = num("autoRespawnDelay", SETTING_DELAY, 500, 0, 5000);
+
+ private final RespawnGate gate = new RespawnGate();
+
+ @Override
+ public void onTick() {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.player == null) {
+ gate.reset();
+ return;
+ }
+
+ boolean dead = mc.player.isDeadOrDying();
+ long now = GameClock.millis();
+ if (!gate.shouldRequestRespawn(dead, now, delay.get())) return;
+ gate.onRequestSent();
+
+ // Exactly what the vanilla death screen "Respawn" button does.
+ mc.player.respawn();
+ }
+
+ /**
+ * Pure death/delay state machine: one request per death, reset by coming
+ * back to life. Unit-testable without Minecraft.
+ */
+ public static final class RespawnGate {
+ private boolean dead;
+ private long deathStartedMs;
+ private boolean requestSent;
+
+ /**
+ * @return true when a respawn request should be sent right now
+ */
+ public boolean shouldRequestRespawn(boolean isDead, long nowMs, long delayMs) {
+ if (!isDead) {
+ reset();
+ return false;
+ }
+ if (!this.dead) {
+ this.dead = true;
+ this.deathStartedMs = nowMs;
+ this.requestSent = false;
+ return false;
+ }
+ if (requestSent) return false;
+ return nowMs - deathStartedMs >= Math.max(0L, delayMs);
+ }
+
+ public void onRequestSent() {
+ requestSent = true;
+ }
+
+ public void reset() {
+ dead = false;
+ requestSent = false;
+ deathStartedMs = 0L;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java
new file mode 100644
index 0000000..1d336f0
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java
@@ -0,0 +1,91 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.features.command.CommandOutput;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.features.module.Notifier;
+import net.minecraft.client.Minecraft;
+
+import java.util.Locale;
+
+/**
+ * Reports the position of your latest death as a client-side chat message and
+ * HUD notification, once per death.
+ */
+@ModuleInfo(
+ id = "deathcoords",
+ displayName = "Death Coords",
+ aliases = {"deathcoords"},
+ category = ModuleCategory.PLAYER
+)
+public class DeathCoords extends Module {
+ private static final String SETTING_SHOW_DIMENSION = "show_dimension";
+
+ private final BooleanValue showDimension =
+ bool("deathCoordsShowDimension", SETTING_SHOW_DIMENSION, true);
+
+ private final DeathDetector detector = new DeathDetector();
+
+ @Override
+ public void onDisable() {
+ detector.reset();
+ }
+
+ @Override
+ public void onTick() {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.player == null || mc.level == null) {
+ detector.reset();
+ return;
+ }
+
+ if (!detector.onTick(mc.player.isDeadOrDying())) return;
+
+ String message = formatDeathMessage(
+ mc.player.getX(),
+ mc.player.getY(),
+ mc.player.getZ(),
+ mc.level.dimension().identifier().toString(),
+ showDimension.get()
+ );
+ CommandOutput.warning(message);
+ Notifier.warning(message);
+ }
+
+ /**
+ * Pure alive-to-dead edge detector, unit-testable without Minecraft.
+ */
+ public static final class DeathDetector {
+ private boolean wasDead;
+
+ /**
+ * @return true exactly on the tick the player transitions to dead
+ */
+ public boolean onTick(boolean dead) {
+ boolean justDied = dead && !wasDead;
+ wasDead = dead;
+ return justDied;
+ }
+
+ public void reset() {
+ wasDead = false;
+ }
+ }
+
+ static String formatDeathMessage(double x, double y, double z, String dimension, boolean withDimension) {
+ String coords = String.format(Locale.ROOT, "%.2f, %.2f, %.2f", x, y, z);
+ if (withDimension && dimension != null && !dimension.isBlank()) {
+ return "You died at " + coords + " (" + dimension + ")";
+ }
+ return "You died at " + coords;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java
new file mode 100644
index 0000000..7ff96d6
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import dev.loki.lovisual.config.values.BooleanMapValue;
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.features.module.Notifier;
+import dev.loki.lovisual.util.protection.NotifyThrottle;
+import dev.loki.lovisual.util.protection.SlotLockPolicy;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.player.LocalPlayer;
+import net.minecraft.util.Util;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.level.Level;
+
+/**
+ * Protects hotbar slots 1-9 from accidental drops.
+ * Covers the Q drop of the selected hotbar slot and THROW clicks on the
+ * player's hotbar slots inside container screens.
+ */
+@ModuleInfo(
+ id = "lockslot",
+ displayName = "Lock Slot",
+ aliases = {"lockslot"},
+ category = ModuleCategory.PLAYER,
+ description = "module.lockslot.description"
+)
+public final class LockSlot extends Module {
+
+ private static final double NEARBY_RADIUS_SQR = 32.0 * 32.0;
+ private static final long WARN_INTERVAL_MS = 1500L;
+
+ private final BooleanMapValue slots = group("lockslot_slots", "slots", SlotLockPolicy.defaultSlotStates());
+ private final BooleanValue onlyPlayersNearby = bool("lockslot_only_players_nearby", "only_players_nearby", true);
+
+ private final NotifyThrottle warnThrottle = new NotifyThrottle(WARN_INTERVAL_MS);
+
+ /**
+ * Path A: Q / Ctrl+Q drop of the selected hotbar slot (LocalPlayer.drop).
+ */
+ public boolean shouldBlockSelectedDrop() {
+ if (!isEnabled()) return false;
+ LocalPlayer player = Minecraft.getInstance().player;
+ if (player == null) return false;
+ return shouldBlockDrop(player, player.getInventory().getSelectedSlot());
+ }
+
+ /**
+ * Path B: THROW container clicks targeting a hotbar slot in an open screen.
+ */
+ public boolean shouldBlockContainerThrow(Player player, int containerId, int slotId) {
+ if (!isEnabled() || player == null) return false;
+ if (player.containerMenu == null || player.containerMenu.containerId != containerId) return false;
+ if (slotId < 0 || slotId >= player.containerMenu.slots.size()) return false;
+ Slot slot = player.containerMenu.slots.get(slotId);
+ if (slot.container != player.getInventory()) return false;
+ return shouldBlockDrop(player, slot.getContainerSlot());
+ }
+
+ private boolean shouldBlockDrop(Player player, int hotbarSlot) {
+ boolean playersNearby = hasPlayersNearby(player);
+ if (!SlotLockPolicy.isDropBlocked(slots.getAll(), hotbarSlot, playersNearby, onlyPlayersNearby.get())) {
+ return false;
+ }
+ notifyBlocked(hotbarSlot);
+ return true;
+ }
+
+ private boolean hasPlayersNearby(Player self) {
+ Level level = self.level();
+ if (level == null) return false;
+ for (Player other : level.players()) {
+ if (other == self || other.isSpectator()) continue;
+ if (other.distanceToSqr(self) <= NEARBY_RADIUS_SQR) return true;
+ }
+ return false;
+ }
+
+ private void notifyBlocked(int hotbarSlot) {
+ if (!warnThrottle.tryAcquire(Util.getMillis())) return;
+ Notifier.warning("Slot " + (hotbarSlot + 1) + " is locked: drop blocked");
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java
new file mode 100644
index 0000000..53bca79
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.config.values.ModeValue;
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.mixins.accessors.KeyMappingAccessor;
+import dev.loki.lovisual.util.time.GameClock;
+import net.minecraft.client.KeyMapping;
+import net.minecraft.client.Minecraft;
+
+/**
+ * Repeats the selected mouse button on an interval, mirroring real clicks by
+ * feeding vanilla {@code KeyMapping.click(...)} so {@code handleKeybinds}
+ * processes them itself.
+ */
+@ModuleInfo(
+ id = "tapemouse",
+ displayName = "Tape Mouse",
+ aliases = {"tapemouse", "tape"},
+ category = ModuleCategory.PLAYER
+)
+public class TapeMouse extends Module {
+ private static final String SETTING_BUTTON = "button";
+ private static final String SETTING_INTERVAL = "interval";
+ private static final String SETTING_ONLY_HOLD = "only_hold";
+
+ private static final String BUTTON_ATTACK = "Attack";
+ private static final String BUTTON_USE = "Use";
+
+ private final ModeValue button = mode("tapeMouseButton", SETTING_BUTTON, BUTTON_ATTACK, BUTTON_USE);
+ private final NumberValue interval = num("tapeMouseInterval", SETTING_INTERVAL, 1000, 100, 5000);
+ private final BooleanValue onlyHold = bool("tapeMouseOnlyHold", SETTING_ONLY_HOLD, true);
+
+ private final ClickPacer pacer = new ClickPacer();
+
+ @Override
+ public void onEnable() {
+ pacer.reset();
+ }
+
+ @Override
+ public void onTick() {
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.player == null || mc.options == null) return;
+ if (mc.gui == null || mc.gui.screen() != null) return;
+
+ boolean attack = BUTTON_ATTACK.equals(button.get());
+ KeyMapping mapping = attack ? mc.options.keyAttack : mc.options.keyUse;
+ if (mapping == null) return;
+ if (onlyHold.get() && !mapping.isDown()) return;
+
+ long now = GameClock.millis();
+ if (!pacer.isDue(now, interval.get())) return;
+ pacer.mark(now);
+
+ // Identical to a physical press: vanilla MouseHandler calls the same
+ // static KeyMapping.click for real input events.
+ KeyMapping.click(((KeyMappingAccessor) (Object) mapping).lovisual$key());
+ }
+
+ /**
+ * Pure interval pacing logic, unit-testable without Minecraft.
+ */
+ public static final class ClickPacer {
+ private long lastClickMs;
+ private boolean primed;
+
+ public boolean isDue(long nowMs, long intervalMs) {
+ long safeInterval = Math.max(1L, intervalMs);
+ return !primed || nowMs - lastClickMs >= safeInterval;
+ }
+
+ public void mark(long nowMs) {
+ lastClickMs = nowMs;
+ primed = true;
+ }
+
+ public void reset() {
+ primed = false;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java
new file mode 100644
index 0000000..e9ce246
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java
@@ -0,0 +1,110 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.visuals;
+
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.util.animation.SmoothValue;
+import dev.loki.lovisual.util.screen.ClientScreen;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
+
+/**
+ * Smooth appear/disappear animations for interface elements.
+ *
+ * Every animated quantity is a {@link SmoothValue} whose boolean target is
+ * computed and advanced in exactly one place: {@link #onFrame(float)}. Renderer
+ * integration points (custom hotbar, container screens) only read the settled
+ * per-frame values through the accessors below, so values can never be pulled
+ * toward two different goals in the same frame.
+ */
+@ModuleInfo(
+ id = "animations",
+ displayName = "Animations",
+ aliases = {"animations"},
+ category = ModuleCategory.VISUALS,
+ description = "module.animations.description"
+)
+public final class Animations extends Module {
+
+ private static final String SETTING_HOTBAR_RAISE = "hotbar_raise";
+ private static final String SETTING_INVENTORY_OPEN = "inventory";
+
+ // Speeds are SmoothValue approach rates in 1/tick units (20 ticks per second).
+ // Inventory: ~0.15s to visually settle (0.9 -> ~1.0).
+ private static final float INVENTORY_OPEN_SPEED = 1.5f;
+ // Hotbar: softer slide-in, ~0.3s.
+ private static final float HOTBAR_RAISE_SPEED = 0.5f;
+
+ private static final float INVENTORY_MIN_SCALE = 0.9f;
+ private static final float HOTBAR_RAISE_OFFSET_PX = 26.0f;
+
+ private final BooleanValue hotbarRaise =
+ bool("animationsHotbarRaise", SETTING_HOTBAR_RAISE, true);
+ private final BooleanValue inventoryOpen =
+ bool("animationsInventoryOpen", SETTING_INVENTORY_OPEN, true);
+
+ private final SmoothValue hotbarRaiseValue = new SmoothValue(HOTBAR_RAISE_SPEED);
+ private final SmoothValue inventoryOpenValue = new SmoothValue(INVENTORY_OPEN_SPEED);
+
+ @Override
+ public void onEnable() {
+ Minecraft mc = Minecraft.getInstance();
+
+ // If a container screen is already open, start settled so enabling
+ // mid-game never pops the screen. Otherwise the open animation plays
+ // naturally the next time a screen appears.
+ boolean containerOpen = ClientScreen.current(mc) instanceof AbstractContainerScreen>;
+ inventoryOpenValue.setInstant(containerOpen);
+
+ // Reset the raise so the hotbar slides in when the module turns on.
+ hotbarRaiseValue.setInstant(false);
+ }
+
+ /**
+ * The single update point for every animated value: compute the boolean
+ * target from the current game state, then advance each value exactly once.
+ */
+ @Override
+ public void onFrame(float delta) {
+ Minecraft mc = Minecraft.getInstance();
+ boolean inWorld = mc != null && mc.player != null;
+ boolean containerOpen = inWorld
+ && ClientScreen.current(mc) instanceof AbstractContainerScreen>;
+
+ hotbarRaiseValue.setTarget(inWorld);
+ hotbarRaiseValue.update(delta);
+
+ inventoryOpenValue.setTarget(containerOpen);
+ inventoryOpenValue.update(delta);
+ }
+
+ /**
+ * Vertical GUI-pixel offset the custom hotbar should be shifted down by while
+ * it slides into place. Reads only; the value is updated in {@link #onFrame(float)}.
+ */
+ public float getHotbarRaiseOffset() {
+ if (!isEnabled() || !hotbarRaise.get()) {
+ return 0.0f;
+ }
+ return HOTBAR_RAISE_OFFSET_PX * (1.0f - hotbarRaiseValue.value());
+ }
+
+ /**
+ * Scale container screens should be drawn with while they open
+ * (1.0 when settled or when the animation is disabled).
+ */
+ public float getInventoryScale() {
+ if (!isEnabled() || !inventoryOpen.get()) {
+ return 1.0f;
+ }
+ return INVENTORY_MIN_SCALE + (1.0f - INVENTORY_MIN_SCALE) * inventoryOpenValue.value();
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java
new file mode 100644
index 0000000..a97d291
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java
@@ -0,0 +1,145 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.visuals;
+
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.config.values.RGBColorValue;
+import dev.loki.lovisual.config.values.ModeValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.features.module.Modules;
+import net.minecraft.client.Camera;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.renderer.fog.FogData;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.material.FogType;
+
+/**
+ * Атмосферный туман зари: плотность тумана и цвет неба зависят от режима
+ * (рассвет/сумерки), времени суток, высоты камеры и мягкости градиента.
+ * В отличие от референса здесь эффект реально применяется к FogData и цвету.
+ */
+@ModuleInfo(
+ id = "atmodawnfog",
+ displayName = "AtmoDawnFog",
+ aliases = {"atmo", "dawnfog"},
+ category = ModuleCategory.VISUALS,
+ description = "module.atmodawnfog.description"
+)
+public final class AtmoDawnFog extends Module {
+
+ private static final float TIME_WINDOW = 0.1f;
+ private static final float ALTITUDE_FADE = 32.0f;
+
+ private final ModeValue mode =
+ modeSetting("atmoDawnFogMode", "mode", "Dawn", "Dawn", "Dusk", "Custom");
+ private final NumberValue density =
+ num("atmoDawnFogDensity", "density", 0.35f, 0.05f, 0.8f);
+ private final NumberValue scatterHeight =
+ num("atmoDawnFogScatterHeight", "scatter_height", 76.0f, 60.0f, 120.0f);
+ private final NumberValue softness =
+ num("atmoDawnFogSoftness", "softness", 0.6f, 0.0f, 1.0f);
+ private final RGBColorValue dawnColor =
+ visibleWhen(colorNoAlpha("atmoDawnFogColor", "dawn_color", "#FFAD7A"), () -> "Custom".equals(mode.get()));
+
+ private static int dawnArgb() {
+ return 0xFFFFAD7A;
+ }
+
+ private static int duskArgb() {
+ return 0xFF5C6BFF;
+ }
+
+ /** Сила эффекта 0..1: время суток, режим и высота камеры. */
+ public float strength(double cameraY) {
+ AtmoDawnFog module = Modules.get(AtmoDawnFog.class);
+ if (module == null || !module.isEnabled()) return 0.0f;
+
+ float timeOfDay = (WorldTweaks.getServerTimeOfDay() % 24000L) / 24000.0f;
+ return strengthFor(timeOfDay, module.mode.get(), cameraY, module.scatterHeight.get());
+ }
+
+ /** Чистая математика силы эффекта — вынесена для тестов. */
+ static float strengthFor(float timeOfDay, String mode, double cameraY, float scatterHeight) {
+ float byMode = switch (mode) {
+ case "Dusk" -> timeCloseness(timeOfDay, 0.5f);
+ case "Custom" -> 1.0f;
+ default -> timeCloseness(timeOfDay, 0.0f);
+ };
+ if (byMode <= 0.0f) return 0.0f;
+
+ float altitude = 1.0f - (float) Math.max(0.0, cameraY - scatterHeight) / ALTITUDE_FADE;
+ return byMode * Math.max(0.0f, Math.min(1.0f, altitude));
+ }
+
+ private static float timeCloseness(float timeOfDay, float target) {
+ float delta = Math.abs(timeOfDay - target);
+ delta = Math.min(delta, 1.0f - delta);
+ return Math.max(0.0f, 1.0f - delta / TIME_WINDOW);
+ }
+
+ /**
+ * Применяет туман к FogData. Вызывается из миксина до WorldTweaks-веток;
+ * false — эффекта нет в этом кадре.
+ */
+ public static boolean applyFog(FogData data, Camera camera, ClientLevel world,
+ float viewDistance, float tickDelta) {
+ AtmoDawnFog module = Modules.get(AtmoDawnFog.class);
+ if (module == null || !module.isEnabled() || data == null || world == null) return false;
+ if (world.dimension() != Level.OVERWORLD) return false;
+ if (camera.getFluidInCamera() != FogType.NONE) return false;
+
+ WorldTweaks tweaks = Modules.get(WorldTweaks.class);
+ if (tweaks != null && tweaks.isFogModifyEnabled()) return false;
+
+ float strength = module.strength(camera.position().y);
+ if (strength <= 0.0f) return false;
+
+ float density = module.density.get();
+ float far = Math.max(viewDistance * (1.0f - 0.75f * density * strength), viewDistance * 0.15f);
+ float start = far * Math.max(0.05f, module.softness.get());
+
+ data.environmentalStart = start;
+ data.environmentalEnd = far;
+ data.renderDistanceStart = start;
+ data.renderDistanceEnd = far;
+ data.skyEnd = far;
+ data.cloudEnd = far;
+ return true;
+ }
+
+ /** Подмешивает цвет зари в туман; true — цвет уже изменён. */
+ public static boolean tintColor(org.joml.Vector4f color, ClientLevel world, double cameraY) {
+ AtmoDawnFog module = Modules.get(AtmoDawnFog.class);
+ if (module == null || !module.isEnabled() || color == null || world == null) return false;
+
+ WorldTweaks tweaks = Modules.get(WorldTweaks.class);
+ if (tweaks != null && tweaks.isFogModifyEnabled()) return false;
+
+ float strength = module.strength(cameraY) * 0.8f;
+ if (strength <= 0.0f) return false;
+
+ int argb = switch (module.mode.get()) {
+ case "Dusk" -> duskArgb();
+ case "Custom" -> module.dawnColor.getArgb();
+ default -> dawnArgb();
+ };
+ float r = ((argb >> 16) & 0xFF) / 255.0f;
+ float g = ((argb >> 8) & 0xFF) / 255.0f;
+ float b = (argb & 0xFF) / 255.0f;
+
+ color.set(
+ color.x + (r - color.x) * strength,
+ color.y + (g - color.y) * strength,
+ color.z + (b - color.z) * strength,
+ color.w
+ );
+ return true;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java
index f8387e4..17e5137 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java
@@ -43,6 +43,7 @@ import dev.loki.lovisual.render.engine.uniform.impl.HandBlendUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandChamsUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandGlassUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandMetallicUniforms;
+import dev.loki.lovisual.render.engine.uniform.impl.HandNoiseUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandSmokeUniforms;
import dev.loki.lovisual.render.iris.IrisHandMaskState;
import dev.loki.lovisual.render.iris.IrisRuntime;
@@ -73,6 +74,7 @@ public class Chams extends Module {
private static final String SETTING_SMOKE_CONTRAST = "smoke_contrast";
private static final String SETTING_SMOKE_SWIRL = "smoke_swirl";
private static final String SETTING_SMOKE_DENSITY = "smoke_density";
+ private static final String SETTING_NOISE_ALPHA = "noise_alpha";
private static final String SETTING_METALLIC_BASE = "metallic_base";
private static final String SETTING_METALLIC_HIGHLIGHT = "metallic_highlight";
private static final String SETTING_METALLIC_INTENSITY = "metallic_intensity";
@@ -114,9 +116,16 @@ public class Chams extends Module {
private static final String SETTING_BALATRO_CONTRAST = "balatro_contrast";
private static final String SETTING_BALATRO_LIGHTING = "balatro_lighting";
private static final String SETTING_HIDE_HAND = "hide_hand";
+ // Noise mode tuning; intentionally fixed so the mode stays zero-config besides color/alpha.
+ private static final float NOISE_OCTAVES = 3.0f;
+ private static final float NOISE_SCALE = 3.4f;
+ private static final float NOISE_SPEED = 0.85f;
+ private static final float NOISE_CONTRAST = 1.15f;
+ private static final float NOISE_BREATHE = 0.55f;
+ private static final float NOISE_DENSITY = 1.0f;
private final Minecraft mc = Minecraft.getInstance();
private final ModeValue mode =
- modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro");
+ modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro");
private final BooleanValue hands =
bool("chamsHands", SETTING_HANDS, true);
@@ -164,6 +173,8 @@ public class Chams extends Module {
visibleWhen(num("handChamsSmokeSwirl", SETTING_SMOKE_SWIRL, 0.85f, 0.0f, 2.5f), this::isSmokeMode);
private final NumberValue smokeDensity =
visibleWhen(num("handChamsSmokeDensity", SETTING_SMOKE_DENSITY, 1.0f, 0.2f, 2.5f), this::isSmokeMode);
+ private final NumberValue noiseAlpha =
+ visibleWhen(num("handChamsNoiseAlpha", SETTING_NOISE_ALPHA, 0.85f, 0.0f, 1.0f), this::isNoiseMode);
private final NumberValue metallicIntensity =
visibleWhen(num("handChamsMetallicIntensity", SETTING_METALLIC_INTENSITY, 1.35f, 0.0f, 5.0f), this::isMetallicMode);
private final NumberValue metallicSharpness =
@@ -548,8 +559,12 @@ public class Chams extends Module {
return "Metallic".equals(mode.get());
}
+ private boolean isNoiseMode() {
+ return "Noise".equals(mode.get());
+ }
+
private boolean isHandEffectMode() {
- return isSmokeMode() || isMetallicMode();
+ return isSmokeMode() || isMetallicMode() || isNoiseMode();
}
private boolean isGlowVisible() {
@@ -766,6 +781,8 @@ public class Chams extends Module {
.sampler("u_Src", src, PostProcessManager.getSampler())
.sampler("u_Mask", maskView, getHandMaskSampler())
.end();
+ } else if ("Noise".equals(mode.get())) {
+ renderNoise(src, dst, maskView, rawTime);
} else {
renderChamsEffect(src, dst, maskView, mode.get(), rawTime);
}
@@ -844,6 +861,33 @@ public class Chams extends Module {
.end();
}
+ private void renderNoise(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) {
+ int fillRgb = materialFillRgb();
+ int glowRgb = glowRgb(fillRgb);
+
+ float fillA = hideHand.get() ? 1.0f : clamp01(noiseAlpha.get());
+ float glowA = glow.get() ? glowAlpha.get() : 0.0f;
+
+ int w = mc.getWindow().getWidth();
+ int h = mc.getWindow().getHeight();
+
+ HandNoiseUniforms.update(
+ channel(fillRgb, 16), channel(fillRgb, 8), channel(fillRgb, 0), fillA,
+ channel(glowRgb, 16), channel(glowRgb, 8), channel(glowRgb, 0), glowA,
+ edgeWidth.get(), quality.get(), NOISE_OCTAVES, rawTime,
+ w, h, NOISE_SCALE, NOISE_SPEED,
+ NOISE_CONTRAST, NOISE_BREATHE, glowStrength.get(), NOISE_DENSITY
+ );
+
+ FullScreenRenderer.begin("LoVisual Fullscreen Pass")
+ .attachment(dst)
+ .pipeline(LoVisualRenderPipelines.HAND_NOISE)
+ .uniform("HandNoise", HandNoiseUniforms.get())
+ .sampler("u_Src", src, PostProcessManager.getSampler())
+ .sampler("u_Mask", maskView, getHandMaskSampler())
+ .end();
+ }
+
private void renderDualShader(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) {
ensureDualBuffers();
if (dualA == null || dualB == null
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java
index fad98d7..fd0ee0a 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java
@@ -12,11 +12,18 @@ import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.util.animation.EaseCurves;
+import dev.loki.lovisual.util.animation.hand.HandAnimState;
+import dev.loki.lovisual.render.helpers.TickDelta;
import net.minecraft.client.Minecraft;
+import net.minecraft.client.player.LocalPlayer;
+import net.minecraft.world.InteractionHand;
/**
- * Smooths and adds a subtle hand-held sway to first-person item rendering, so weapons and tools
- * settle into place with an ease-out-back curve instead of vanilla's linear equip motion.
+ * Плавная анимация предмета в руке: ease-out-back equip, sway по обзору,
+ * боб при ходьбе, дрейф при использовании. Состояние шагает ровно раз за
+ * кадр в {@link #onFrame} — вне рендер-пути, поэтому повторный рендер рук
+ * (маска Chams) всегда видит те же значения, что и основной проход.
*/
@ModuleInfo(
id = "holdmyitems",
@@ -27,30 +34,44 @@ import net.minecraft.client.Minecraft;
)
public final class HoldMyItems extends Module {
- public final Minecraft mc = Minecraft.getInstance();
+ private final HandAnimState handAnim = new HandAnimState();
public final NumberValue swayIntensity = num("sway_intensity", 1.0f, 0.0f, 2.0f);
public final NumberValue easeStrength = num("ease_strength", 1.6f, 0.5f, 3.5f);
public final BooleanValue offhandToo = bool("offhand", true);
+ public final BooleanValue walkBob = bool("walk_bob", true);
+ public final NumberValue bobStrength = visibleWhen(num("bob_strength", 1.0f, 0.0f, 2.0f), walkBob::get);
+ public final BooleanValue useDrift = bool("use_drift", true);
+ public final NumberValue driftStrength = visibleWhen(num("drift_strength", 1.0f, 0.0f, 2.0f), useDrift::get);
+ public final BooleanValue joinAppear = bool("join_appear", true);
- /**
- * Ease-out-back: overshoots slightly past 1.0 before settling, giving the equip motion weight.
- * Generalized with a configurable strength.
- */
- public float easeOutBack(float x) {
- float c1 = easeStrength.get();
- float c3 = c1 + 1.0f;
- float t = x - 1.0f;
- return 1.0f + c3 * t * t * t + c1 * t * t;
+ public HandAnimState handAnim() {
+ return handAnim;
}
- /**
- * Reshapes the raw [0,1] equip progress with the ease-out-back curve, then blends by intensity
- * so a intensity of 0 falls back to vanilla's linear motion.
- */
+ @Override
+ public void onFrame(float tickDelta) {
+ if (!isEnabled()) return;
+ LocalPlayer player = Minecraft.getInstance().player;
+ if (player == null) return;
+ handAnim.beginFrame(
+ player,
+ Minecraft.getInstance().level,
+ player.getViewYRot(tickDelta),
+ player.getViewXRot(tickDelta),
+ player.isUsingItem() && player.getUsedItemHand() == InteractionHand.MAIN_HAND,
+ player.isUsingItem() && player.getUsedItemHand() == InteractionHand.OFF_HAND,
+ TickDelta.fixedDeltaTicks()
+ );
+ }
+
+ @Override
+ public void onEnable() {
+ handAnim.reset();
+ }
+
+ /** Перекладывает линейный equip-прогресс через ease-out-back с настройками модуля. */
public float shapeEquipProgress(float linear) {
- float eased = easeOutBack(linear);
- float amount = swayIntensity.get();
- return linear + (eased - linear) * amount;
+ return EaseCurves.shapeEquipProgress(linear, easeStrength.get(), swayIntensity.get());
}
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java
index 0a28949..54bcd27 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java
@@ -7,23 +7,15 @@
package dev.loki.lovisual.features.module.modules.visuals;
-import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.NumberValue;
-import dev.loki.lovisual.config.values.RGBAColorValue;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
-import dev.loki.lovisual.features.module.WorldPhase;
-import dev.loki.lovisual.features.relations.PlayerRelations;
-import dev.loki.lovisual.render.engine.RenderState;
-import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
-import dev.loki.lovisual.render.engine.renderer.Renderer3D;
-import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
-import net.minecraft.client.Minecraft;
-import net.minecraft.world.effect.MobEffects;
-import net.minecraft.world.entity.player.Player;
-import net.minecraft.world.phys.AABB;
+/**
+ * Делает невидимых игроков видимыми: модель рендерится полупрозрачной,
+ * сила прозрачности регулируется слайдером.
+ */
@ModuleInfo(
id = "invisesp",
displayName = "InvisESP",
@@ -33,55 +25,19 @@ import net.minecraft.world.phys.AABB;
)
public final class InvisEsp extends Module {
- private final Minecraft mc = Minecraft.getInstance();
-
- private final RGBAColorValue outlineColor =
- color("invisesp_color", "color", "#FF00E5FF");
- private final BooleanValue highlightFriends =
- bool("invisesp_highlight_friends", "highlight_friends", true);
- private final RGBAColorValue friendColor =
- visibleWhen(color("invisesp_friend_color", "friend_color", "#FF00FF00"), highlightFriends::get);
- private final NumberValue lineWidth =
- num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f);
+ private final NumberValue opacity =
+ num("invisesp_opacity", "opacity", 0.5f, 0.1f, 1.0f);
{
setDefaultBind("L");
}
- @Override
- public WorldPhase getWorldPhase() {
- return WorldPhase.END_MAIN;
+ public float getOpacity() {
+ return opacity.get();
}
- @Override
- public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
- if (!isEnabled() || mc.level == null || mc.player == null) return;
-
- float previousLineWidth = RenderState.lineWidth;
- MeshBuilder lines;
- try {
- RenderState.lineWidth = lineWidth.get();
- lines = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
- } finally {
- RenderState.lineWidth = previousLineWidth;
- }
-
- int defaultColor = outlineColor.getArgb();
- boolean highlightFriendsEnabled = highlightFriends.get();
- for (Player player : mc.level.players()) {
- if (player == mc.player) continue;
- if (!isInvisible(player)) continue;
-
- int color = highlightFriendsEnabled && PlayerRelations.get().isFriendOrStaff(player.getGameProfile().name())
- ? friendColor.getArgb()
- : defaultColor;
-
- AABB box = Esp2DRenderer.resolveWorldBox(player, tickDelta);
- EspMeshBuilder.addOutlineBox(lines, box, color, color, color, color, color, color, color, color);
- }
- }
-
- private boolean isInvisible(Player player) {
- return player.isInvisible() || player.hasEffect(MobEffects.INVISIBILITY);
+ /** Прозрачный рендер имеет смысл только ниже полной непрозрачности. */
+ public boolean isTranslucentReveal() {
+ return opacity.get() < 0.99f;
}
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java
new file mode 100644
index 0000000..46bb43f
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.visuals;
+
+import dev.loki.lovisual.config.values.BooleanValue;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+
+/**
+ * Физика предметов на земле: предметы лежат плашмя, крутятся в воздухе,
+ * без ваниллового покачивания; опционально уменьшенный размер.
+ */
+@ModuleInfo(
+ id = "itemphysic",
+ displayName = "Item Physic",
+ aliases = {"itemphysic", "physics"},
+ category = ModuleCategory.VISUALS,
+ description = "module.itemphysic.description"
+)
+public final class ItemPhysic extends Module {
+
+ private final BooleanValue reduceSize =
+ bool("itemphysicReduceSize", "reduce_size", false);
+ private final BooleanValue layFlat =
+ bool("itemphysicLayFlat", "lay_flat", true);
+ private final BooleanValue spinInAir =
+ bool("itemphysicSpinInAir", "spin_in_air", true);
+ private final BooleanValue noBob =
+ bool("itemphysicNoBob", "no_bob", true);
+
+ public boolean shouldReduceSize() {
+ return isEnabled() && reduceSize.get();
+ }
+
+ public boolean shouldLayFlat() {
+ return isEnabled() && layFlat.get();
+ }
+
+ public boolean shouldSpinInAir() {
+ return isEnabled() && spinInAir.get();
+ }
+
+ public boolean shouldCancelBob() {
+ return isEnabled() && noBob.get();
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java
index 9c37bd0..3868c6c 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java
@@ -54,7 +54,7 @@ public class PostFX extends Module implements PostProcessPass {
private final Minecraft mc = Minecraft.getInstance();
private final ModeValue preset =
- modeSetting("postFxPreset", SETTING_PRESET, "Vanilla", "Vanilla", "Soft", "Warm", "Cool", "Custom");
+ modeSetting("postFxPreset", SETTING_PRESET, "Cinematic", "Cinematic", "Vanilla", "Soft", "Warm", "Cool", "Custom");
private final ModeValue lutPreset =
visibleWhen(modeSetting("postFxLutPreset", SETTING_LUT_PRESET, "Identity", "Identity", "Warm", "Cool"), this::isCustomPreset);
@@ -221,7 +221,7 @@ public class PostFX extends Module implements PostProcessPass {
private float resolveLutMode(String presetName, boolean isCustom) {
String pick = isCustom ? lutPreset.get() : presetName;
return switch (pick) {
- case "Warm" -> 1.0f;
+ case "Warm", "Cinematic" -> 1.0f;
case "Cool" -> 2.0f;
default -> 0.0f;
};
@@ -236,100 +236,135 @@ public class PostFX extends Module implements PostProcessPass {
}
private void applyPreset(String name) {
- String presetName = name == null ? "Vanilla" : name;
+ applyPresetTo(name, ubo);
+ }
+
+ /** Чистое заполнение UBO пресетом — вынесено для тестов. */
+ static void applyPresetTo(String name, float[] out) {
+ String presetName = name == null ? "Cinematic" : name;
switch (presetName) {
- case "Soft" -> fillPresetSoft();
- case "Warm" -> fillPresetWarm();
- case "Cool" -> fillPresetCool();
- default -> fillPresetVanilla();
+ case "Vanilla" -> fillPresetVanilla(out);
+ case "Warm" -> fillPresetWarm(out);
+ case "Cool" -> fillPresetCool(out);
+ case "Cinematic" -> fillPresetCinematic(out);
+ default -> fillPresetSoft(out);
}
}
- private void fillPresetVanilla() {
- ubo[0] = 0.0f; // exposure
- ubo[1] = 1.0f; // gamma
- ubo[2] = 1.0f; // contrast
- ubo[3] = 1.0f; // saturation
- ubo[4] = 1.0f; // vibrance
- ubo[5] = 0.0f; // temp
- ubo[6] = 0.0f; // tint
- ubo[7] = 0.0f; // lut strength
- ubo[8] = 0.08f; // vignette strength
- ubo[9] = 0.55f; // vignette radius
- ubo[10] = 0.35f; // vignette softness
- ubo[11] = 0.0f; // chroma
- ubo[12] = 0.0f; // grain amount
- ubo[13] = 1.0f; // grain size
- ubo[14] = 0.05f; // sharpen
- ubo[15] = 0.0f; // tone map none
- fillColor(ubo, 16, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 20, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 24, 0xFFFFFFFF, 0.0f);
+ private static void setTint(float[] out, int offset, float r, float g, float b, float strength) {
+ out[offset] = r;
+ out[offset + 1] = g;
+ out[offset + 2] = b;
+ out[offset + 3] = strength;
}
- private void fillPresetSoft() {
- ubo[0] = 0.0f;
- ubo[1] = 1.0f;
- ubo[2] = 1.0f;
- ubo[3] = 1.01f;
- ubo[4] = 1.0f;
- ubo[5] = 0.0f;
- ubo[6] = 0.0f;
- ubo[7] = 0.18f;
- ubo[8] = 0.08f;
- ubo[9] = 0.55f;
- ubo[10] = 0.35f;
- ubo[11] = 0.01f;
- ubo[12] = 0.0f;
- ubo[13] = 1.0f;
- ubo[14] = 0.05f;
- ubo[15] = 0.0f;
- fillColor(ubo, 16, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 20, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 24, 0xFFFFFFFF, 0.0f);
+ private static void fillPresetCinematic(float[] out) {
+ out[0] = 0.02f; // exposure
+ out[1] = 1.0f; // gamma
+ out[2] = 1.10f; // contrast
+ out[3] = 1.14f; // saturation
+ out[4] = 1.08f; // vibrance
+ out[5] = 0.06f; // temp (теплее)
+ out[6] = 0.0f; // tint
+ out[7] = 0.30f; // LUT strength (warm)
+ out[8] = 0.18f; // vignette strength
+ out[9] = 0.55f; // vignette radius
+ out[10] = 0.35f; // vignette softness
+ out[11] = 0.0f; // chroma
+ out[12] = 0.0f; // grain
+ out[13] = 1.0f; // grain size
+ out[14] = 0.10f; // sharpen
+ out[15] = 1.0f; // tone map: filmic
+ setTint(out, 16, 0.72f, 0.80f, 1.0f, 0.22f); // тени — холодные
+ setTint(out, 20, 1.0f, 1.0f, 1.0f, 0.0f); // полутона — нейтральные
+ setTint(out, 24, 1.0f, 0.93f, 0.80f, 0.18f); // света — тёплые
}
- private void fillPresetWarm() {
- ubo[0] = 0.0f;
- ubo[1] = 1.0f;
- ubo[2] = 1.01f;
- ubo[3] = 1.02f;
- ubo[4] = 1.01f;
- ubo[5] = 0.03f;
- ubo[6] = 0.01f;
- ubo[7] = 0.18f;
- ubo[8] = 0.08f;
- ubo[9] = 0.55f;
- ubo[10] = 0.35f;
- ubo[11] = 0.01f;
- ubo[12] = 0.0f;
- ubo[13] = 1.0f;
- ubo[14] = 0.05f;
- ubo[15] = 0.0f;
- fillColor(ubo, 16, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 20, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 24, 0xFFFFFFFF, 0.0f);
+ private static void fillPresetVanilla(float[] out) {
+ out[0] = 0.0f; // exposure
+ out[1] = 1.0f; // gamma
+ out[2] = 1.0f; // contrast
+ out[3] = 1.0f; // saturation
+ out[4] = 1.0f; // vibrance
+ out[5] = 0.0f; // temp
+ out[6] = 0.0f; // tint
+ out[7] = 0.0f; // lut strength
+ out[8] = 0.08f; // vignette strength
+ out[9] = 0.55f; // vignette radius
+ out[10] = 0.35f; // vignette softness
+ out[11] = 0.0f; // chroma
+ out[12] = 0.0f; // grain amount
+ out[13] = 1.0f; // grain size
+ out[14] = 0.05f; // sharpen
+ out[15] = 0.0f; // tone map none
+ setTint(out, 16, 1f, 1f, 1f, 0.0f);
+ setTint(out, 20, 1f, 1f, 1f, 0.0f);
+ setTint(out, 24, 1f, 1f, 1f, 0.0f);
}
- private void fillPresetCool() {
- ubo[0] = 0.0f;
- ubo[1] = 1.0f;
- ubo[2] = 1.01f;
- ubo[3] = 1.0f;
- ubo[4] = 1.01f;
- ubo[5] = -0.04f;
- ubo[6] = 0.0f;
- ubo[7] = 0.18f;
- ubo[8] = 0.08f;
- ubo[9] = 0.55f;
- ubo[10] = 0.35f;
- ubo[11] = 0.01f;
- ubo[12] = 0.0f;
- ubo[13] = 1.0f;
- ubo[14] = 0.05f;
- ubo[15] = 0.0f;
- fillColor(ubo, 16, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 20, 0xFFFFFFFF, 0.0f);
- fillColor(ubo, 24, 0xFFFFFFFF, 0.0f);
+ private static void fillPresetSoft(float[] out) {
+ out[0] = 0.0f;
+ out[1] = 1.0f;
+ out[2] = 1.01f;
+ out[3] = 1.01f;
+ out[4] = 1.0f;
+ out[5] = 0.0f;
+ out[6] = 0.0f;
+ out[7] = 0.18f;
+ out[8] = 0.08f;
+ out[9] = 0.55f;
+ out[10] = 0.35f;
+ out[11] = 0.01f;
+ out[12] = 0.0f;
+ out[13] = 1.0f;
+ out[14] = 0.05f;
+ out[15] = 0.0f;
+ setTint(out, 16, 1f, 1f, 1f, 0.0f);
+ setTint(out, 20, 1f, 1f, 1f, 0.0f);
+ setTint(out, 24, 1f, 1f, 1f, 0.0f);
+ }
+
+ private static void fillPresetWarm(float[] out) {
+ out[0] = 0.0f;
+ out[1] = 1.0f;
+ out[2] = 1.01f;
+ out[3] = 1.02f;
+ out[4] = 1.01f;
+ out[5] = 0.03f;
+ out[6] = 0.01f;
+ out[7] = 0.18f;
+ out[8] = 0.08f;
+ out[9] = 0.55f;
+ out[10] = 0.35f;
+ out[11] = 0.01f;
+ out[12] = 0.0f;
+ out[13] = 1.0f;
+ out[14] = 0.05f;
+ out[15] = 0.0f;
+ setTint(out, 16, 1f, 1f, 1f, 0.0f);
+ setTint(out, 20, 1f, 1f, 1f, 0.0f);
+ setTint(out, 24, 1f, 1f, 1f, 0.0f);
+ }
+
+ private static void fillPresetCool(float[] out) {
+ out[0] = 0.0f;
+ out[1] = 1.0f;
+ out[2] = 1.01f;
+ out[3] = 1.0f;
+ out[4] = 1.01f;
+ out[5] = -0.04f;
+ out[6] = 0.0f;
+ out[7] = 0.18f;
+ out[8] = 0.08f;
+ out[9] = 0.55f;
+ out[10] = 0.35f;
+ out[11] = 0.01f;
+ out[12] = 0.0f;
+ out[13] = 1.0f;
+ out[14] = 0.05f;
+ out[15] = 0.0f;
+ setTint(out, 16, 1f, 1f, 1f, 0.0f);
+ setTint(out, 20, 1f, 1f, 1f, 0.0f);
+ setTint(out, 24, 1f, 1f, 1f, 0.0f);
}
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java
new file mode 100644
index 0000000..4cff9ae
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java
@@ -0,0 +1,154 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.visuals;
+
+import dev.loki.lovisual.config.values.NumberValue;
+import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
+import dev.loki.lovisual.features.module.Module;
+import dev.loki.lovisual.features.module.ModuleCategory;
+import dev.loki.lovisual.features.module.ModuleInfo;
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.theme.Theme;
+import dev.loki.lovisual.features.theme.Themes;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.GuiGraphicsExtractor;
+import net.minecraft.client.renderer.RenderPipelines;
+import net.minecraft.core.NonNullList;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.util.Mth;
+import net.minecraft.world.item.BlockItem;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.ItemContainerContents;
+import net.minecraft.world.level.block.ShulkerBoxBlock;
+
+@ModuleInfo(
+ id = "shulkerpreview",
+ displayName = "Shulker Preview",
+ aliases = {"shulkerpreview", "preview"},
+ category = ModuleCategory.VISUALS,
+ description = "module.shulkerpreview.description"
+)
+public final class ShulkerPreview extends Module {
+
+ private static final int COLS = 9;
+ private static final int ROWS = 3;
+ private static final int SLOT_SIZE = 18;
+ private static final int PAD = 4;
+ private static final int HEADER_HEIGHT = 10;
+ private static final int HEADER_GAP = 3;
+ private static final int CURSOR_OFFSET = 14;
+ private static final int BG_ALPHA = 0xE6;
+ private static final int STROKE_ALPHA = 0xDC;
+ private static final int CELL_ALPHA = 0x8C;
+
+ private final NumberValue scale =
+ num("shulkerpreview_scale", "scale", 1.0f, 0.5f, 2.0f);
+
+ /**
+ * Called from AbstractContainerScreenMixin at the end of screen extraction.
+ */
+ public static void onScreenExtract(GuiGraphicsExtractor ctx, ItemStack stack, int mouseX, int mouseY) {
+ ShulkerPreview module = Modules.get(ShulkerPreview.class);
+ if (module == null || !module.isEnabled()) return;
+ module.drawPreview(ctx, stack, mouseX, mouseY);
+ }
+
+ private void drawPreview(GuiGraphicsExtractor ctx, ItemStack stack, int mouseX, int mouseY) {
+ if (ctx == null || !isShulkerBox(stack)) return;
+
+ NonNullList contents = collectContents(stack);
+ if (contents == null) return;
+
+ Minecraft mc = Minecraft.getInstance();
+ if (mc == null || mc.font == null) return;
+
+ float panelScale = Mth.clamp(scale.get(), 0.5f, 2.0f);
+ int width = PAD * 2 + COLS * SLOT_SIZE;
+ int height = PAD * 2 + HEADER_HEIGHT + HEADER_GAP + ROWS * SLOT_SIZE;
+
+ int x = horizontalOrigin(mouseX, width * panelScale, ctx.guiWidth());
+ int y = verticalOrigin(mouseY, height * panelScale, ctx.guiHeight());
+
+ Themes.Theme theme = Theme.theme();
+ int bg = HudRenderUtil.setAlpha(theme.windowBg(), BG_ALPHA);
+ int stroke = HudRenderUtil.setAlpha(
+ HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.34f),
+ STROKE_ALPHA);
+ int headerColor = HudRenderUtil.setAlpha(theme.textPrimary(), 0xFF);
+ int cellColor = HudRenderUtil.setAlpha(theme.surface(), CELL_ALPHA);
+
+ Font font = mc.font;
+ ctx.pose().pushMatrix();
+ ctx.pose().translate(x, y);
+ ctx.pose().scale(panelScale, panelScale);
+ try {
+ ctx.fill(RenderPipelines.GUI, 0, 0, width, height, bg);
+ ctx.outline(0, 0, width, height, stroke);
+ ctx.text(font, stack.getHoverName(), PAD, PAD, headerColor);
+
+ int gridY = PAD + HEADER_HEIGHT + HEADER_GAP;
+ for (int row = 0; row < ROWS; row++) {
+ for (int col = 0; col < COLS; col++) {
+ int cellX = PAD + col * SLOT_SIZE;
+ int cellY = gridY + row * SLOT_SIZE;
+ ctx.fill(RenderPipelines.GUI,
+ cellX, cellY, cellX + SLOT_SIZE, cellY + SLOT_SIZE,
+ cellColor);
+
+ ItemStack item = contents.get(row * COLS + col);
+ if (item == null || item.isEmpty()) continue;
+ ctx.item(item, cellX + 1, cellY + 1);
+ ctx.itemDecorations(font, item, cellX + 1, cellY + 1);
+ }
+ }
+ } finally {
+ ctx.pose().popMatrix();
+ }
+ }
+
+ private static NonNullList collectContents(ItemStack stack) {
+ ItemContainerContents container = stack.get(DataComponents.CONTAINER);
+ if (container == null) return null;
+
+ NonNullList contents = NonNullList.withSize(COLS * ROWS, ItemStack.EMPTY);
+ container.copyInto(contents);
+
+ for (ItemStack item : contents) {
+ if (item != null && !item.isEmpty()) {
+ return contents;
+ }
+ }
+ return null;
+ }
+
+ private static boolean isShulkerBox(ItemStack stack) {
+ if (stack == null || stack.isEmpty()) return false;
+ if (!(stack.getItem() instanceof BlockItem blockItem)) return false;
+ return blockItem.getBlock() instanceof ShulkerBoxBlock;
+ }
+
+ private static int horizontalOrigin(int mouseX, float width, int bounds) {
+ int right = (int) (mouseX + CURSOR_OFFSET);
+ if (right + width <= bounds) return right;
+
+ int left = (int) (mouseX - CURSOR_OFFSET - width);
+ if (left >= 0) return left;
+
+ int clamped = (int) (bounds - width);
+ return Math.max(0, Math.min(right, clamped));
+ }
+
+ private static int verticalOrigin(int mouseY, float height, int bounds) {
+ int top = (int) (mouseY - height / 2f);
+ if (top + height >= bounds) {
+ top = (int) (bounds - height);
+ }
+ return Math.max(0, top);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java
index 61d111d..d7b92a2 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java
@@ -61,7 +61,6 @@ public class WorldTweaks extends Module {
private static final String SETTING_CHANGE_TIME = "change_time";
private static final String SETTING_TIME_HOURS = "time_hours";
private static final String SETTING_SYNC_SYSTEM_TIME = "sync_system_time";
- private static final String SETTING_ITEM_PHYSICS = "item_physics";
private static volatile long serverTimeOfDay;
private final BooleanValue fogControlEnabled =
bool("worldTweaksFogControl", SETTING_FOG_CONTROL, true);
@@ -128,8 +127,6 @@ public class WorldTweaks extends Module {
private final NumberValue timeHours =
visibleWhen(num("worldTweaksTimeHours", SETTING_TIME_HOURS, 21, 0, 23),
() -> changeTime.get() && !syncSystemTime.get());
- private final BooleanValue itemPhysicsEnabled =
- bool("worldTweaksItemPhysics", SETTING_ITEM_PHYSICS, true);
private boolean wasOverridingTime;
private static int getHourTicks(int hour) {
@@ -224,9 +221,6 @@ public class WorldTweaks extends Module {
return isEnabled() && changeTime.get();
}
- public boolean isItemPhysicsEnabled() {
- return isEnabled() && itemPhysicsEnabled.get();
- }
public int getTimeOfDayTicks() {
if (syncSystemTime.get()) {
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java
index d9e80db..ffe1fd3 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java
@@ -7,8 +7,17 @@
package dev.loki.lovisual.mixins;
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.visuals.Animations;
+import dev.loki.lovisual.features.module.modules.visuals.ShulkerPreview;
+import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.joml.Matrix3x2fStack;
import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -16,8 +25,77 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(AbstractContainerScreen.class)
public abstract class AbstractContainerScreenMixin {
+ @Shadow
+ protected Slot hoveredSlot;
+
+ @Unique
+ private boolean lovisual$animations$scaleApplied;
+
@Inject(method = "onClose", at = @At("HEAD"), cancellable = true)
private void lovisual$inventoryMove$deferClose(CallbackInfo ci) {
// InventoryMove module removed; keep hook for compatibility.
}
+
+ /**
+ * Smooth open animation from the Animations module: the whole screen is drawn
+ * with a scale between 0.9 and 1.0 around the screen center. The animated
+ * value itself is advanced once per frame inside the module; this only reads
+ * the settled value and wraps the extraction in a matching push/pop.
+ */
+ @Inject(
+ method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V",
+ at = @At("HEAD")
+ )
+ private void lovisual$animations$beginOpenScale(GuiGraphicsExtractor graphics,
+ int mouseX,
+ int mouseY,
+ float partialTick,
+ CallbackInfo ci) {
+ lovisual$animations$scaleApplied = false;
+
+ Animations animations = Modules.get(Animations.class);
+ if (animations == null) return;
+
+ float scale = animations.getInventoryScale();
+ if (scale >= 0.999f) return;
+
+ Matrix3x2fStack pose = graphics.pose();
+ float centerX = graphics.guiWidth() * 0.5f;
+ float centerY = graphics.guiHeight() * 0.5f;
+
+ pose.pushMatrix();
+ pose.translate(centerX, centerY);
+ pose.scale(scale, scale);
+ pose.translate(-centerX, -centerY);
+ lovisual$animations$scaleApplied = true;
+ }
+
+ @Inject(
+ method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V",
+ at = @At("RETURN")
+ )
+ private void lovisual$animations$endOpenScale(GuiGraphicsExtractor graphics,
+ int mouseX,
+ int mouseY,
+ float partialTick,
+ CallbackInfo ci) {
+ if (!lovisual$animations$scaleApplied) return;
+ lovisual$animations$scaleApplied = false;
+ graphics.pose().popMatrix();
+ }
+
+ @Inject(
+ method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V",
+ at = @At("TAIL")
+ )
+ private void lovisual$shulkerPreview$draw(GuiGraphicsExtractor graphics,
+ int mouseX,
+ int mouseY,
+ float partialTick,
+ CallbackInfo ci) {
+ Slot slot = this.hoveredSlot;
+ ItemStack stack = slot == null ? ItemStack.EMPTY : slot.getItem();
+ if (stack.isEmpty()) return;
+ ShulkerPreview.onScreenExtract(graphics, stack, mouseX, mouseY);
+ }
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java
index bb143af..f0bf7b9 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java
@@ -18,6 +18,7 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.visuals.AtmoDawnFog;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(value = AtmosphericFogEnvironment.class, priority = 1200)
@@ -30,6 +31,12 @@ public class AtmosphericFogEnvironmentMixin {
float viewDistance,
DeltaTracker tickCounter,
CallbackInfo ci) {
+ if (AtmoDawnFog.applyFog(data, camera, world, viewDistance,
+ tickCounter.getGameTimeDeltaPartialTick(false))) {
+ ci.cancel();
+ return;
+ }
+
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogControlEnabled()) return;
if (module.isFogModifyEnabled()) {
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java
index c69d1cd..2a0423b 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java
@@ -7,10 +7,14 @@
package dev.loki.lovisual.mixins;
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.misc.NoServerPack;
import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket;
+import net.minecraft.network.protocol.common.ServerboundResourcePackPacket;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
@@ -30,4 +34,20 @@ public abstract class ClientCommonPacketListenerImplMixin {
private void lovisual$flightSendPacket(Packet> packet, CallbackInfo ci) {
// Flight module removed; no move-packet override remains.
}
+
+ @Inject(
+ method = "handleResourcePackPush(Lnet/minecraft/network/protocol/common/ClientboundResourcePackPushPacket;)V",
+ at = @At("HEAD"),
+ cancellable = true
+ )
+ private void lovisual$onResourcePackPush(ClientboundResourcePackPushPacket packet, CallbackInfo ci) {
+ NoServerPack module = Modules.get(NoServerPack.class);
+ if (module == null || !module.shouldDeclinePush()) return;
+ // Same terminal answer vanilla sends when the pack is refused, but
+ // before any prompt screen or download can start.
+ connection.send(new ServerboundResourcePackPacket(
+ packet.id(), ServerboundResourcePackPacket.Action.DECLINED));
+ module.onPackDeclined(packet.id());
+ ci.cancel();
+ }
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java
index 8dad537..3c4b912 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java
@@ -19,6 +19,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.visuals.AtmoDawnFog;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(value = FogRenderer.class, priority = 1200)
@@ -67,6 +68,10 @@ public class FogRendererMixin {
int viewDistance,
float skyDarkness,
Vector4f color, CallbackInfo ci) {
+ if (AtmoDawnFog.tintColor(color, world, camera.position().y)) {
+ return;
+ }
+
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogModifyEnabled()) return;
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java
new file mode 100644
index 0000000..eb8f275
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.mixins;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Axis;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.player.AbstractClientPlayer;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.entity.HumanoidArm;
+import dev.loki.lovisual.features.module.modules.visuals.HoldMyItems;
+import dev.loki.lovisual.render.helpers.TickDelta;
+import dev.loki.lovisual.util.animation.hand.BobMath;
+import dev.loki.lovisual.util.animation.hand.HandAnimState;
+import dev.loki.lovisual.util.animation.hand.HandPoseMath;
+
+/**
+ * Клей между рендером руки и чистой математикой динамики: собирает ввод
+ * (дельты взгляда, скорость, использование предмета), собирает позу и
+ * применяет её к матрице вокруг ванильных трансформов. Состояние не хранит —
+ * оно живёт в {@link HandAnimState}, обновляемом раз за кадр.
+ */
+final class HandDynamics {
+
+ /** Смещения/углы слабее этого считаются нулевыми и не применяются. */
+ private static final float SIGNIFICANT_EPSILON = 1.0e-4f;
+
+ private HandDynamics() {
+ }
+
+ /** Один шаг динамики за кадр; вызывается из начала submitHandsWithItems. */
+ static void beginFrame(HandAnimState state, AbstractClientPlayer player, float tickDelta) {
+ state.beginFrame(
+ player,
+ Minecraft.getInstance().level,
+ player.getViewYRot(tickDelta),
+ player.getViewXRot(tickDelta),
+ player.isUsingItem() && player.getUsedItemHand() == InteractionHand.MAIN_HAND,
+ player.isUsingItem() && player.getUsedItemHand() == InteractionHand.OFF_HAND,
+ TickDelta.fixedDeltaTicks()
+ );
+ }
+
+ /**
+ * Применяет позу динамики к матрице руки: pushPose, затем смещение и
+ * повороты вокруг точки взгляда до ванильных трансформов. Вызывающий
+ * обязан сделать popPose, если вернулось true.
+ *
+ * @return true, если был pushPose и нужен popPose
+ */
+ static boolean apply(
+ HoldMyItems module, HandAnimState state, AbstractClientPlayer player,
+ float tickDelta, InteractionHand hand, PoseStack matrices
+ ) {
+ boolean mainHand = hand == InteractionHand.MAIN_HAND;
+ float bobStrength = module.walkBob.get() ? module.bobStrength.get() : 0.0f;
+ float driftStrength = module.useDrift.get() ? module.driftStrength.get() : 0.0f;
+
+ boolean usingThisHand = player.isUsingItem() && player.getUsedItemHand() == hand;
+ float drift = usingThisHand ? state.drift(mainHand) : 0.0f;
+
+ HandPoseMath.Pose pose = HandPoseMath.sway(
+ state.sway().yawLag(), state.sway().pitchLag(), module.swayIntensity.get());
+ if (bobStrength > 0.0f) {
+ pose = HandPoseMath.add(pose, HandPoseMath.bob(
+ player.walkAnimation.position(tickDelta),
+ BobMath.amplitude(player.walkAnimation.speed(tickDelta)),
+ bobStrength));
+ }
+ if (driftStrength > 0.0f && drift > 0.0f) {
+ boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand;
+ pose = HandPoseMath.add(pose, HandPoseMath.drift(drift, driftStrength, rightArm));
+ }
+ if (module.joinAppear.get()) {
+ pose = HandPoseMath.add(pose, HandPoseMath.joinDrop(state.joinProgress()));
+ }
+ if (!pose.isSignificant(SIGNIFICANT_EPSILON)) {
+ return false;
+ }
+
+ matrices.pushPose();
+ matrices.translate(pose.dx(), pose.dy(), pose.dz());
+ if (pose.rx() != 0.0f) matrices.mulPose(Axis.XP.rotationDegrees(pose.rx()));
+ if (pose.ry() != 0.0f) matrices.mulPose(Axis.YP.rotationDegrees(pose.ry()));
+ if (pose.rz() != 0.0f) matrices.mulPose(Axis.ZP.rotationDegrees(pose.rz()));
+ return true;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java
index 6a6772b..2bcfde6 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java
@@ -25,8 +25,9 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
-import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
+import dev.loki.lovisual.features.module.modules.visuals.ItemPhysic;
import dev.loki.lovisual.mixininterface.IItemEntityRenderState;
+import net.minecraft.util.Mth;
@Mixin(ItemEntityRenderer.class)
public abstract class ItemEntityRendererMixin extends EntityRenderer {
@@ -51,19 +52,26 @@ public abstract class ItemEntityRendererMixin extends EntityRenderer cir) {
+ LockSlot lockSlot = Modules.get(LockSlot.class);
+ if (lockSlot != null && lockSlot.shouldBlockSelectedDrop()) {
+ cir.setReturnValue(false);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java
index 733badd..4bb1cc1 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java
@@ -30,6 +30,7 @@ import dev.loki.lovisual.events.impl.AttackEntityEvent;
import dev.loki.lovisual.events.impl.EventBreakBlock;
import dev.loki.lovisual.features.gui.hud.draggable.impl.TargetHud;
import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.combat.NoFriendDamage;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
import dev.loki.lovisual.features.module.modules.misc.HitSounds;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
@@ -141,8 +142,14 @@ public class MultiPlayerGameModeMixin {
}
@Inject(method = "attack(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;)V",
- at = @At("HEAD"))
+ at = @At("HEAD"), cancellable = true)
private void onAttackEntity(Player player, Entity target, CallbackInfo ci) {
+ NoFriendDamage noFriendDamage = Modules.get(NoFriendDamage.class);
+ if (noFriendDamage != null && noFriendDamage.shouldBlockAttack(target)) {
+ ci.cancel();
+ return;
+ }
+
if (!((WorldAccessor) player.level()).lovisual$isClient()) return;
if (Events.BUS.hasListeners(AttackEntityEvent.class)) {
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java
new file mode 100644
index 0000000..d2bf8ec
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.mixins;
+
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.player.LockSlot;
+import net.minecraft.client.multiplayer.MultiPlayerGameMode;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.ContainerInput;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+/**
+ * Cancels THROW container clicks (Q / Ctrl+Q over a hovered slot and
+ * throw clicks) targeting locked hotbar slots while LockSlot is enabled.
+ */
+@Mixin(MultiPlayerGameMode.class)
+public class MultiPlayerGameModeThrowMixin {
+
+ @Inject(method = "handleContainerInput", at = @At("HEAD"), cancellable = true)
+ private void lovisual$lockSlot$blockThrowClick(int containerId, int slotId, int button,
+ ContainerInput input, Player player, CallbackInfo ci) {
+ if (input != ContainerInput.THROW) return;
+ LockSlot lockSlot = Modules.get(LockSlot.class);
+ if (lockSlot != null && lockSlot.shouldBlockContainerThrow(player, containerId, slotId)) {
+ ci.cancel();
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java
new file mode 100644
index 0000000..a66b3f4
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java
@@ -0,0 +1,20 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.mixins.accessors;
+
+import com.mojang.blaze3d.platform.InputConstants;
+import net.minecraft.client.KeyMapping;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(KeyMapping.class)
+public interface KeyMappingAccessor {
+
+ @Accessor("key")
+ InputConstants.Key lovisual$key();
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java
new file mode 100644
index 0000000..b2cc78d
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render;
+
+import dev.loki.lovisual.features.module.Modules;
+import dev.loki.lovisual.features.module.modules.visuals.InvisEsp;
+import dev.loki.lovisual.render.iris.IrisRuntime;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
+import net.minecraft.client.renderer.rendertype.RenderType;
+import net.minecraft.client.renderer.rendertype.RenderTypes;
+import net.minecraft.resources.Identifier;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.player.Player;
+
+/**
+ * Логика See Invisibles: невидимый игрок раскрывается — флаг невидимости
+ * снимается перед рендером, а render type форсится в полупрозрачный.
+ */
+public enum SeeInvisiblesReveal {
+ ;
+
+ public static void reveal(T entity, LivingEntityRenderState state) {
+ if (!(entity instanceof Player player) || player == Minecraft.getInstance().player) {
+ return;
+ }
+ InvisEsp module = Modules.get(InvisEsp.class);
+ if (module == null || !module.isEnabled() || !entity.isInvisible()) {
+ return;
+ }
+ if (state instanceof SeeInvisiblesState seeInvis) {
+ seeInvis.lovisual$setSeeInvisRevealed(true);
+ }
+ state.isInvisible = false;
+ }
+
+ public static RenderType renderTypeFor(RenderType original, Identifier texture) {
+ InvisEsp module = Modules.get(InvisEsp.class);
+ if (module == null || !module.isEnabled() || IrisRuntime.isRenderingShadowPass()) {
+ return original;
+ }
+ if (!module.isTranslucentReveal()) {
+ return original;
+ }
+ if (original != null && original.hasBlending()) {
+ return original;
+ }
+ return RenderTypes.entityTranslucent(texture, true);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java
new file mode 100644
index 0000000..149af2e
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java
@@ -0,0 +1,18 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render;
+
+/**
+ * Marker injected into entity render states when InvisESP reveals
+ * an invisible player this frame.
+ */
+public interface SeeInvisiblesState {
+ boolean lovisual$isSeeInvisRevealed();
+
+ void lovisual$setSeeInvisRevealed(boolean revealed);
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java
index 14ea855..0b4021b 100644
--- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java
@@ -7,21 +7,23 @@
package dev.loki.lovisual.render.engine.pipeline;
+import com.mojang.blaze3d.PrimitiveTopology;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
-import com.mojang.blaze3d.platform.BlendFactor;
-import com.mojang.blaze3d.shaders.UniformType;
import com.mojang.blaze3d.systems.GpuDevice;
import com.mojang.blaze3d.systems.RenderSystem;
-import com.mojang.blaze3d.vertex.DefaultVertexFormat;
-import net.minecraft.resources.Identifier;
-import net.minecraft.server.packs.resources.ResourceManager;
import dev.loki.lovisual.mixininterface.IRenderPipeline;
+import dev.loki.lovisual.render.engine.pipeline.pipelines.PipelineTemplates;
+import dev.loki.lovisual.render.engine.pipeline.pipelines.ScreenPipelines;
+import dev.loki.lovisual.render.engine.pipeline.pipelines.UiPipelines;
+import dev.loki.lovisual.render.engine.pipeline.pipelines.WorldPipelines;
import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract;
import dev.loki.lovisual.render.engine.rhi.pipeline.RenderPipelineRegistry;
import dev.loki.lovisual.render.engine.shader.LoVisualShaderSources;
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
import dev.loki.lovisual.util.logging.DebugLog;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.packs.resources.ResourceManager;
import java.util.ArrayList;
import java.util.Collections;
@@ -30,1428 +32,299 @@ import java.util.List;
/**
* Pipeline registry for LoVisual's custom rendering engine.
*
+ * Public API facade: every shader identifier and pipeline constant keeps its original
+ * name and value. The repetitive builder chains live in the template classes of
+ * {@link dev.loki.lovisual.render.engine.pipeline.pipelines}.
+ *
* Keep this class free of module-specific logic: it is the foundation for all custom 3D/2D effects.
*/
public enum LoVisualRenderPipelines {
;
+ // ======================
// Shaders (assets/lovisual/shaders/*)
- public static final Identifier SHADER_POS_COLOR_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color.vert");
- public static final Identifier SHADER_WIDE_LINE_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/wide_line.vert");
- public static final Identifier SHADER_POS_COLOR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color.frag");
- public static final Identifier SHADER_POS_COLOR_FOG_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_fog.vert");
- public static final Identifier SHADER_POS_COLOR_FOG_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_fog.frag");
- public static final Identifier SHADER_POS_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_rect_params.vert");
- public static final Identifier SHADER_POS_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_rect_params2.vert");
- public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params.vert");
- public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params2.vert");
- public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params5.vert");
- public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params5_generic.vert");
- public static final Identifier SHADER_POS_TEX_COLOR_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color.vert");
- public static final Identifier SHADER_POS_TEX_COLOR_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_params2.vert");
- public static final Identifier SHADER_POS_TEX_COLOR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color.frag");
- public static final Identifier SHADER_GUI_TEXTURE_LOOKUP_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/gui_texture_lookup.frag");
- public static final Identifier SHADER_POS_TEX_COLOR_TINT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_tint.frag");
- public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog.vert");
- public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog.frag");
- public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog_additive.frag");
- public static final Identifier SHADER_POS_COLOR_SKYBOX_SHADER_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_skybox_shader.vert");
- public static final Identifier SHADER_REIMAGINED_SKYBOX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/reimagined_skybox.frag");
- public static final Identifier SHADER_SHADER_SKY_THUNDER_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_sky_thunder.frag");
- public static final Identifier SHADER_SHADER_SKY_PULSAR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_sky_pulsar.frag");
- public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_rect_params.vert");
- public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_local_color_rect_params.vert");
- public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_local_color_rect_params2.vert");
- public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_rect_params2.vert");
- public static final Identifier SHADER_TEXT_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/text.vert");
- public static final Identifier SHADER_TEXT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text.frag");
- public static final Identifier SHADER_TEXT_MSDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text_msdf.frag");
- public static final Identifier SHADER_SVG_MSDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/svg_msdf.frag");
- public static final Identifier SHADER_TEXT_LIQUID_GLASS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text_liquid_glass.frag");
- public static final Identifier SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text_liquid_glass_msdf.frag");
- public static final Identifier SHADER_CIRCLE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/circle_batch.frag");
- public static final Identifier SHADER_ARC_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/arc_batch.frag");
- public static final Identifier SHADER_ORBIZ_RING_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/orbiz_ring_batch.frag");
- public static final Identifier SHADER_WORLD_DECAL_SDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/world_decal_sdf.frag");
- public static final Identifier SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_stroke_corners_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_batch.frag");
- public static final Identifier SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_rounded_fill_smoke_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_stroke_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_stroke_angular_batch.frag");
- public static final Identifier SHADER_CHAMFERED_RECT_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/chamfered_rect_batch.frag");
- public static final Identifier SHADER_UI_SHAPE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_shape_batch.frag");
- public static final Identifier SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/chamfered_rect_stroke_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_corners_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_glow_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_shadow_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_soft_shadow_batch.frag");
- public static final Identifier SHADER_RADIAL_GLOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/radial_glow_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_TEX_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_tex_batch.frag");
- public static final Identifier SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_tex_mask_batch.frag");
- public static final Identifier SHADER_UI_BLUR_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur_batch.frag");
- public static final Identifier SHADER_UI_BLUR_BATCH_CORNERS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur_batch_corners.frag");
- public static final Identifier SHADER_UI_LIQUID_GLASS_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_liquid_glass_batch.frag");
- public static final Identifier SHADER_DAMAGE_TINT_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/damage_tint.vert");
- public static final Identifier SHADER_DAMAGE_TINT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/damage_tint.frag");
- public static final Identifier SHADER_POST_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/post_fx.frag");
- public static final Identifier SHADER_MOTION_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/motion_blur.frag");
- public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/depth_of_field.frag");
- public static final Identifier SHADER_WET_SURFACE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/wet_surface.frag");
- public static final Identifier SHADER_HEAT_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/heat_fx.frag");
- public static final Identifier SHADER_ESP_GRADIENT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_gradient.frag");
- public static final Identifier SHADER_ESP_SHADOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_shadow.frag");
- public static final Identifier SHADER_ESP_SMOKE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_smoke.frag");
- public static final Identifier SHADER_PORTAL_RIFT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/portal_rift.frag");
- public static final Identifier SHADER_SLEEP_OVERLAY_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sleep_overlay.frag");
- public static final Identifier SHADER_HAND_SMOKE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_smoke.frag");
- public static final Identifier SHADER_HAND_METALLIC_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_metallic.frag");
- public static final Identifier SHADER_UI_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur.frag");
- public static final Identifier SHADER_HAND_GLASS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_glass.frag");
- public static final Identifier SHADER_HAND_CHAMS_SOLID_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_solid.frag");
- public static final Identifier SHADER_HAND_CHAMS_CHROMA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_chroma.frag");
- public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_stripes.frag");
- public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_glow.frag");
- public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_snow.frag");
- public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_balatro.frag");
- public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_blend.frag");
- public static final Identifier SHADER_SKY_SUN_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_sun.frag");
- public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_waves.frag");
- public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_aurora.frag");
- public static final Identifier SHADER_SKY_SHADER_SKY_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_sky.frag");
- public static final Identifier SHADER_SKY_SHADER_WATER_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_water.frag");
- public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_caustic.frag");
+ // ======================
+ // Core colored / line / fog shaders
+ public static final Identifier SHADER_POS_COLOR_VERT = shader("pos_color.vert");
+ public static final Identifier SHADER_WIDE_LINE_VERT = shader("wide_line.vert");
+ public static final Identifier SHADER_POS_COLOR_FRAG = shader("pos_color.frag");
+ public static final Identifier SHADER_POS_COLOR_FOG_VERT = shader("pos_color_fog.vert");
+ public static final Identifier SHADER_POS_COLOR_FOG_FRAG = shader("pos_color_fog.frag");
+ // Rect-parameter vertex shaders (batched shapes)
+ public static final Identifier SHADER_POS_COLOR_RECT_PARAMS_VERT = shader("pos_color_rect_params.vert");
+ public static final Identifier SHADER_POS_COLOR_RECT_PARAMS2_VERT = shader("pos_color_rect_params2.vert");
+ public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT = shader("pos_local_color_rect_params.vert");
+ public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT = shader("pos_local_color_rect_params2.vert");
+ public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT = shader("pos_local_color_rect_params5.vert");
+ public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT = shader("pos_local_color_rect_params5_generic.vert");
+ // Textured primitives & GUI lookup
+ public static final Identifier SHADER_POS_TEX_COLOR_VERT = shader("pos_tex_color.vert");
+ public static final Identifier SHADER_POS_TEX_COLOR_PARAMS2_VERT = shader("pos_tex_color_params2.vert");
+ public static final Identifier SHADER_POS_TEX_COLOR_FRAG = shader("pos_tex_color.frag");
+ public static final Identifier SHADER_GUI_TEXTURE_LOOKUP_FRAG = shader("gui_texture_lookup.frag");
+ public static final Identifier SHADER_POS_TEX_COLOR_TINT_FRAG = shader("pos_tex_color_tint.frag");
+ public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_VERT = shader("pos_tex_color_sky_fog.vert");
+ public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_FRAG = shader("pos_tex_color_sky_fog.frag");
+ public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG = shader("pos_tex_color_sky_fog_additive.frag");
+ // Skyboxes & shader skies
+ public static final Identifier SHADER_POS_COLOR_SKYBOX_SHADER_VERT = shader("pos_color_skybox_shader.vert");
+ public static final Identifier SHADER_REIMAGINED_SKYBOX_FRAG = shader("reimagined_skybox.frag");
+ public static final Identifier SHADER_SHADER_SKY_THUNDER_FRAG = shader("shader_sky_thunder.frag");
+ public static final Identifier SHADER_SHADER_SKY_PULSAR_FRAG = shader("shader_sky_pulsar.frag");
+ public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS_VERT = shader("pos_tex_color_rect_params.vert");
+ public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT = shader("pos_tex_local_color_rect_params.vert");
+ public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT = shader("pos_tex_local_color_rect_params2.vert");
+ public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS2_VERT = shader("pos_tex_color_rect_params2.vert");
+ // Text & MSDF
+ public static final Identifier SHADER_TEXT_VERT = shader("text.vert");
+ public static final Identifier SHADER_TEXT_FRAG = shader("text.frag");
+ public static final Identifier SHADER_TEXT_MSDF_FRAG = shader("text_msdf.frag");
+ public static final Identifier SHADER_SVG_MSDF_FRAG = shader("svg_msdf.frag");
+ public static final Identifier SHADER_TEXT_LIQUID_GLASS_FRAG = shader("text_liquid_glass.frag");
+ public static final Identifier SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG = shader("text_liquid_glass_msdf.frag");
+ // Batched UI shapes (SDF fragments)
+ public static final Identifier SHADER_CIRCLE_BATCH_FRAG = shader("circle_batch.frag");
+ public static final Identifier SHADER_ARC_BATCH_FRAG = shader("arc_batch.frag");
+ public static final Identifier SHADER_ORBIZ_RING_BATCH_FRAG = shader("orbiz_ring_batch.frag");
+ public static final Identifier SHADER_WORLD_DECAL_SDF_FRAG = shader("world_decal_sdf.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG = shader("rounded_rect_stroke_corners_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_BATCH_FRAG = shader("rounded_rect_batch.frag");
+ public static final Identifier SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG = shader("ui_rounded_fill_smoke_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG = shader("rounded_rect_stroke_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG = shader("rounded_rect_stroke_angular_batch.frag");
+ public static final Identifier SHADER_CHAMFERED_RECT_BATCH_FRAG = shader("chamfered_rect_batch.frag");
+ public static final Identifier SHADER_UI_SHAPE_BATCH_FRAG = shader("ui_shape_batch.frag");
+ public static final Identifier SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG = shader("chamfered_rect_stroke_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG = shader("rounded_rect_corners_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG = shader("rounded_rect_glow_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG = shader("rounded_rect_shadow_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG = shader("rounded_rect_soft_shadow_batch.frag");
+ public static final Identifier SHADER_RADIAL_GLOW_BATCH_FRAG = shader("radial_glow_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_TEX_BATCH_FRAG = shader("rounded_rect_tex_batch.frag");
+ public static final Identifier SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG = shader("rounded_rect_tex_mask_batch.frag");
+ public static final Identifier SHADER_UI_BLUR_BATCH_FRAG = shader("ui_blur_batch.frag");
+ public static final Identifier SHADER_UI_BLUR_BATCH_CORNERS_FRAG = shader("ui_blur_batch_corners.frag");
+ public static final Identifier SHADER_UI_LIQUID_GLASS_BATCH_FRAG = shader("ui_liquid_glass_batch.frag");
+ // Fullscreen post-processing & overlays
+ public static final Identifier SHADER_DAMAGE_TINT_VERT = shader("damage_tint.vert");
+ public static final Identifier SHADER_DAMAGE_TINT_FRAG = shader("damage_tint.frag");
+ public static final Identifier SHADER_POST_FX_FRAG = shader("post_fx.frag");
+ public static final Identifier SHADER_MOTION_BLUR_FRAG = shader("motion_blur.frag");
+ public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = shader("depth_of_field.frag");
+ public static final Identifier SHADER_WET_SURFACE_FRAG = shader("wet_surface.frag");
+ public static final Identifier SHADER_HEAT_FX_FRAG = shader("heat_fx.frag");
+ public static final Identifier SHADER_ESP_GRADIENT_FRAG = shader("shader_esp_gradient.frag");
+ public static final Identifier SHADER_ESP_SHADOW_FRAG = shader("shader_esp_shadow.frag");
+ public static final Identifier SHADER_ESP_SMOKE_FRAG = shader("shader_esp_smoke.frag");
+ public static final Identifier SHADER_PORTAL_RIFT_FRAG = shader("portal_rift.frag");
+ public static final Identifier SHADER_SLEEP_OVERLAY_FRAG = shader("sleep_overlay.frag");
+ // Hand effects & chams
+ public static final Identifier SHADER_HAND_SMOKE_FRAG = shader("hand_smoke.frag");
+ public static final Identifier SHADER_HAND_METALLIC_FRAG = shader("hand_metallic.frag");
+ public static final Identifier SHADER_HAND_NOISE_FRAG = shader("hand_noise.frag");
+ public static final Identifier SHADER_UI_BLUR_FRAG = shader("ui_blur.frag");
+ public static final Identifier SHADER_HAND_GLASS_FRAG = shader("hand_glass.frag");
+ public static final Identifier SHADER_HAND_CHAMS_SOLID_FRAG = shader("hand_chams_solid.frag");
+ public static final Identifier SHADER_HAND_CHAMS_CHROMA_FRAG = shader("hand_chams_chroma.frag");
+ public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = shader("hand_chams_stripes.frag");
+ public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = shader("hand_chams_glow.frag");
+ public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = shader("hand_chams_snow.frag");
+ public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = shader("hand_chams_balatro.frag");
+ public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = shader("hand_chams_blend.frag");
+ // Sky & menu backgrounds
+ public static final Identifier SHADER_SKY_SUN_FRAG = shader("sky_sun.frag");
+ public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = shader("menu_background_waves.frag");
+ public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = shader("menu_background_aurora.frag");
+ public static final Identifier SHADER_SKY_SHADER_SKY_FRAG = shader("sky_shader_sky.frag");
+ public static final Identifier SHADER_SKY_SHADER_WATER_FRAG = shader("sky_shader_water.frag");
+ public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = shader("sky_shader_caustic.frag");
+
private static final List PIPELINES = new ArrayList<>();
- public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(new ExtendedRenderPipelineBuilder(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED_SNIPPET)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/gui_texture_lookup"))
- .withFragmentShader(SHADER_GUI_TEXTURE_LOOKUP_FRAG)
- .build()
- );
- // Snippets
- private static final RenderPipeline.Snippet MESH_UNIFORMS = new ExtendedRenderPipelineBuilder()
- .withUniform("MeshData", UniformType.UNIFORM_BUFFER)
- .buildSnippet();
- /**
- * No depth test; translucent; triangles.
- */
- public static final RenderPipeline WORLD_COLORED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
+
+ public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(ScreenPipelines.guiTextureLookup());
// ======================
// World (3D)
// ======================
- /**
- * No depth test; translucent; lines.
- */
- public static final RenderPipeline WORLD_COLORED_LINES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLineSmooth()
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Screen-space wide line expansion for WORLD_COLORED_LINES. Uses triangles; never depends on glLineWidth.
- */
- public static final RenderPipeline WORLD_WIDE_COLORED_LINES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines"))
- .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_WIDE_LINE_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
-
- /**
- * Depth test (GEQUAL); translucent; triangles.
- */
- public static final RenderPipeline WORLD_COLORED_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * No depth test; translucent; textured triangles.
- */
- public static final RenderPipeline WORLD_TEXTURED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; textured triangles.
- */
- public static final RenderPipeline WORLD_TEXTURED_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); additive; colored triangles.
- */
- public static final RenderPipeline WORLD_COLORED_ADDITIVE_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_additive_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * No depth test; translucent; world decal quads with SDF fill/stroke.
- */
- public static final RenderPipeline WORLD_DECAL_SDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_decal_sdf"))
- .withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_PARAMS2_VERT)
- .withFragmentShader(SHADER_WORLD_DECAL_SDF_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; world decal quads with SDF fill/stroke.
- */
- public static final RenderPipeline WORLD_DECAL_SDF_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_decal_sdf_depth"))
- .withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_PARAMS2_VERT)
- .withFragmentShader(SHADER_WORLD_DECAL_SDF_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * No depth test; translucent; world text quads.
- */
- public static final RenderPipeline WORLD_TEXT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; world text quads.
- */
- public static final RenderPipeline WORLD_TEXT_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * No depth test; translucent; world MSDF text quads.
- */
- public static final RenderPipeline WORLD_TEXT_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text_msdf"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_MSDF_FRAG)
- .withSampler("u_Texture")
- .withUniform("MsdfText", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; world MSDF text quads.
- */
- public static final RenderPipeline WORLD_TEXT_MSDF_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text_msdf_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_MSDF_FRAG)
- .withSampler("u_Texture")
- .withUniform("MsdfText", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * No depth test; additive; textured triangles.
- */
- public static final RenderPipeline WORLD_TEXTURED_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * No depth test; additive; textured triangles with shader-side tint.
- */
- public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_tint_additive"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_TINT_FRAG)
- .withSampler("u_Texture")
- .withUniform("TextureTint", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); additive; textured triangles.
- */
- public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; lines.
- */
- public static final RenderPipeline WORLD_COLORED_LINES_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLineSmooth()
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines_depth"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Screen-space wide line expansion for WORLD_COLORED_LINES_DEPTH. Uses triangles; never depends on glLineWidth.
- */
- public static final RenderPipeline WORLD_WIDE_COLORED_LINES_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines_depth"))
- .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_WIDE_LINE_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
-
- /**
- * Depth test (GEQUAL); translucent; triangles; lets liquids blend over (no depth write).
- */
- public static final RenderPipeline WORLD_COLORED_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_liquid_blend"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; lines; lets liquids blend over (no depth write).
- */
- public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLineSmooth()
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines_liquid_blend"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_BLEND. Uses triangles; never depends on glLineWidth.
- */
- public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines_liquid_blend"))
- .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_WIDE_LINE_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
-
- /**
- * Depth test (GEQUAL); translucent; textured triangles; lets liquids blend over (no depth write).
- */
- public static final RenderPipeline WORLD_TEXTURED_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_liquid_blend"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); additive; textured triangles; lets liquids blend over (no depth write).
- */
- public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive_liquid_blend"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; triangles; intended for above-liquids pass (no depth write).
- */
- public static final RenderPipeline WORLD_COLORED_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_liquid_ignore"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; lines; writes depth so liquids render below (above-liquids).
- */
- public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLineSmooth()
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines_liquid_ignore"))
- .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_IGNORE. Uses triangles; never depends on glLineWidth.
- */
- public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines_liquid_ignore"))
- .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_WIDE_LINE_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
-
- /**
- * Depth test (GEQUAL); additive; textured triangles; intended for pre-water depth.
- */
- public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive_liquid_ignore"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); additive; textured triangles with shader-side tint; intended for pre-water depth.
- */
- public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_tint_additive_liquid_ignore"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_TINT_FRAG)
- .withSampler("u_Texture")
- .withUniform("TextureTint", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Depth test (GEQUAL); translucent; textured triangles; intended for pre-water depth.
- */
- public static final RenderPipeline WORLD_TEXTURED_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_liquid_ignore"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * UI triangles (pos2 + color).
- */
- public static final RenderPipeline UI_COLORED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_colored"))
- .withVertexFormat(LoVisualVertexFormats.POS2_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- /**
- * UI lines (pos2 + color).
- */
- public static final RenderPipeline UI_COLORED_LINES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_colored_lines"))
- .withVertexFormat(LoVisualVertexFormats.POS2_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES)
- .withVertexShader(SHADER_POS_COLOR_VERT)
- .withFragmentShader(SHADER_POS_COLOR_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- /**
- * UI textured triangles (pos2 + tex + color).
- */
- public static final RenderPipeline UI_TEXTURED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_textured"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- /**
- * UI MSDF SVG quads (pos2 + tex + color).
- */
- public static final RenderPipeline UI_SVG_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_svg_msdf"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_SVG_MSDF_FRAG)
- .withSampler("u_Texture")
- .withUniform("MsdfText", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- /**
- * UI textured triangles (pos2 + tex + color), additive blend.
- */
- public static final RenderPipeline UI_TEXTURED_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_textured_additive"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(true)
- .build()
- );
+ /** No depth test; translucent; triangles. */
+ public static final RenderPipeline WORLD_COLORED = add(WorldPipelines.colored("world_colored", false, BlendFunction.TRANSLUCENT));
+ /** No depth test; translucent; lines. */
+ public static final RenderPipeline WORLD_COLORED_LINES = add(WorldPipelines.coloredLines("world_colored_lines", false));
+ /** Screen-space wide line expansion for WORLD_COLORED_LINES. Uses triangles; never depends on glLineWidth. */
+ public static final RenderPipeline WORLD_WIDE_COLORED_LINES = add(WorldPipelines.wideLines("world_wide_colored_lines", false));
+ /** Depth test (GEQUAL); translucent; triangles. */
+ public static final RenderPipeline WORLD_COLORED_DEPTH = add(WorldPipelines.colored("world_colored_depth", true, BlendFunction.TRANSLUCENT));
+ /** No depth test; translucent; textured triangles. */
+ public static final RenderPipeline WORLD_TEXTURED = add(WorldPipelines.textured("world_textured", false, BlendFunction.TRANSLUCENT));
+ /** Depth test (GEQUAL); translucent; textured triangles. */
+ public static final RenderPipeline WORLD_TEXTURED_DEPTH = add(WorldPipelines.textured("world_textured_depth", true, BlendFunction.TRANSLUCENT));
+ /** Depth test (GEQUAL); additive; colored triangles. */
+ public static final RenderPipeline WORLD_COLORED_ADDITIVE_DEPTH = add(WorldPipelines.colored("world_colored_additive_depth", true, PipelineTemplates.ADDITIVE));
+ /** No depth test; translucent; world decal quads with SDF fill/stroke. */
+ public static final RenderPipeline WORLD_DECAL_SDF = add(WorldPipelines.decalSdf("world_decal_sdf", false));
+ /** Depth test (GEQUAL); translucent; world decal quads with SDF fill/stroke. */
+ public static final RenderPipeline WORLD_DECAL_SDF_DEPTH = add(WorldPipelines.decalSdf("world_decal_sdf_depth", true));
+ /** No depth test; translucent; world text quads. */
+ public static final RenderPipeline WORLD_TEXT = add(WorldPipelines.worldText("world_text", false, SHADER_TEXT_FRAG, false));
+ /** Depth test (GEQUAL); translucent; world text quads. */
+ public static final RenderPipeline WORLD_TEXT_DEPTH = add(WorldPipelines.worldText("world_text_depth", true, SHADER_TEXT_FRAG, false));
+ /** No depth test; translucent; world MSDF text quads. */
+ public static final RenderPipeline WORLD_TEXT_MSDF = add(WorldPipelines.worldText("world_text_msdf", false, SHADER_TEXT_MSDF_FRAG, true));
+ /** Depth test (GEQUAL); translucent; world MSDF text quads. */
+ public static final RenderPipeline WORLD_TEXT_MSDF_DEPTH = add(WorldPipelines.worldText("world_text_msdf_depth", true, SHADER_TEXT_MSDF_FRAG, true));
+ /** No depth test; additive; textured triangles. */
+ public static final RenderPipeline WORLD_TEXTURED_ADDITIVE = add(WorldPipelines.textured("world_textured_additive", false, PipelineTemplates.ADDITIVE));
+ /** No depth test; additive; textured triangles with shader-side tint. */
+ public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE = add(WorldPipelines.tintedTextured("world_textured_tint_additive", false));
+ /** Depth test (GEQUAL); additive; textured triangles. */
+ public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_DEPTH = add(WorldPipelines.textured("world_textured_additive_depth", true, PipelineTemplates.ADDITIVE));
+ /** Depth test (GEQUAL); translucent; lines. */
+ public static final RenderPipeline WORLD_COLORED_LINES_DEPTH = add(WorldPipelines.coloredLines("world_colored_lines_depth", true));
+ /** Screen-space wide line expansion for WORLD_COLORED_LINES_DEPTH. Uses triangles; never depends on glLineWidth. */
+ public static final RenderPipeline WORLD_WIDE_COLORED_LINES_DEPTH = add(WorldPipelines.wideLines("world_wide_colored_lines_depth", true));
+ /** Depth test (GEQUAL); translucent; triangles; lets liquids blend over (no depth write). */
+ public static final RenderPipeline WORLD_COLORED_LIQUID_BLEND = add(WorldPipelines.colored("world_colored_liquid_blend", true, BlendFunction.TRANSLUCENT));
+ /** Depth test (GEQUAL); translucent; lines; lets liquids blend over (no depth write). */
+ public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_BLEND = add(WorldPipelines.coloredLines("world_colored_lines_liquid_blend", true));
+ /** Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_BLEND. Uses triangles; never depends on glLineWidth. */
+ public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_BLEND = add(WorldPipelines.wideLines("world_wide_colored_lines_liquid_blend", true));
+ /** Depth test (GEQUAL); translucent; textured triangles; lets liquids blend over (no depth write). */
+ public static final RenderPipeline WORLD_TEXTURED_LIQUID_BLEND = add(WorldPipelines.textured("world_textured_liquid_blend", true, BlendFunction.TRANSLUCENT));
+ /** Depth test (GEQUAL); additive; textured triangles; lets liquids blend over (no depth write). */
+ public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_BLEND = add(WorldPipelines.textured("world_textured_additive_liquid_blend", true, PipelineTemplates.ADDITIVE));
+ /** Depth test (GEQUAL); translucent; triangles; intended for above-liquids pass (no depth write). */
+ public static final RenderPipeline WORLD_COLORED_LIQUID_IGNORE = add(WorldPipelines.colored("world_colored_liquid_ignore", true, BlendFunction.TRANSLUCENT));
+ /** Depth test (GEQUAL); translucent; lines; writes depth so liquids render below (above-liquids). */
+ public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_IGNORE = add(WorldPipelines.coloredLines("world_colored_lines_liquid_ignore", true));
+ /** Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_IGNORE. Uses triangles; never depends on glLineWidth. */
+ public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_IGNORE = add(WorldPipelines.wideLines("world_wide_colored_lines_liquid_ignore", true));
+ /** Depth test (GEQUAL); additive; textured triangles; intended for pre-water depth. */
+ public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE = add(WorldPipelines.textured("world_textured_additive_liquid_ignore", true, PipelineTemplates.ADDITIVE));
+ /** Depth test (GEQUAL); additive; textured triangles with shader-side tint; intended for pre-water depth. */
+ public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE = add(WorldPipelines.tintedTextured("world_textured_tint_additive_liquid_ignore", true));
+ /** Depth test (GEQUAL); translucent; textured triangles; intended for pre-water depth. */
+ public static final RenderPipeline WORLD_TEXTURED_LIQUID_IGNORE = add(WorldPipelines.textured("world_textured_liquid_ignore", true, BlendFunction.TRANSLUCENT));
// ======================
// UI (2D)
// ======================
- /**
- * UI text (pos2 + tex + color).
- */
- public static final RenderPipeline UI_TEXT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- /**
- * UI MSDF text (pos2 + tex + color).
- */
- public static final RenderPipeline UI_TEXT_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text_msdf"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_MSDF_FRAG)
- .withSampler("u_Texture")
- .withUniform("MsdfText", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_TEXT_LIQUID_GLASS = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text_liquid_glass"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_LIQUID_GLASS_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_SceneTexture")
- .withSampler("u_BlurTexture")
- .withUniform("UIBatch", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_TEXT_LIQUID_GLASS_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text_liquid_glass_msdf"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_TEXT_VERT)
- .withFragmentShader(SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_SceneTexture")
- .withSampler("u_BlurTexture")
- .withUniform("UIBatch", UniformType.UNIFORM_BUFFER)
- .withUniform("MsdfText", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_BLUR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_blur"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_UI_BLUR_FRAG)
- .withSampler("u_Texture")
- .withUniform("UIBlur", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen menu background (pos2).
- */
- public static final RenderPipeline MENU_BACKGROUND_WAVES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/menu_background_waves"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_MENU_BACKGROUND_WAVES_FRAG)
- .withUniform("MenuBackground", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen SkyShader passes (pos2). Sky/water/caustic share the same uniform layout.
- */
- public static final RenderPipeline SKY_SHADER_SKY = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_sky"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_SKY_SHADER_SKY_FRAG)
- .withUniform("SkyShader", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline SKY_SHADER_WATER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_water"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_SKY_SHADER_WATER_FRAG)
- .withUniform("SkyShader", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline SKY_SHADER_CAUSTIC = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_caustic"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_SKY_SHADER_CAUSTIC_FRAG)
- .withUniform("SkyShader", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen main menu background (pos2).
- */
- public static final RenderPipeline MENU_BACKGROUND_AURORA = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/menu_background_aurora"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_MENU_BACKGROUND_AURORA_FRAG)
- .withUniform("MenuBackground", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen postprocess (pos2).
- */
- public static final RenderPipeline DAMAGE_TINT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/damage_tint"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_DAMAGE_TINT_FRAG)
- .withSampler("u_Texture")
- .withUniform("PostProcess", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen visual grading + LUT (pos2).
- */
- public static final RenderPipeline POST_FX = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/post_fx"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_POST_FX_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_Lut")
- .withUniform("PostFX", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen camera-only perceptual motion blur (pos2).
- */
- public static final RenderPipeline MOTION_BLUR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/motion_blur"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_MOTION_BLUR_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_PreviousColor")
- .withUniform("MotionBlur", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen scene-depth-aware far depth of field (pos2).
- */
- public static final RenderPipeline DEPTH_OF_FIELD = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/depth_of_field"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_DEPTH_OF_FIELD_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_MainDepth")
- .withSampler("u_TranslucentDepth")
- .withSampler("u_ItemEntityDepth")
- .withSampler("u_ParticlesDepth")
- .withSampler("u_WeatherDepth")
- .withSampler("u_CloudsDepth")
- .withUniform("DepthOfField", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen wet-ground screen-space reflection pass (pos2).
- */
- public static final RenderPipeline WET_SURFACE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/wet_surface"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_WET_SURFACE_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_MainDepth")
- .withUniform("WetSurface", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen heat distortion + vignette (pos2).
- */
- public static final RenderPipeline HEAT_FX = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/heat_fx"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HEAT_FX_FRAG)
- .withSampler("u_Texture")
- .withUniform("Heat", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen shader ESP gradient composite (pos2).
- */
- public static final RenderPipeline SHADER_ESP_GRADIENT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/shader_esp_gradient"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_ESP_GRADIENT_FRAG)
- .withSampler("u_Texture")
- .withUniform("ShaderEspGradient", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen shader ESP separable shadow blur (pos2).
- */
- public static final RenderPipeline SHADER_ESP_SHADOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/shader_esp_shadow"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_ESP_SHADOW_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_Mask")
- .withUniform("ShaderEspBlur", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen shader ESP procedural smoke fill (pos2).
- */
- public static final RenderPipeline SHADER_ESP_SMOKE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/shader_esp_smoke"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_ESP_SMOKE_FRAG)
- .withSampler("u_Texture")
- .withUniform("ShaderEspSmoke", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * No depth test; additive; textured-color triangles with procedural sun shading.
- */
- public static final RenderPipeline WORLD_SKY_SUN = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_sky_sun"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_VERT)
- .withFragmentShader(SHADER_SKY_SUN_FRAG)
- .withUniform("SkySun", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen nether portal rift (pos2).
- */
- public static final RenderPipeline PORTAL_RIFT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/portal_rift"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_PORTAL_RIFT_FRAG)
- .withSampler("u_Texture")
- .withUniform("Heat", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen sleep overlay (pos2).
- */
- public static final RenderPipeline SLEEP_OVERLAY = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sleep_overlay"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_SLEEP_OVERLAY_FRAG)
- .withSampler("u_Texture")
- .withUniform("PostProcess", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen hand glass (pos2).
- */
- public static final RenderPipeline HAND_GLASS = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_glass"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_GLASS_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandGlass", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_SMOKE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_smoke"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_SMOKE_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandSmoke", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_METALLIC = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_metallic"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_METALLIC_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandMetallic", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_SOLID = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_solid"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_SOLID_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandChams", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_CHROMA = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_chroma"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_CHROMA_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandChams", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_STRIPES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_stripes"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_STRIPES_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandChams", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_GLOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_glow"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_GLOW_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandChams", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_SNOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_snow"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_SNOW_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandChams", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_BALATRO = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_balatro"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_BALATRO_FRAG)
- .withSampler("u_Src")
- .withSampler("u_Mask")
- .withUniform("HandChams", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline HAND_CHAMS_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_blend"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_HAND_CHAMS_BLEND_FRAG)
- .withSampler("u_Shader1")
- .withSampler("u_Shader2")
- .withSampler("u_Mask")
- .withUniform("HandBlend", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- private static final RenderPipeline.Snippet FOG_UNIFORMS = new ExtendedRenderPipelineBuilder()
- .withUniform("Fog", UniformType.UNIFORM_BUFFER)
- .buildSnippet();
- /**
- * No depth test; translucent; textured skybox with vanilla sky/fog blending.
- */
- public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, FOG_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_skybox_textured_fog"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_SKY_FOG_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_SKY_FOG_FRAG)
- .withSampler("u_Texture")
- .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * No depth test; additive; textured skybox with vanilla sky/fog blending.
- */
- public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, FOG_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_skybox_textured_fog_additive"))
- .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_COLOR_SKY_FOG_VERT)
- .withFragmentShader(SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG)
- .withSampler("u_Texture")
- .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen procedural Reimagined skybox with sky/fog blending.
- */
- public static final RenderPipeline WORLD_REIMAGINED_SKYBOX_SHADER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, FOG_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_reimagined_skybox_shader"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_REIMAGINED_SKYBOX_FRAG)
- .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen procedural ShaderSky (Thunder) with sky/fog blending.
- */
- public static final RenderPipeline WORLD_SHADER_SKY_THUNDER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_shader_sky_thunder"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_SHADER_SKY_THUNDER_FRAG)
- .withUniform("ShaderSky", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- /**
- * Fullscreen procedural ShaderSky (Pulsar) with sky/fog blending.
- */
- public static final RenderPipeline WORLD_SHADER_SKY_PULSAR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_shader_sky_pulsar"))
- .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_DAMAGE_TINT_VERT)
- .withFragmentShader(SHADER_SHADER_SKY_PULSAR_FRAG)
- .withUniform("ShaderSky", UniformType.UNIFORM_BUFFER)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- private static final RenderPipeline.Snippet UI_BATCH_UNIFORMS = new ExtendedRenderPipelineBuilder()
- .withUniform("UIBatch", UniformType.UNIFORM_BUFFER)
- .buildSnippet();
- public static final RenderPipeline UI_CIRCLE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_circle_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_CIRCLE_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ARC_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_arc_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_ARC_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ORBIZ_RING_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_orbiz_ring_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT)
- .withFragmentShader(SHADER_ORBIZ_RING_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_FILL_SMOKE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_fill_smoke_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT)
- .withFragmentShader(SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_STROKE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_stroke_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_STROKE_ANGULAR_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_stroke_angular_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_CHAMFERED_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_chamfered_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_CHAMFERED_RECT_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_SHAPE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_shape_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT)
- .withFragmentShader(SHADER_UI_SHAPE_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_CHAMFERED_STROKE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_chamfered_stroke_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_STROKE_CORNERS_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_stroke_corners_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_CORNERS_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_corners_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_GLOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_glow_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_SHADOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_shadow_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_SOFT_SHADOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_soft_shadow_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_RADIAL_GLOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_radial_glow_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_RADIAL_GLOW_BATCH_FRAG)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE))
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_TEXTURED_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_textured_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_TEX_BATCH_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_ROUNDED_TEXTURED_MASK_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_textured_mask_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(true)
- .build()
- );
- public static final RenderPipeline UI_BLUR_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_blur_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT)
- .withFragmentShader(SHADER_UI_BLUR_BATCH_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline UI_BLUR_BATCH_CORNERS = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_blur_batch_corners"))
- .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_UI_BLUR_BATCH_CORNERS_FRAG)
- .withSampler("u_Texture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
- public static final RenderPipeline UI_LIQUID_GLASS_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS)
- .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_liquid_glass_batch"))
- .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
- .withVertexShader(SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT)
- .withFragmentShader(SHADER_UI_LIQUID_GLASS_BATCH_FRAG)
- .withSampler("u_Texture")
- .withSampler("u_BlurTexture")
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
- .withDepthWrite(false)
- .withBlend(BlendFunction.TRANSLUCENT)
- .withCull(false)
- .build()
- );
+ /** UI triangles (pos2 + color). */
+ public static final RenderPipeline UI_COLORED = add(UiPipelines.colored("ui_colored", PrimitiveTopology.TRIANGLES));
+ /** UI lines (pos2 + color). */
+ public static final RenderPipeline UI_COLORED_LINES = add(UiPipelines.colored("ui_colored_lines", PrimitiveTopology.DEBUG_LINES));
+ /** UI textured triangles (pos2 + tex + color). */
+ public static final RenderPipeline UI_TEXTURED = add(UiPipelines.textured("ui_textured", SHADER_POS_TEX_COLOR_VERT, SHADER_POS_TEX_COLOR_FRAG, BlendFunction.TRANSLUCENT, true, null, "u_Texture"));
+ /** UI MSDF SVG quads (pos2 + tex + color). */
+ public static final RenderPipeline UI_SVG_MSDF = add(UiPipelines.textured("ui_svg_msdf", SHADER_POS_TEX_COLOR_VERT, SHADER_SVG_MSDF_FRAG, BlendFunction.TRANSLUCENT, true, "MsdfText", "u_Texture"));
+ /** UI textured triangles (pos2 + tex + color), additive blend. */
+ public static final RenderPipeline UI_TEXTURED_ADDITIVE = add(UiPipelines.textured("ui_textured_additive", SHADER_POS_TEX_COLOR_VERT, SHADER_POS_TEX_COLOR_FRAG, PipelineTemplates.ADDITIVE, true, null, "u_Texture"));
+ /** UI text (pos2 + tex + color). */
+ public static final RenderPipeline UI_TEXT = add(UiPipelines.textured("ui_text", SHADER_TEXT_VERT, SHADER_TEXT_FRAG, BlendFunction.TRANSLUCENT, true, null, "u_Texture"));
+ /** UI MSDF text (pos2 + tex + color). */
+ public static final RenderPipeline UI_TEXT_MSDF = add(UiPipelines.textured("ui_text_msdf", SHADER_TEXT_VERT, SHADER_TEXT_MSDF_FRAG, BlendFunction.TRANSLUCENT, true, "MsdfText", "u_Texture"));
+ public static final RenderPipeline UI_TEXT_LIQUID_GLASS = add(UiPipelines.textLiquidGlass("ui_text_liquid_glass", SHADER_TEXT_LIQUID_GLASS_FRAG, false));
+ public static final RenderPipeline UI_TEXT_LIQUID_GLASS_MSDF = add(UiPipelines.textLiquidGlass("ui_text_liquid_glass_msdf", SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG, true));
+ public static final RenderPipeline UI_BLUR = add(UiPipelines.textured("ui_blur", SHADER_POS_TEX_COLOR_VERT, SHADER_UI_BLUR_FRAG, BlendFunction.TRANSLUCENT, false, "UIBlur", "u_Texture"));
+ /** Fullscreen menu background (pos2). */
+ public static final RenderPipeline MENU_BACKGROUND_WAVES = add(ScreenPipelines.screen("menu_background_waves", SHADER_MENU_BACKGROUND_WAVES_FRAG, "MenuBackground"));
+ /** Fullscreen SkyShader passes (pos2). Sky/water/caustic share the same uniform layout. */
+ public static final RenderPipeline SKY_SHADER_SKY = add(ScreenPipelines.screen("sky_shader_sky", SHADER_SKY_SHADER_SKY_FRAG, "SkyShader"));
+ public static final RenderPipeline SKY_SHADER_WATER = add(ScreenPipelines.screen("sky_shader_water", SHADER_SKY_SHADER_WATER_FRAG, "SkyShader"));
+ public static final RenderPipeline SKY_SHADER_CAUSTIC = add(ScreenPipelines.screen("sky_shader_caustic", SHADER_SKY_SHADER_CAUSTIC_FRAG, "SkyShader"));
+ /** Fullscreen main menu background (pos2). */
+ public static final RenderPipeline MENU_BACKGROUND_AURORA = add(ScreenPipelines.screen("menu_background_aurora", SHADER_MENU_BACKGROUND_AURORA_FRAG, "MenuBackground"));
+ /** Fullscreen postprocess (pos2). */
+ public static final RenderPipeline DAMAGE_TINT = add(ScreenPipelines.screen("damage_tint", SHADER_DAMAGE_TINT_FRAG, "PostProcess", "u_Texture"));
+ /** Fullscreen visual grading + LUT (pos2). */
+ public static final RenderPipeline POST_FX = add(ScreenPipelines.screen("post_fx", SHADER_POST_FX_FRAG, "PostFX", "u_Texture", "u_Lut"));
+ /** Fullscreen camera-only perceptual motion blur (pos2). */
+ public static final RenderPipeline MOTION_BLUR = add(ScreenPipelines.screen("motion_blur", SHADER_MOTION_BLUR_FRAG, "MotionBlur", "u_Texture", "u_PreviousColor"));
+ /** Fullscreen scene-depth-aware far depth of field (pos2). */
+ public static final RenderPipeline DEPTH_OF_FIELD = add(ScreenPipelines.screen("depth_of_field", SHADER_DEPTH_OF_FIELD_FRAG, "DepthOfField",
+ "u_Texture", "u_MainDepth", "u_TranslucentDepth", "u_ItemEntityDepth", "u_ParticlesDepth", "u_WeatherDepth", "u_CloudsDepth"));
+ /** Fullscreen wet-ground screen-space reflection pass (pos2). */
+ public static final RenderPipeline WET_SURFACE = add(ScreenPipelines.screen("wet_surface", SHADER_WET_SURFACE_FRAG, "WetSurface", "u_Texture", "u_MainDepth"));
+ /** Fullscreen heat distortion + vignette (pos2). */
+ public static final RenderPipeline HEAT_FX = add(ScreenPipelines.screen("heat_fx", SHADER_HEAT_FX_FRAG, "Heat", "u_Texture"));
+ /** Fullscreen shader ESP gradient composite (pos2). */
+ public static final RenderPipeline SHADER_ESP_GRADIENT = add(ScreenPipelines.screen("shader_esp_gradient", SHADER_ESP_GRADIENT_FRAG, "ShaderEspGradient", "u_Texture"));
+ /** Fullscreen shader ESP separable shadow blur (pos2). */
+ public static final RenderPipeline SHADER_ESP_SHADOW = add(ScreenPipelines.screen("shader_esp_shadow", SHADER_ESP_SHADOW_FRAG, "ShaderEspBlur", "u_Texture", "u_Mask"));
+ /** Fullscreen shader ESP procedural smoke fill (pos2). */
+ public static final RenderPipeline SHADER_ESP_SMOKE = add(ScreenPipelines.screen("shader_esp_smoke", SHADER_ESP_SMOKE_FRAG, "ShaderEspSmoke", "u_Texture"));
+ /** No depth test; additive; textured-color triangles with procedural sun shading. */
+ public static final RenderPipeline WORLD_SKY_SUN = add(WorldPipelines.skySun("world_sky_sun"));
+ /** Fullscreen nether portal rift (pos2). */
+ public static final RenderPipeline PORTAL_RIFT = add(ScreenPipelines.screen("portal_rift", SHADER_PORTAL_RIFT_FRAG, "Heat", "u_Texture"));
+ /** Fullscreen sleep overlay (pos2). */
+ public static final RenderPipeline SLEEP_OVERLAY = add(ScreenPipelines.screen("sleep_overlay", SHADER_SLEEP_OVERLAY_FRAG, "PostProcess", "u_Texture"));
+ /** Fullscreen hand glass (pos2). */
+ public static final RenderPipeline HAND_GLASS = add(ScreenPipelines.handEffect("hand_glass", SHADER_HAND_GLASS_FRAG, "HandGlass"));
+ public static final RenderPipeline HAND_SMOKE = add(ScreenPipelines.handEffect("hand_smoke", SHADER_HAND_SMOKE_FRAG, "HandSmoke"));
+ public static final RenderPipeline HAND_METALLIC = add(ScreenPipelines.handEffect("hand_metallic", SHADER_HAND_METALLIC_FRAG, "HandMetallic"));
+ public static final RenderPipeline HAND_NOISE = add(ScreenPipelines.handEffect("hand_noise", SHADER_HAND_NOISE_FRAG, "HandNoise"));
+ public static final RenderPipeline HAND_CHAMS_SOLID = add(ScreenPipelines.handEffect("hand_chams_solid", SHADER_HAND_CHAMS_SOLID_FRAG, "HandChams"));
+ public static final RenderPipeline HAND_CHAMS_CHROMA = add(ScreenPipelines.handEffect("hand_chams_chroma", SHADER_HAND_CHAMS_CHROMA_FRAG, "HandChams"));
+ public static final RenderPipeline HAND_CHAMS_STRIPES = add(ScreenPipelines.handEffect("hand_chams_stripes", SHADER_HAND_CHAMS_STRIPES_FRAG, "HandChams"));
+ public static final RenderPipeline HAND_CHAMS_GLOW = add(ScreenPipelines.handEffect("hand_chams_glow", SHADER_HAND_CHAMS_GLOW_FRAG, "HandChams"));
+ public static final RenderPipeline HAND_CHAMS_SNOW = add(ScreenPipelines.handEffect("hand_chams_snow", SHADER_HAND_CHAMS_SNOW_FRAG, "HandChams"));
+ public static final RenderPipeline HAND_CHAMS_BALATRO = add(ScreenPipelines.handEffect("hand_chams_balatro", SHADER_HAND_CHAMS_BALATRO_FRAG, "HandChams"));
+ public static final RenderPipeline HAND_CHAMS_BLEND = add(ScreenPipelines.handBlendEffect("hand_chams_blend", SHADER_HAND_CHAMS_BLEND_FRAG));
+ /** No depth test; translucent; textured skybox with vanilla sky/fog blending. */
+ public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG = add(WorldPipelines.texturedSkyboxFog("world_skybox_textured_fog", SHADER_POS_TEX_COLOR_SKY_FOG_FRAG, BlendFunction.TRANSLUCENT));
+ /** No depth test; additive; textured skybox with vanilla sky/fog blending. */
+ public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG_ADDITIVE = add(WorldPipelines.texturedSkyboxFog("world_skybox_textured_fog_additive", SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG, PipelineTemplates.ADDITIVE));
+ /** Fullscreen procedural Reimagined skybox with sky/fog blending. */
+ public static final RenderPipeline WORLD_REIMAGINED_SKYBOX_SHADER = add(WorldPipelines.proceduralSkyboxFog("world_reimagined_skybox_shader", SHADER_REIMAGINED_SKYBOX_FRAG));
+ /** Fullscreen procedural ShaderSky (Thunder) with sky/fog blending. */
+ public static final RenderPipeline WORLD_SHADER_SKY_THUNDER = add(ScreenPipelines.screen("world_shader_sky_thunder", SHADER_SHADER_SKY_THUNDER_FRAG, "ShaderSky"));
+ /** Fullscreen procedural ShaderSky (Pulsar) with sky/fog blending. */
+ public static final RenderPipeline WORLD_SHADER_SKY_PULSAR = add(ScreenPipelines.screen("world_shader_sky_pulsar", SHADER_SHADER_SKY_PULSAR_FRAG, "ShaderSky"));
+
+ // ======================
+ // Batched UI shapes (2D)
+ // ======================
+ public static final RenderPipeline UI_CIRCLE_BATCH = add(UiPipelines.batch("ui_circle_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_CIRCLE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ARC_BATCH = add(UiPipelines.batch("ui_arc_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ARC_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ORBIZ_RING_BATCH = add(UiPipelines.batch("ui_orbiz_ring_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT, SHADER_ORBIZ_RING_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_BATCH = add(UiPipelines.batch("ui_rounded_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_FILL_SMOKE_BATCH = add(UiPipelines.batch("ui_rounded_fill_smoke_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_STROKE_BATCH = add(UiPipelines.batch("ui_rounded_stroke_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_STROKE_ANGULAR_BATCH = add(UiPipelines.batch("ui_rounded_stroke_angular_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_CHAMFERED_BATCH = add(UiPipelines.batch("ui_chamfered_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_CHAMFERED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_SHAPE_BATCH = add(UiPipelines.batch("ui_shape_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_SHAPE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_CHAMFERED_STROKE_BATCH = add(UiPipelines.batch("ui_chamfered_stroke_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_STROKE_CORNERS_BATCH = add(UiPipelines.batch("ui_rounded_stroke_corners_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_CORNERS_BATCH = add(UiPipelines.batch("ui_rounded_corners_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_GLOW_BATCH = add(UiPipelines.batch("ui_rounded_glow_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG, PipelineTemplates.ADDITIVE, true));
+ public static final RenderPipeline UI_ROUNDED_SHADOW_BATCH = add(UiPipelines.batch("ui_rounded_shadow_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_ROUNDED_SOFT_SHADOW_BATCH = add(UiPipelines.batch("ui_rounded_soft_shadow_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG, BlendFunction.TRANSLUCENT, true));
+ public static final RenderPipeline UI_RADIAL_GLOW_BATCH = add(UiPipelines.batch("ui_radial_glow_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_RADIAL_GLOW_BATCH_FRAG, PipelineTemplates.ADDITIVE, true));
+ public static final RenderPipeline UI_ROUNDED_TEXTURED_BATCH = add(UiPipelines.batch("ui_rounded_textured_batch",
+ LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_TEX_BATCH_FRAG, BlendFunction.TRANSLUCENT, true, "u_Texture"));
+ public static final RenderPipeline UI_ROUNDED_TEXTURED_MASK_BATCH = add(UiPipelines.batch("ui_rounded_textured_mask_batch",
+ LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG, BlendFunction.TRANSLUCENT, true, "u_Texture"));
+ public static final RenderPipeline UI_BLUR_BATCH = add(UiPipelines.batch("ui_blur_batch",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_UI_BLUR_BATCH_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture"));
+ public static final RenderPipeline UI_BLUR_BATCH_CORNERS = add(UiPipelines.batch("ui_blur_batch_corners",
+ LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_UI_BLUR_BATCH_CORNERS_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture"));
+ public static final RenderPipeline UI_LIQUID_GLASS_BATCH = add(UiPipelines.batch("ui_liquid_glass_batch",
+ LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_UI_LIQUID_GLASS_BATCH_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture", "u_BlurTexture"));
/**
* Compile all registered pipelines (use after resource reload).
@@ -1492,7 +365,7 @@ public enum LoVisualRenderPipelines {
}
public static RenderPipeline.Snippet meshUniforms() {
- return MESH_UNIFORMS;
+ return PipelineTemplates.MESH_UNIFORMS;
}
public static RenderPipeline registerAddonPipeline(RenderPipeline pipeline) {
@@ -1518,4 +391,11 @@ public enum LoVisualRenderPipelines {
}
}
+ /**
+ * LoVisual shader asset identifier for "shaders/<name>".
+ */
+ private static Identifier shader(String name) {
+ return Identifier.fromNamespaceAndPath("lovisual", "shaders/" + name);
+ }
+
}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java
new file mode 100644
index 0000000..7276174
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java
@@ -0,0 +1,60 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render.engine.pipeline.pipelines;
+
+import com.mojang.blaze3d.pipeline.BlendFunction;
+import com.mojang.blaze3d.pipeline.RenderPipeline;
+import com.mojang.blaze3d.platform.BlendFactor;
+import com.mojang.blaze3d.shaders.UniformType;
+import dev.loki.lovisual.render.engine.pipeline.DepthTestFunction;
+import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
+import net.minecraft.resources.Identifier;
+
+/**
+ * Shared building blocks for the LoVisual pipeline factories: uniform snippets, blend
+ * presets and the common depth/blend/cull tail that every custom pipeline applies.
+ */
+public final class PipelineTemplates {
+ /** Additive blending (SRC_ALPHA, ONE) shared by all additive LoVisual pipelines. */
+ public static final BlendFunction ADDITIVE = new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE);
+
+ public static final RenderPipeline.Snippet MESH_UNIFORMS = uniformSnippet("MeshData");
+ public static final RenderPipeline.Snippet FOG_UNIFORMS = uniformSnippet("Fog");
+ public static final RenderPipeline.Snippet UI_BATCH_UNIFORMS = uniformSnippet("UIBatch");
+
+ private PipelineTemplates() {
+ }
+
+ /**
+ * Snippet declaring a single {@link UniformType#UNIFORM_BUFFER} uniform.
+ */
+ public static RenderPipeline.Snippet uniformSnippet(String name) {
+ return new ExtendedRenderPipelineBuilder()
+ .withUniform(name, UniformType.UNIFORM_BUFFER)
+ .buildSnippet();
+ }
+
+ /**
+ * Location identifier for a "pipeline/<name>" entry in the lovisual namespace.
+ */
+ public static Identifier location(String name) {
+ return Identifier.fromNamespaceAndPath("lovisual", "pipeline/" + name);
+ }
+
+ /**
+ * Applies the shared pipeline tail (depth test, depth write off, blend, cull) and builds.
+ */
+ public static RenderPipeline finish(ExtendedRenderPipelineBuilder builder, boolean depthTest, BlendFunction blend, boolean cull) {
+ return builder
+ .withDepthTestFunction(depthTest ? DepthTestFunction.GEQUAL_DEPTH_TEST : DepthTestFunction.NO_DEPTH_TEST)
+ .withDepthWrite(false)
+ .withBlend(blend)
+ .withCull(cull)
+ .build();
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java
new file mode 100644
index 0000000..6acea68
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java
@@ -0,0 +1,72 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render.engine.pipeline.pipelines;
+
+import com.mojang.blaze3d.PrimitiveTopology;
+import com.mojang.blaze3d.pipeline.BlendFunction;
+import com.mojang.blaze3d.pipeline.RenderPipeline;
+import com.mojang.blaze3d.shaders.UniformType;
+import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
+import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
+import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
+import net.minecraft.client.renderer.RenderPipelines;
+import net.minecraft.resources.Identifier;
+
+/**
+ * Factory templates for LoVisual's fullscreen pos2 pipelines (post effects, hand effects,
+ * shader skies) plus the vanilla-textured GUI lookup pipeline. Samplers are declared
+ * before the single uniform buffer, matching the original declaration order.
+ */
+public final class ScreenPipelines {
+ private ScreenPipelines() {
+ }
+
+ /**
+ * Minimal vanilla-textured pipeline used for GUI texture lookups.
+ */
+ public static RenderPipeline guiTextureLookup() {
+ return new ExtendedRenderPipelineBuilder(RenderPipelines.GUI_TEXTURED_SNIPPET)
+ .withLocation(PipelineTemplates.location("gui_texture_lookup"))
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_GUI_TEXTURE_LOOKUP_FRAG)
+ .build();
+ }
+
+ /**
+ * Fullscreen pos2 pass: damage-tint vertex shader, no depth test, translucent, no culling.
+ */
+ public static RenderPipeline screen(String name, Identifier fragmentShader, String uniform, String... samplers) {
+ ExtendedRenderPipelineBuilder builder = base(name)
+ .withVertexFormat(LoVisualVertexFormats.POS2, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_DAMAGE_TINT_VERT)
+ .withFragmentShader(fragmentShader);
+ for (String sampler : samplers) {
+ builder.withSampler(sampler);
+ }
+ builder.withUniform(uniform, UniformType.UNIFORM_BUFFER);
+ return PipelineTemplates.finish(builder, false, BlendFunction.TRANSLUCENT, false);
+ }
+
+ /**
+ * Fullscreen hand effect with the standard "u_Src"/"u_Mask" sampler pair.
+ */
+ public static RenderPipeline handEffect(String name, Identifier fragmentShader, String uniform) {
+ return screen(name, fragmentShader, uniform, "u_Src", "u_Mask");
+ }
+
+ /**
+ * Fullscreen chams blend effect sampling two shader textures plus the mask.
+ */
+ public static RenderPipeline handBlendEffect(String name, Identifier fragmentShader) {
+ return screen(name, fragmentShader, "HandBlend", "u_Shader1", "u_Shader2", "u_Mask");
+ }
+
+ private static ExtendedRenderPipelineBuilder base(String name) {
+ return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS)
+ .withLocation(PipelineTemplates.location(name));
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java
new file mode 100644
index 0000000..40ae811
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java
@@ -0,0 +1,96 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render.engine.pipeline.pipelines;
+
+import com.mojang.blaze3d.PrimitiveTopology;
+import com.mojang.blaze3d.pipeline.BlendFunction;
+import com.mojang.blaze3d.pipeline.RenderPipeline;
+import com.mojang.blaze3d.shaders.UniformType;
+import com.mojang.blaze3d.vertex.VertexFormat;
+import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
+import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
+import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
+import net.minecraft.resources.Identifier;
+
+/**
+ * Factory templates for LoVisual's UI (2D) pipelines: the base colored/textured set and
+ * the batched SDF shape pipelines built on the local-color rect-params vertex formats.
+ * Samplers are declared before extra uniform buffers, matching the original order.
+ */
+public final class UiPipelines {
+ private UiPipelines() {
+ }
+
+ /**
+ * UI colored primitives (pos2 + color); the canvas check in the builder still applies.
+ */
+ public static RenderPipeline colored(String name, PrimitiveTopology topology) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(LoVisualVertexFormats.POS2_COLOR, topology)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), false, BlendFunction.TRANSLUCENT, true);
+ }
+
+ /**
+ * UI textured triangles (pos2 + tex + color); {@code uniform} may be null.
+ */
+ public static RenderPipeline textured(String name, Identifier vertexShader, Identifier fragmentShader,
+ BlendFunction blend, boolean cull, String uniform, String... samplers) {
+ ExtendedRenderPipelineBuilder builder = base(name)
+ .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(vertexShader)
+ .withFragmentShader(fragmentShader);
+ for (String sampler : samplers) {
+ builder.withSampler(sampler);
+ }
+ if (uniform != null) {
+ builder.withUniform(uniform, UniformType.UNIFORM_BUFFER);
+ }
+ return PipelineTemplates.finish(builder, false, blend, cull);
+ }
+
+ /**
+ * UI text with liquid glass sampling; MSDF variants add the "MsdfText" uniform.
+ */
+ public static RenderPipeline textLiquidGlass(String name, Identifier fragmentShader, boolean msdf) {
+ ExtendedRenderPipelineBuilder builder = base(name)
+ .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_TEXT_VERT)
+ .withFragmentShader(fragmentShader)
+ .withSampler("u_Texture")
+ .withSampler("u_SceneTexture")
+ .withSampler("u_BlurTexture")
+ .withUniform("UIBatch", UniformType.UNIFORM_BUFFER);
+ if (msdf) {
+ builder.withUniform("MsdfText", UniformType.UNIFORM_BUFFER);
+ }
+ return PipelineTemplates.finish(builder, false, BlendFunction.TRANSLUCENT, true);
+ }
+
+ /**
+ * Batched SDF shape pipeline (local-color rect-params formats) with the shared UI batch
+ * uniforms; optional texture samplers keep their original declaration order.
+ */
+ public static RenderPipeline batch(String name, VertexFormat format, Identifier vertexShader, Identifier fragmentShader,
+ BlendFunction blend, boolean cull, String... samplers) {
+ ExtendedRenderPipelineBuilder builder = new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS, PipelineTemplates.UI_BATCH_UNIFORMS)
+ .withLocation(PipelineTemplates.location(name))
+ .withVertexFormat(format, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(vertexShader)
+ .withFragmentShader(fragmentShader);
+ for (String sampler : samplers) {
+ builder.withSampler(sampler);
+ }
+ return PipelineTemplates.finish(builder, false, blend, cull);
+ }
+
+ private static ExtendedRenderPipelineBuilder base(String name) {
+ return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS)
+ .withLocation(PipelineTemplates.location(name));
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java
new file mode 100644
index 0000000..1bb0e5d
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java
@@ -0,0 +1,151 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render.engine.pipeline.pipelines;
+
+import com.mojang.blaze3d.PrimitiveTopology;
+import com.mojang.blaze3d.pipeline.BlendFunction;
+import com.mojang.blaze3d.pipeline.RenderPipeline;
+import com.mojang.blaze3d.shaders.UniformType;
+import com.mojang.blaze3d.vertex.DefaultVertexFormat;
+import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
+import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
+import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
+import net.minecraft.resources.Identifier;
+
+/**
+ * Factory templates for LoVisual's world-space (3D) pipelines. They all share the mesh
+ * uniform snippet, disable depth writing and culling; only depth test, blending, shaders
+ * and vertex format vary. Shader identifiers come from the public facade constants.
+ */
+public final class WorldPipelines {
+ private WorldPipelines() {
+ }
+
+ /**
+ * Solid colored triangles; {@code depthTest} selects GEQUAL vs no depth test.
+ */
+ public static RenderPipeline colored(String name, boolean depthTest, BlendFunction blend) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, blend, false);
+ }
+
+ /**
+ * Smooth debug lines rendered with the plain color shader.
+ */
+ public static RenderPipeline coloredLines(String name, boolean depthTest) {
+ return PipelineTemplates.finish(base(name)
+ .withLineSmooth()
+ .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, PrimitiveTopology.DEBUG_LINES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, BlendFunction.TRANSLUCENT, false);
+ }
+
+ /**
+ * Screen-space wide line expansion; triangles, never depends on glLineWidth.
+ */
+ public static RenderPipeline wideLines(String name, boolean depthTest) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_WIDE_LINE_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, BlendFunction.TRANSLUCENT, false);
+ }
+
+ /**
+ * Textured triangles sampling "u_Texture".
+ */
+ public static RenderPipeline textured(String name, boolean depthTest, BlendFunction blend) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_FRAG)
+ .withSampler("u_Texture"), depthTest, blend, false);
+ }
+
+ /**
+ * Textured triangles with shader-side tint ("TextureTint" uniform), additive blending.
+ */
+ public static RenderPipeline tintedTextured(String name, boolean depthTest) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_TINT_FRAG)
+ .withSampler("u_Texture")
+ .withUniform("TextureTint", UniformType.UNIFORM_BUFFER), depthTest, PipelineTemplates.ADDITIVE, false);
+ }
+
+ /**
+ * World decal quads with SDF fill/stroke.
+ */
+ public static RenderPipeline decalSdf(String name, boolean depthTest) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_PARAMS2_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_WORLD_DECAL_SDF_FRAG), depthTest, BlendFunction.TRANSLUCENT, false);
+ }
+
+ /**
+ * World text quads; MSDF variants add the "MsdfText" uniform after "u_Texture".
+ */
+ public static RenderPipeline worldText(String name, boolean depthTest, Identifier fragmentShader, boolean msdf) {
+ ExtendedRenderPipelineBuilder builder = base(name)
+ .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_TEXT_VERT)
+ .withFragmentShader(fragmentShader)
+ .withSampler("u_Texture");
+ if (msdf) {
+ builder.withUniform("MsdfText", UniformType.UNIFORM_BUFFER);
+ }
+ return PipelineTemplates.finish(builder, depthTest, BlendFunction.TRANSLUCENT, false);
+ }
+
+ /**
+ * Textured triangles with procedural sun shading ("SkySun" uniform), additive.
+ */
+ public static RenderPipeline skySun(String name) {
+ return PipelineTemplates.finish(base(name)
+ .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT)
+ .withFragmentShader(LoVisualRenderPipelines.SHADER_SKY_SUN_FRAG)
+ .withUniform("SkySun", UniformType.UNIFORM_BUFFER), false, PipelineTemplates.ADDITIVE, false);
+ }
+
+ /**
+ * Textured skybox with vanilla sky/fog blending ("SkyboxShader" uniform + fog snippet).
+ */
+ public static RenderPipeline texturedSkyboxFog(String name, Identifier fragmentShader, BlendFunction blend) {
+ return PipelineTemplates.finish(fogBase(name)
+ .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_SKY_FOG_VERT)
+ .withFragmentShader(fragmentShader)
+ .withSampler("u_Texture")
+ .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER), false, blend, false);
+ }
+
+ /**
+ * Fullscreen procedural skybox with sky/fog blending ("SkyboxShader" uniform + fog snippet).
+ */
+ public static RenderPipeline proceduralSkyboxFog(String name, Identifier fragmentShader) {
+ return PipelineTemplates.finish(fogBase(name)
+ .withVertexFormat(LoVisualVertexFormats.POS2, PrimitiveTopology.TRIANGLES)
+ .withVertexShader(LoVisualRenderPipelines.SHADER_DAMAGE_TINT_VERT)
+ .withFragmentShader(fragmentShader)
+ .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER), false, BlendFunction.TRANSLUCENT, false);
+ }
+
+ private static ExtendedRenderPipelineBuilder base(String name) {
+ return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS)
+ .withLocation(PipelineTemplates.location(name));
+ }
+
+ private static ExtendedRenderPipelineBuilder fogBase(String name) {
+ return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS, PipelineTemplates.FOG_UNIFORMS)
+ .withLocation(PipelineTemplates.location(name));
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java
new file mode 100644
index 0000000..dd8a0c7
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java
@@ -0,0 +1,88 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.render.engine.uniform.impl;
+
+import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
+
+import com.mojang.blaze3d.buffers.GpuBufferSlice;
+import com.mojang.blaze3d.buffers.Std140Builder;
+import com.mojang.blaze3d.buffers.Std140SizeCalculator;
+import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
+
+public enum HandNoiseUniforms {
+ ;
+ public static final int SIZE = new Std140SizeCalculator()
+ .putFloat().putFloat().putFloat().putFloat() // u_Color
+ .putFloat().putFloat().putFloat().putFloat() // u_Glow
+ .putFloat().putFloat().putFloat().putFloat() // u_Params0
+ .putFloat().putFloat().putFloat().putFloat() // u_Params1
+ .putFloat().putFloat().putFloat().putFloat() // u_Params2
+ .get();
+
+ private static final Data DATA = new Data();
+ private static final String UNIFORM_NAME = "LoVisual - Hand Noise UBO";
+ private static final int EXPECTED_WRITES_PER_FRAME = 16;
+
+ public static void update(
+ float colorR, float colorG, float colorB, float colorA,
+ float glowR, float glowG, float glowB, float glowA,
+ float edgeWidth, float quality, float octaves, float time,
+ float resX, float resY, float scale, float speed,
+ float contrast, float breathe, float glowStrength, float density
+ ) {
+ DATA.colorR = colorR;
+ DATA.colorG = colorG;
+ DATA.colorB = colorB;
+ DATA.colorA = colorA;
+ DATA.glowR = glowR;
+ DATA.glowG = glowG;
+ DATA.glowB = glowB;
+ DATA.glowA = glowA;
+ DATA.edgeWidth = edgeWidth;
+ DATA.quality = quality;
+ DATA.octaves = octaves;
+ DATA.time = time;
+ DATA.resX = resX;
+ DATA.resY = resY;
+ DATA.scale = scale;
+ DATA.speed = speed;
+ DATA.contrast = contrast;
+ DATA.breathe = breathe;
+ DATA.glowStrength = glowStrength;
+ DATA.density = density;
+ LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA);
+ }
+
+ public static GpuBufferSlice get() {
+ return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
+ }
+
+ private static final class Data implements LoVisualUniformAllocator.UniformWriter {
+ private float colorR, colorG, colorB, colorA;
+ private float glowR, glowG, glowB, glowA;
+ private float edgeWidth, quality, octaves, time;
+ private float resX, resY, scale, speed;
+ private float contrast, breathe, glowStrength, density;
+
+ @Override
+ public void write(java.nio.ByteBuffer buffer) {
+ Std140Builder.intoBuffer(buffer)
+ .putFloat(colorR).putFloat(colorG).putFloat(colorB).putFloat(colorA)
+ .putFloat(glowR).putFloat(glowG).putFloat(glowB).putFloat(glowA)
+ .putFloat(edgeWidth).putFloat(quality).putFloat(octaves).putFloat(time)
+ .putFloat(resX).putFloat(resY).putFloat(scale).putFloat(speed)
+ .putFloat(contrast).putFloat(breathe).putFloat(glowStrength).putFloat(density);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return false;
+ }
+ }
+
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java
new file mode 100644
index 0000000..0788c45
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation;
+
+/**
+ * Кривые анимации для equip-движений руки. Чистая математика без Minecraft.
+ */
+public enum EaseCurves {
+ ;
+
+ /**
+ * Ease-out-back: перелетает чуть дальше 1.0 перед установкой, давая движению вес.
+ *
+ * @param c1 сила «отдачи» (оригинал — 1.70158)
+ */
+ public static float easeOutBack(float x, float c1) {
+ float c3 = c1 + 1.0f;
+ float t = x - 1.0f;
+ return 1.0f + c3 * t * t * t + c1 * t * t;
+ }
+
+ /**
+ * Перекладывает линейный [0,1] equip-прогресс через ease-out-back,
+ * затем подмешивает по intensity: 0 — ванильная линейность, 1 — полная кривая.
+ */
+ public static float shapeEquipProgress(float linear, float c1, float intensity) {
+ float eased = easeOutBack(linear, c1);
+ float amount = Math.max(0.0f, Math.min(1.0f, intensity));
+ return linear + (eased - linear) * amount;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java
new file mode 100644
index 0000000..7f48244
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java
@@ -0,0 +1,32 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation;
+
+/**
+ * Единая цепочка обработки equip-прогресса руки: сначала кривая HoldMyItems
+ * (перелёт через ease-out-back на полном диапазоне 0..1), затем занижение
+ * амплитуды от ViewModel. Порядок важен: кривая должна получать полный
+ * диапазон, а ViewModel — масштабировать уже готовое движение, иначе
+ * конечная позиция предмета уезжает.
+ */
+public enum EquipProgressComposer {
+ ;
+
+ public static float compose(float linear,
+ boolean curveEnabled, float curveC1, float curveIntensity,
+ boolean lowerEnabled, float lowering) {
+ float progress = linear;
+ if (curveEnabled) {
+ progress = EaseCurves.shapeEquipProgress(progress, curveC1, curveIntensity);
+ }
+ if (lowerEnabled) {
+ progress *= lowering;
+ }
+ return progress;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java
new file mode 100644
index 0000000..1f0c442
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java
@@ -0,0 +1,119 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation;
+
+/**
+ * Frame-rate independent smooth 0..1 transition value driven by a boolean state.
+ *
+ * The contract is intentionally strict so animations can never fight each other:
+ * the owner computes the target once per frame with {@link #setTarget(boolean)}
+ * and advances the value exactly once per frame with {@link #update(float)}.
+ * Renderers only read {@link #value()}. The value approaches the target with
+ * exponential easing {@code value += (target - value) * (1 - exp(-speed * delta))},
+ * which is monotonic, symmetric for appear/disappear, and identical in wall-clock
+ * time regardless of frame rate.
+ *
+ * This class is pure Java: no Minecraft types, safe to unit test.
+ */
+public final class SmoothValue {
+
+ /**
+ * When the remaining distance to the target drops below this, the value snaps
+ * exactly onto the target so it converges in finite time instead of forever
+ * trailing a microscopic difference.
+ */
+ private static final float CONVERGENCE_EPSILON = 1.0e-4f;
+
+ private final float speed;
+ private float value;
+ private float target;
+
+ /**
+ * @param speed approach rate in 1/delta units; higher is faster.
+ * With delta measured in ticks (20 per second), a speed of
+ * {@code s} needs roughly {@code ln(100) / s} ticks to close 99% of a transition.
+ */
+ public SmoothValue(float speed) {
+ this(speed, 0.0f);
+ }
+
+ public SmoothValue(float speed, float initialValue) {
+ if (!isFinite(speed) || speed <= 0.0f) {
+ throw new IllegalArgumentException("speed must be finite and positive: " + speed);
+ }
+ this.speed = speed;
+ this.value = clamp01(initialValue);
+ this.target = this.value;
+ }
+
+ /**
+ * Sets the goal for the next {@link #update(float)} calls: 1 when the animated
+ * state is active, 0 when it is not. Does not move the value by itself.
+ */
+ public void setTarget(boolean active) {
+ target = active ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Advances the value one step toward the target. Call exactly once per frame.
+ *
+ * @param delta elapsed time since the previous update, in the same units the
+ * speed was configured for (ticks in this client). Non-positive
+ * or broken deltas are ignored.
+ */
+ public void update(float delta) {
+ if (!isFinite(delta) || delta <= 0.0f) {
+ return;
+ }
+
+ float factor = 1.0f - (float) Math.exp(-speed * delta);
+ if (factor <= 0.0f) {
+ return;
+ }
+
+ value += (target - value) * factor;
+ value = clamp01(value);
+
+ if (Math.abs(target - value) <= CONVERGENCE_EPSILON) {
+ value = target;
+ }
+ }
+
+ /**
+ * Teleports both value and target: no transition, the next frame renders settled.
+ */
+ public void setInstant(boolean active) {
+ target = active ? 1.0f : 0.0f;
+ value = target;
+ }
+
+ /** Current smoothed position in 0..1. */
+ public float value() {
+ return value;
+ }
+
+ /** Current goal in 0..1 as last set by {@link #setTarget(boolean)}. */
+ public float target() {
+ return target;
+ }
+
+ /** True once the value has fully reached the target. */
+ public boolean isSettled() {
+ return value == target;
+ }
+
+ private static float clamp01(float v) {
+ if (v < 0.0f) return 0.0f;
+ if (v > 1.0f) return 1.0f;
+ return v;
+ }
+
+ private static boolean isFinite(float v) {
+ return !Float.isNaN(v) && !Float.isInfinite(v);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java
new file mode 100644
index 0000000..32c92fd
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java
@@ -0,0 +1,60 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+/**
+ * Чистая математика синусоидального покачивания предмета при ходьбе/беге.
+ *
+ * Фаза берётся из накопителя шагов игрока ({@code walkAnimation.position()}),
+ * амплитуда — из нормализованной скорости ({@code walkAnimation.speed()},
+ * 0..1). Один цикл латерали соответствует двум шагам, вертикаль проседает
+ * дважды за цикл с лёгким сдвигом фазы (как у ванильного боба камеры).
+ * Класс чистый: без Minecraft-типов, покрыт юнит-тестами.
+ */
+public enum BobMath {
+ ;
+
+ /** Радиан на единицу пройденной фазы (та же частота шагов, что у конечностей). */
+ public static final float STEP_RATE = 0.6662f;
+
+ /** Сдвиг фазы вертикали, чтобы проседание не совпадало с крайними точками латерали. */
+ static final float VERTICAL_PHASE_OFFSET = 0.2f;
+
+ /**
+ * Амплитуда боба по нормализованной скорости: 0 — стоим, 1 — бег/полная ходьба.
+ * Некорректные входы (NaN/Inf) дают 0.
+ */
+ public static float amplitude(float speed01) {
+ if (Float.isNaN(speed01) || Float.isInfinite(speed01)) {
+ return 0.0f;
+ }
+ if (speed01 <= 0.0f) {
+ return 0.0f;
+ }
+ return Math.min(1.0f, speed01);
+ }
+
+ /** Латеральная компонента, -1..1, период — два шага. */
+ public static float lateral(float phase) {
+ return (float) Math.sin(phase * STEP_RATE);
+ }
+
+ /** Вертикальная компонента, -1..0: проседание дважды за цикл. */
+ public static float vertical(float phase) {
+ float c = (float) Math.cos(phase * STEP_RATE - VERTICAL_PHASE_OFFSET);
+ return -Math.abs(c);
+ }
+
+ /**
+ * Точка фазы, где латеральная компонента достигает максимума:
+ * полезна тестам для проверки симметрии цикла.
+ */
+ public static float lateralPeakPhase() {
+ return (float) (Math.PI / 2.0 / STEP_RATE);
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java
new file mode 100644
index 0000000..305ef0d
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java
@@ -0,0 +1,125 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+import dev.loki.lovisual.util.animation.SmoothValue;
+
+/**
+ * Покадровое состояние динамики рук: сводит sway, дрейф использования и
+ * появление при входе в мир. Обновляется ровно один раз за кадр
+ * ({@link #beginFrame}), рендер только читает значения — это исключает
+ * двойные шаги, когда кадр рендерит обе руки.
+ *
+ * Player/level передаются как Object-идентичности: смена игрока или мира
+ * перезапускает анимацию появления и гасит ложные дельты взгляда.
+ * Класс чистый: без Minecraft-типов, покрыт юнит-тестами.
+ */
+public final class HandAnimState {
+
+ /** Резкая смена взгляда больше этих пределов считается телепортом, а не движением мыши. */
+ static final float TELEPORT_YAW_DEG = 45.0f;
+ static final float TELEPORT_PITCH_DEG = 25.0f;
+ static final float MAX_FRAME_DELTA_TICKS = 5.0f;
+
+ /** Скорость дрейфа использования (99% за ~ln(100)/speed тиков). */
+ static final float DRIFT_SPEED = 1.8f;
+ /** Скорость появления при входе: ~10 тиков до почти полной посадки. */
+ static final float JOIN_SPEED = 0.45f;
+
+ private final SwayState sway = new SwayState();
+ private final SmoothValue driftMain = new SmoothValue(DRIFT_SPEED);
+ private final SmoothValue driftOff = new SmoothValue(DRIFT_SPEED);
+ private final SmoothValue join = new SmoothValue(JOIN_SPEED);
+
+ private float lastYaw = Float.NaN;
+ private float lastPitch = Float.NaN;
+ private Object lastPlayer;
+ private Object lastLevel;
+
+ /**
+ * Один шаг на кадр.
+ *
+ * @param player идентичность игрока (для смены мира/респавна)
+ * @param level идентичность мира
+ * @param yaw текущий yaw взгляда, градусы (любой диапазон)
+ * @param pitch текущий pitch взгляда, градусы
+ * @param usingMain предмет используется в главной руке
+ * @param usingOff предмет используется во второй руке
+ * @param deltaTicks реальное время кадра в тиках
+ */
+ public void beginFrame(Object player, Object level, float yaw, float pitch,
+ boolean usingMain, boolean usingOff, float deltaTicks) {
+ if (player == null) {
+ reset();
+ return;
+ }
+
+ if (player != lastPlayer || level != lastLevel) {
+ lastPlayer = player;
+ lastLevel = level;
+ join.setInstant(false);
+ lastYaw = Float.NaN;
+ lastPitch = Float.NaN;
+ }
+
+ float dt = SwayState.sanitize(deltaTicks);
+ if (Float.isNaN(lastYaw)) {
+ lastYaw = yaw;
+ lastPitch = pitch;
+ return;
+ }
+
+ float deltaYaw = SwayState.wrapDegrees(yaw - lastYaw);
+ float deltaPitch = pitch - lastPitch;
+ lastYaw = yaw;
+ lastPitch = pitch;
+
+ boolean lookJump = Math.abs(deltaYaw) > TELEPORT_YAW_DEG
+ || Math.abs(deltaPitch) > TELEPORT_PITCH_DEG;
+
+ if (dt > 0.0f) {
+ if (lookJump) {
+ sway.reset();
+ } else {
+ sway.update(deltaYaw, deltaPitch, Math.min(dt, MAX_FRAME_DELTA_TICKS));
+ }
+ driftMain.setTarget(usingMain);
+ driftMain.update(dt);
+ driftOff.setTarget(usingOff);
+ driftOff.update(dt);
+ join.setTarget(true);
+ join.update(dt);
+ }
+ }
+
+ /** Полный сброс: следующий кадр считается первым после входа в мир. */
+ public void reset() {
+ sway.reset();
+ driftMain.setInstant(false);
+ driftOff.setInstant(false);
+ join.setInstant(false);
+ lastYaw = Float.NaN;
+ lastPitch = Float.NaN;
+ lastPlayer = null;
+ lastLevel = null;
+ }
+
+ public SwayState sway() {
+ return sway;
+ }
+
+ /** Сглаженный прогресс дрейфа для указанной руки, 0..1. */
+ public float drift(boolean mainHand) {
+ return (mainHand ? driftMain : driftOff).value();
+ }
+
+ /** Прогресс появления при входе в мир, 0..1 (1 — предмет встал на место). */
+ public float joinProgress() {
+ return join.value();
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java
new file mode 100644
index 0000000..9d9e5dd
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java
@@ -0,0 +1,160 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+/**
+ * Сборка итоговой позы руки из отдельных эффектов: sway от обзора, боб от
+ * ходьбы, дрейф к центру при использовании предмета и «въезд» при входе в мир.
+ *
+ * Конвенции знаков соответствуют пространству ванильного рендера руки:
+ * +X — влево на экране (правая рука стоит при X<0), +Y — вверх, +Z — к
+ * зрителю; углы в градусах. Смещения — в условных блоках.
+ * Класс чистый: без Minecraft-типов, покрыт юнит-тестами.
+ */
+public enum HandPoseMath {
+ ;
+
+ /** Нулевая поза — применять трансформы не нужно. */
+ public static final Pose ZERO = new Pose(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
+
+ private static final float SWAY_POS_PER_DEG = 0.010f;
+ private static final float SWAY_ROT_Y_PER_DEG = 1.4f;
+ private static final float SWAY_ROT_X_PER_DEG = 1.1f;
+ private static final float SWAY_ROT_Z_PER_DEG = 0.5f;
+
+ private static final float BOB_POS_X = 0.045f;
+ private static final float BOB_POS_Y = 0.035f;
+ private static final float BOB_ROT_Z_DEG = 2.5f;
+ private static final float BOB_ROT_X_DEG = 3.0f;
+
+ private static final float DRIFT_POS_X = 0.10f;
+ private static final float DRIFT_POS_Z = 0.05f;
+ private static final float DRIFT_ROT_Y_DEG = 10.0f;
+ private static final float DRIFT_ROT_Z_DEG = 9.0f;
+
+ private static final float JOIN_DROP = 0.85f;
+ private static final float JOIN_TILT_DEG = 18.0f;
+
+ /**
+ * Отставание от поворота камеры: камера повернула вправо (yaw+) — предмет
+ * сместился влево на экране (+X) и довернулся назад (ry-), взгляд вниз
+ * (pitch+) — предмет приподнялся (+Y) и задрался (rx-).
+ */
+ public static Pose sway(float yawLagDeg, float pitchLagDeg, float strength) {
+ float s = scale(strength);
+ if (s <= 0.0f) {
+ return ZERO;
+ }
+ return new Pose(
+ yawLagDeg * SWAY_POS_PER_DEG * s,
+ pitchLagDeg * SWAY_POS_PER_DEG * s,
+ 0.0f,
+ -pitchLagDeg * SWAY_ROT_X_PER_DEG * s,
+ -yawLagDeg * SWAY_ROT_Y_PER_DEG * s,
+ yawLagDeg * SWAY_ROT_Z_PER_DEG * s
+ );
+ }
+
+ /** Покачивание при ходьбе: {@code phase} — позиция шага, {@code amplitude01} — 0..1 от скорости. */
+ public static Pose bob(float phase, float amplitude01, float strength) {
+ float a = clamp01(amplitude01) * scale(strength);
+ if (a <= 0.0f) {
+ return ZERO;
+ }
+ float lateral = BobMath.lateral(phase);
+ float vertical = BobMath.vertical(phase);
+ return new Pose(
+ -lateral * BOB_POS_X * a,
+ vertical * BOB_POS_Y * a,
+ 0.0f,
+ vertical * BOB_ROT_X_DEG * a,
+ 0.0f,
+ -lateral * BOB_ROT_Z_DEG * a
+ );
+ }
+
+ /**
+ * Дрейф используемого предмета (еда/зелье/щит) к центру экрана.
+ * Правая рука тянется влево (+X), левая — вправо (-X), плюс лёгкий
+ * наклон внутрь.
+ */
+ public static Pose drift(float progress01, float strength, boolean rightArm) {
+ float s = clamp01(progress01) * scale(strength);
+ if (s <= 0.0f) {
+ return ZERO;
+ }
+ float side = rightArm ? 1.0f : -1.0f;
+ return new Pose(
+ side * DRIFT_POS_X * s,
+ 0.0f,
+ -DRIFT_POS_Z * s,
+ 0.0f,
+ -side * DRIFT_ROT_Y_DEG * s,
+ side * DRIFT_ROT_Z_DEG * s
+ );
+ }
+
+ /** Появление при входе в мир: предмет въезжает снизу и распрямляется. */
+ public static Pose joinDrop(float progress01) {
+ float p = clamp01(progress01);
+ float remain = 1.0f - p;
+ if (remain <= 0.0f) {
+ return ZERO;
+ }
+ float square = remain * remain;
+ return new Pose(0.0f, -JOIN_DROP * square, 0.0f, JOIN_TILT_DEG * square, 0.0f, 0.0f);
+ }
+
+ /** Поэлементная сумма поз. */
+ public static Pose add(Pose a, Pose b) {
+ if (a == ZERO) return b;
+ if (b == ZERO) return a;
+ return new Pose(
+ a.dx() + b.dx(),
+ a.dy() + b.dy(),
+ a.dz() + b.dz(),
+ a.rx() + b.rx(),
+ a.ry() + b.ry(),
+ a.rz() + b.rz()
+ );
+ }
+
+ static float clamp01(float v) {
+ if (Float.isNaN(v)) return 0.0f;
+ if (v < 0.0f) return 0.0f;
+ if (v > 1.0f) return 1.0f;
+ return v;
+ }
+
+ /**
+ * Множитель силы настройки: линейный, без потолка — диапазон настроек 0..2
+ * должен именно усиливать эффект, а не обрезаться. Отрицательные и
+ * сломанные значения дают ноль.
+ */
+ static float scale(float strength) {
+ if (Float.isNaN(strength) || Float.isInfinite(strength)) return 0.0f;
+ return Math.max(0.0f, strength);
+ }
+
+ /**
+ * Итоговая поза руки: смещение (dx, dy, dz) и поворот (rx, ry, rz в градусах),
+ * применяемые вокруг точки взгляда до ванильных трансформов руки.
+ */
+ public record Pose(float dx, float dy, float dz, float rx, float ry, float rz) {
+
+ /** Все компоненты по модулю не больше epsilon — трансформ можно не применять. */
+ public boolean isSignificant(float epsilon) {
+ return Math.abs(dx) > epsilon
+ || Math.abs(dy) > epsilon
+ || Math.abs(dz) > epsilon
+ || Math.abs(rx) > epsilon
+ || Math.abs(ry) > epsilon
+ || Math.abs(rz) > epsilon;
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java
new file mode 100644
index 0000000..c4c2a44
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java
@@ -0,0 +1,117 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+/**
+ * Накопитель «отставания» предмета от поворота камеры (view sway).
+ *
+ * Модель: дельты взгляда за кадр (в градусах) накапливаются в отставание
+ * {@code lag}, ограниченное сверху, и экспоненциально затухают к нулю
+ * (RELAX). Отображаемое значение догоняет накопленное отставание
+ * экспонентой (RESPONSE). Всё в градусах, время — в тиках, поэтому
+ * поведение не зависит от частоты кадров.
+ *
+ * Класс чистый: без Minecraft-типов, покрыт юнит-тестами.
+ */
+public final class SwayState {
+
+ public static final float MAX_YAW_LAG_DEG = 6.0f;
+ public static final float MAX_PITCH_LAG_DEG = 4.0f;
+
+ /** Скорость догоняния отображаемого отставания, 1/тик. */
+ static final float RESPONSE_PER_TICK = 6.0f;
+ /** Скорость возврата накопленного отставания к нулю, 1/тик. */
+ static final float RELAX_PER_TICK = 0.25f;
+ /** Верхняя граница шага, чтобы пауза/лаг не давали рывок. */
+ static final float MAX_DELTA_TICKS = 5.0f;
+
+ /** Ниже этого всё состояние защёлкивается в точный ноль — конечное время затухания. */
+ private static final float SETTLE_EPSILON = 1.0e-4f;
+
+ private float yawLag; // накопленное отставание по yaw, градусы
+ private float pitchLag; // накопленное отставание по pitch, градусы
+ private float yawSmooth; // отображаемое отставание по yaw, градусы
+ private float pitchSmooth;
+
+ /**
+ * Продвигает состояние на один кадр.
+ *
+ * @param deltaYawDeg изменение yaw камеры за кадр (может быть любым по знаку)
+ * @param deltaPitchDeg изменение pitch камеры за кадр
+ * @param deltaTicks реальное время кадра в тиках; неположительное пропускается
+ */
+ public void update(float deltaYawDeg, float deltaPitchDeg, float deltaTicks) {
+ float dt = sanitize(deltaTicks);
+ if (dt <= 0.0f) {
+ return;
+ }
+ yawLag = clamp(yawLag + sanitize(deltaYawDeg), -MAX_YAW_LAG_DEG, MAX_YAW_LAG_DEG);
+ pitchLag = clamp(pitchLag + sanitize(deltaPitchDeg), -MAX_PITCH_LAG_DEG, MAX_PITCH_LAG_DEG);
+
+ float follow = 1.0f - (float) Math.exp(-RESPONSE_PER_TICK * dt);
+ yawSmooth += (yawLag - yawSmooth) * follow;
+ pitchSmooth += (pitchLag - pitchSmooth) * follow;
+
+ float relax = (float) Math.exp(-RELAX_PER_TICK * dt);
+ yawLag *= relax;
+ pitchLag *= relax;
+
+ if (Math.abs(yawLag) <= SETTLE_EPSILON && Math.abs(pitchLag) <= SETTLE_EPSILON
+ && Math.abs(yawSmooth) <= SETTLE_EPSILON && Math.abs(pitchSmooth) <= SETTLE_EPSILON) {
+ reset();
+ }
+ }
+
+ /** Мгновенный сброс: предмет стоит ровно, без хвостов. */
+ public void reset() {
+ yawLag = 0.0f;
+ pitchLag = 0.0f;
+ yawSmooth = 0.0f;
+ pitchSmooth = 0.0f;
+ }
+
+ /** Сглаженное отставание по yaw в градусах. */
+ public float yawLag() {
+ return yawSmooth;
+ }
+
+ /** Сглаженное отставание по pitch в градусах. */
+ public float pitchLag() {
+ return pitchSmooth;
+ }
+
+ /** Обе оси в нуле — считать можно не применять трансформ вовсе. */
+ public boolean isSettled() {
+ return yawLag == 0.0f && pitchLag == 0.0f && yawSmooth == 0.0f && pitchSmooth == 0.0f;
+ }
+
+ /**
+ * Приводит разницу углов к диапазону [-180, 180): переход через
+ * границу мира не даёт ложного рывка на 360 градусов.
+ */
+ public static float wrapDegrees(float degrees) {
+ float d = sanitize(degrees) % 360.0f;
+ if (d >= 180.0f) {
+ d -= 360.0f;
+ }
+ if (d < -180.0f) {
+ d += 360.0f;
+ }
+ return d;
+ }
+
+ static float sanitize(float v) {
+ return (Float.isNaN(v) || Float.isInfinite(v)) ? 0.0f : v;
+ }
+
+ static float clamp(float v, float min, float max) {
+ if (v < min) return min;
+ if (v > max) return max;
+ return v;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java
new file mode 100644
index 0000000..a5f7db8
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java
@@ -0,0 +1,61 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.protection;
+
+/**
+ * Minimal pure throttle for repeated warnings.
+ *
+ *
The caller supplies the current time, so the class has no clock
+ * dependency and stays unit-testable.
+ */
+public final class NotifyThrottle {
+
+ private final long intervalMs;
+ private boolean fired;
+ private long lastMs;
+
+ public NotifyThrottle(long intervalMs) {
+ this.intervalMs = Math.max(0L, intervalMs);
+ }
+
+ /**
+ * Returns true when the caller may emit a notification now, recording
+ * the given time as the last emission. Returns false while the previous
+ * emission is still inside the interval.
+ */
+ public boolean tryAcquire(long nowMs) {
+ if (remainingMs(nowMs) > 0L) {
+ return false;
+ }
+ fired = true;
+ lastMs = nowMs;
+ return true;
+ }
+
+ /**
+ * Milliseconds left until the next notification is allowed.
+ * Zero when the throttle has never fired or the window elapsed.
+ */
+ public long remainingMs(long nowMs) {
+ if (!fired) {
+ return 0L;
+ }
+ long elapsed = nowMs - lastMs;
+ if (elapsed < 0L) {
+ // Clock moved backwards: do not lock out the caller.
+ return 0L;
+ }
+ long left = intervalMs - elapsed;
+ return Math.max(0L, left);
+ }
+
+ public void reset() {
+ fired = false;
+ lastMs = 0L;
+ }
+}
diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java
new file mode 100644
index 0000000..935c5fa
--- /dev/null
+++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java
@@ -0,0 +1,71 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.protection;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Pure decision logic for the LockSlot module: whether dropping an item
+ * from a given hotbar slot must be blocked.
+ */
+public final class SlotLockPolicy {
+
+ public static final int HOTBAR_SIZE = 9;
+
+ private SlotLockPolicy() {
+ }
+
+ /**
+ * Default setting state: all hotbar slots unlocked.
+ * Keys are ordered "slot1".."slot9".
+ */
+ public static Map defaultSlotStates() {
+ Map out = new LinkedHashMap<>();
+ for (int i = 0; i < HOTBAR_SIZE; i++) {
+ out.put(slotKey(i), false);
+ }
+ return out;
+ }
+
+ /**
+ * Setting key for a zero-based hotbar slot: 0 -> "slot1", 8 -> "slot9".
+ */
+ public static String slotKey(int hotbarSlot) {
+ return "slot" + (hotbarSlot + 1);
+ }
+
+ /**
+ * True when the slot is explicitly locked by the given states map.
+ * Missing or out-of-range slots are treated as unlocked.
+ */
+ public static boolean isSlotLocked(Map slotStates, int hotbarSlot) {
+ if (slotStates == null || slotStates.isEmpty()) return false;
+ if (hotbarSlot < 0 || hotbarSlot >= HOTBAR_SIZE) return false;
+ Boolean locked = slotStates.get(slotKey(hotbarSlot));
+ return locked != null && locked;
+ }
+
+ /**
+ * Full drop decision for a hotbar slot.
+ *
+ * @param slotStates locked-slot setting states keyed by {@link #slotKey(int)}
+ * @param selectedSlot zero-based hotbar slot the drop comes from
+ * @param playersNearby whether other players are within the guard radius
+ * @param onlyNearby the "only_players_nearby" setting value
+ */
+ public static boolean isDropBlocked(
+ Map slotStates,
+ int selectedSlot,
+ boolean playersNearby,
+ boolean onlyNearby
+ ) {
+ if (!isSlotLocked(slotStates, selectedSlot)) return false;
+ return !onlyNearby || playersNearby;
+ }
+}
diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json
index c64ab3d..8586cb7 100644
--- a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json
+++ b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json
@@ -522,7 +522,6 @@
"setting.worldtweaks.change_time": "Change time",
"setting.worldtweaks.time_hours": "Time (hours)",
"setting.worldtweaks.sync_system_time": "Sync with system time",
- "setting.worldtweaks.item_physics": "Item physics",
"setting.fovcontrol.use_custom_fov": "Use custom FOV",
"setting.fovcontrol.custom_fov": "Custom FOV",
"setting.fovcontrol.expand_scale": "Expand scale",
@@ -1297,8 +1296,6 @@
"setting.blockhighlight.breaking_animation.option.static": "Static",
"setting.blockhighlight.breaking_color": "Breaking color",
"setting.blockhighlight.breaking_color2": "Second breaking color",
- "setting.invisesp.color": "Color",
- "setting.invisesp.line_width": "Line width",
"setting.holeesp.range_xz": "Range XZ",
"setting.holeesp.range_y": "Range Y",
"setting.holeesp.height": "Height",
@@ -1913,7 +1910,19 @@
"setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime",
"setting.draggable.triangulator.triangulator_auto_reset": "Auto reset",
"module.autobed.description": "Automatically places and detonates beds in explosive dimensions.",
- "module.invisesp.description": "Outlines players hidden by invisibility.",
+ "module.invisesp.description": "Makes invisible players visible with adjustable translucency.",
+ "setting.invisesp.opacity": "Opacity",
+ "module.itemphysic.description": "Gives dropped items physical behavior: they lie flat on the ground and spin in the air.",
+ "setting.itemphysic.reduce_size": "Reduce item size",
+ "setting.itemphysic.lay_flat": "Lie flat on ground",
+ "setting.itemphysic.spin_in_air": "Spin in air",
+ "setting.itemphysic.no_bob": "No bobbing",
+ "module.atmodawnfog.description": "Cinematic atmosphere: dawn/dusk fog that follows the time of day and camera altitude.",
+ "setting.atmodawnfog.mode": "Mode",
+ "setting.atmodawnfog.density": "Density",
+ "setting.atmodawnfog.scatter_height": "Scatter height",
+ "setting.atmodawnfog.softness": "Softness",
+ "setting.atmodawnfog.dawn_color": "Dawn color",
"module.holeesp.description": "Highlights safe holes with a distance-faded fill.",
"setting.autobed.place": "Place",
"setting.autobed.explode": "Explode",
@@ -1963,6 +1972,8 @@
"setting.autobed.text_color": "Text color",
"setting.autobed.slide_delay": "Slide delay",
"setting.autobed.fade_time": "Fade time",
+ "module.shulkerpreview.description": "Preview shulker box contents by hovering it in an inventory.",
+ "setting.shulkerpreview.scale": "Scale",
"module.rubberhand.description": "Allows attacking and module block-use actions while an item is actively being used.",
"setting.rubberhand.actions": "Actions",
"setting.rubberhand.actions.option.attack_while_shield": "Hit while shielding",
@@ -3213,5 +3224,44 @@
"setting.chinahat.brim_thickness": "Brim thickness",
"setting.chinahat.head_top_offset": "Head height",
"setting.chinahat.head_forward_offset": "Forward offset",
- "setting.chinahat.head_right_offset": "Right offset"
+ "setting.chinahat.head_right_offset": "Right offset",
+ "module.animations.description": "Animates interface transitions.",
+ "setting.animations.hotbar_raise": "Hotbar raise",
+ "setting.animations.inventory": "Inventory open",
+ "module.nofrienddamage.description": "Blocks attacks on players added as friends.",
+ "module.lockslot.description": "Blocks dropping items from locked hotbar slots (anti-accidental Q).",
+ "setting.lockslot.slots": "Locked slots",
+ "setting.lockslot.only_players_nearby": "Only with players nearby",
+ "setting.lockslot.slots.option.slot1": "Slot 1",
+ "setting.lockslot.slots.option.slot2": "Slot 2",
+ "setting.lockslot.slots.option.slot3": "Slot 3",
+ "setting.lockslot.slots.option.slot4": "Slot 4",
+ "setting.lockslot.slots.option.slot5": "Slot 5",
+ "setting.lockslot.slots.option.slot6": "Slot 6",
+ "setting.lockslot.slots.option.slot7": "Slot 7",
+ "setting.lockslot.slots.option.slot8": "Slot 8",
+ "setting.lockslot.slots.option.slot9": "Slot 9",
+ "module.noserverpack.description": "Blocks forced server resource packs: auto-declines pushes and can remove already applied packs.",
+ "setting.noserverpack.action": "Action",
+ "setting.noserverpack.action.option.Decline": "Decline",
+ "setting.noserverpack.action.option.Remove": "Remove",
+ "setting.noserverpack.action.option.Ask": "Ask",
+ "module.tapemouse.description": "Repeats the selected mouse button on an interval, e.g. for AFK farms.",
+ "setting.tapemouse.button": "Button",
+ "setting.tapemouse.button.option.Attack": "Attack",
+ "setting.tapemouse.button.option.Use": "Use",
+ "setting.tapemouse.interval": "Interval",
+ "setting.tapemouse.only_hold": "Only while held",
+ "module.autorespawn.description": "Automatically respawns after death once the configured delay passes.",
+ "setting.autorespawn.delay": "Delay",
+ "module.deathcoords.description": "Sends the coordinates of your latest death to the chat.",
+ "setting.deathcoords.show_dimension": "Show dimension",
+ "setting.holdmyitems.sway_intensity": "Sway intensity",
+ "setting.holdmyitems.ease_strength": "Equip ease strength",
+ "setting.holdmyitems.offhand": "Apply to offhand",
+ "setting.holdmyitems.walk_bob": "Walk bobbing",
+ "setting.holdmyitems.bob_strength": "Bobbing strength",
+ "setting.holdmyitems.use_drift": "Using-item drift",
+ "setting.holdmyitems.drift_strength": "Drift strength",
+ "setting.holdmyitems.join_appear": "Smooth world entry"
}
diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json
index 485bc83..3f95ed6 100644
--- a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json
+++ b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json
@@ -457,7 +457,6 @@
"setting.worldtweaks.change_time": "Изменять время",
"setting.worldtweaks.time_hours": "Время (часы)",
"setting.worldtweaks.sync_system_time": "Синхронизировать с временем системы",
- "setting.worldtweaks.item_physics": "Физика предметов",
"setting.fovcontrol.use_custom_fov": "Свой FOV",
"setting.fovcontrol.custom_fov": "Пользовательский FOV",
"setting.fovcontrol.expand_scale": "Масштаб расширения",
@@ -1092,8 +1091,6 @@
"setting.blockhighlight.breaking_color": "Цвет ломания",
"setting.blockhighlight.breaking_color2": "Второй цвет ломания",
"setting.aspectratio.aspect_multiplier": "Множитель соотношения сторон",
- "setting.invisesp.color": "Цвет",
- "setting.invisesp.line_width": "Толщина линии",
"setting.holeesp.range_xz": "Радиус XZ",
"setting.holeesp.range_y": "Радиус Y",
"setting.holeesp.height": "Высота",
@@ -1708,7 +1705,19 @@
"setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша",
"setting.draggable.triangulator.triangulator_auto_reset": "Автосброс",
"module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.",
- "module.invisesp.description": "Обводит игроков, скрытых невидимостью.",
+ "module.invisesp.description": "Делает невидимых игроков видимыми (полупрозрачно).",
+ "setting.invisesp.opacity": "Прозрачность",
+ "module.itemphysic.description": "Даёт предметам на земле физику: лежат плашмя, крутятся в воздухе.",
+ "setting.itemphysic.reduce_size": "Уменьшить размер",
+ "setting.itemphysic.lay_flat": "Лежит плашмя на земле",
+ "setting.itemphysic.spin_in_air": "Крутится в воздухе",
+ "setting.itemphysic.no_bob": "Без покачивания",
+ "module.atmodawnfog.description": "Кинематографичная атмосфера: туман рассвета/заката, зависящий от времени суток и высоты.",
+ "setting.atmodawnfog.mode": "Режим",
+ "setting.atmodawnfog.density": "Плотность",
+ "setting.atmodawnfog.scatter_height": "Высота рассеивания",
+ "setting.atmodawnfog.softness": "Мягкость",
+ "setting.atmodawnfog.dawn_color": "Цвет зари",
"module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.",
"setting.autobed.place": "Ставить",
"setting.autobed.explode": "Взрывать",
@@ -1758,6 +1767,8 @@
"setting.autobed.text_color": "Цвет текста",
"setting.autobed.slide_delay": "Задержка слайда",
"setting.autobed.fade_time": "Время затухания",
+ "module.shulkerpreview.description": "Показывает содержимое шалкера при наведении в инвентаре.",
+ "setting.shulkerpreview.scale": "Масштаб",
"module.rubberhand.description": "Позволяет атаковать и выполнять блочные действия модулей во время активного использования предмета.",
"setting.rubberhand.actions": "Действия",
"setting.rubberhand.actions.option.attack_while_shield": "Бить в щите",
@@ -3003,5 +3014,44 @@
"setting.chinahat.brim_thickness": "Толщина полей",
"setting.chinahat.head_top_offset": "Высота на голове",
"setting.chinahat.head_forward_offset": "Смещение вперёд",
- "setting.chinahat.head_right_offset": "Смещение вправо"
+ "setting.chinahat.head_right_offset": "Смещение вправо",
+ "module.animations.description": "Плавные анимации интерфейса.",
+ "setting.animations.hotbar_raise": "Подъём хотбара",
+ "setting.animations.inventory": "Открытие инвентаря",
+ "module.nofrienddamage.description": "Блокирует удары по игрокам, добавленным в друзья.",
+ "module.lockslot.description": "Запрещает выбрасывать предметы из заблокированных слотов хотбара (защита от случайного Q).",
+ "setting.lockslot.slots": "Заблокированные слоты",
+ "setting.lockslot.only_players_nearby": "Только при игроках рядом",
+ "setting.lockslot.slots.option.slot1": "Слот 1",
+ "setting.lockslot.slots.option.slot2": "Слот 2",
+ "setting.lockslot.slots.option.slot3": "Слот 3",
+ "setting.lockslot.slots.option.slot4": "Слот 4",
+ "setting.lockslot.slots.option.slot5": "Слот 5",
+ "setting.lockslot.slots.option.slot6": "Слот 6",
+ "setting.lockslot.slots.option.slot7": "Слот 7",
+ "setting.lockslot.slots.option.slot8": "Слот 8",
+ "setting.lockslot.slots.option.slot9": "Слот 9",
+ "module.noserverpack.description": "Блокирует принудительную загрузку серверного ресурспака: автоматически отклоняет запросы и может убрать уже применённые паки.",
+ "setting.noserverpack.action": "Действие",
+ "setting.noserverpack.action.option.Decline": "Отклонять",
+ "setting.noserverpack.action.option.Remove": "Убирать",
+ "setting.noserverpack.action.option.Ask": "Спрашивать",
+ "module.tapemouse.description": "Автоматически нажимает выбранную кнопку мыши с заданным интервалом, например для AFK-ферм.",
+ "setting.tapemouse.button": "Кнопка",
+ "setting.tapemouse.button.option.Attack": "Атака",
+ "setting.tapemouse.button.option.Use": "Использование",
+ "setting.tapemouse.interval": "Интервал",
+ "setting.tapemouse.only_hold": "Только при зажатии",
+ "module.autorespawn.description": "Автоматически возрождает после смерти по истечении заданной задержки.",
+ "setting.autorespawn.delay": "Задержка",
+ "module.deathcoords.description": "Отправляет в чат координаты вашей последней смерти.",
+ "setting.deathcoords.show_dimension": "Показывать измерение",
+ "setting.holdmyitems.sway_intensity": "Интенсивность покачивания",
+ "setting.holdmyitems.ease_strength": "Плавность equip",
+ "setting.holdmyitems.offhand": "Применять к оффхенду",
+ "setting.holdmyitems.walk_bob": "Покачивание при ходьбе",
+ "setting.holdmyitems.bob_strength": "Сила покачивания",
+ "setting.holdmyitems.use_drift": "Дрейф при использовании",
+ "setting.holdmyitems.drift_strength": "Сила дрейфа",
+ "setting.holdmyitems.join_appear": "Плавное появление при входе"
}
diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag b/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag
new file mode 100644
index 0000000..b0b85f8
--- /dev/null
+++ b/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag
@@ -0,0 +1,156 @@
+#version 330 core
+
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+out vec4 color;
+
+uniform sampler2D u_Src;
+uniform sampler2D u_Mask;
+
+layout (std140) uniform HandNoise {
+ vec4 u_Color; // rgb, alpha
+ vec4 u_Glow; // rgb, alpha
+ vec4 u_Params0; // edgeWidth, quality, octaves, time
+ vec4 u_Params1; // resX, resY, scale, speed
+ vec4 u_Params2; // contrast, breathe, glowStrength, density
+};
+
+in vec2 v_TexCoord;
+
+float handNoiseHash21(vec2 p) {
+ p = fract(p * vec2(127.31, 311.7));
+ p += dot(p, p + 41.31);
+ return fract(p.x * p.y);
+}
+
+float handNoiseValue(vec2 p) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+ float a = handNoiseHash21(i);
+ float b = handNoiseHash21(i + vec2(1.0, 0.0));
+ float c = handNoiseHash21(i + vec2(0.0, 1.0));
+ float d = handNoiseHash21(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+}
+
+float handNoiseFbm(vec2 p, int octaves) {
+ float v = 0.0;
+ float a = 0.55;
+ mat2 r = mat2(0.80, 0.60, -0.60, 0.80);
+ int o = clamp(octaves, 1, 3);
+ for (int i = 0; i < 3; ++i) {
+ if (i >= o) break;
+ v += a * handNoiseValue(p);
+ p = r * p * 2.07 + vec2(23.3, 41.7);
+ a *= 0.5;
+ }
+ return v;
+}
+
+float handMaskAt(vec2 uv) {
+ vec4 mask = texture(u_Mask, uv);
+ return max(mask.a, max(mask.r, max(mask.g, mask.b)));
+}
+
+float handNoiseEdgeDistance(vec2 oneTexel, int quality, float edgeWidth, out float hitAmount) {
+ float best = 1.0e5;
+ float hit = 0.0;
+ int rings = clamp(quality, 1, 4);
+ int dirs = quality <= 1 ? 8 : 12;
+ float radiusPx = max(edgeWidth, 1.0);
+
+ for (int r = 1; r <= 4; ++r) {
+ if (r > rings) continue;
+ float rr = float(r) / float(rings);
+ float distPx = radiusPx * rr;
+ for (int i = 0; i < 12; ++i) {
+ if (i >= dirs) continue;
+ float angle = (float(i) + float(r) * 0.37) * 6.28318530718 / float(dirs);
+ vec2 dir = vec2(cos(angle), sin(angle));
+ float a = handMaskAt(v_TexCoord + dir * oneTexel * distPx);
+ if (a > 0.02) {
+ best = min(best, rr);
+ hit = max(hit, a);
+ }
+ }
+ }
+
+ hitAmount = hit;
+ return best;
+}
+
+float handNoiseField(vec2 fragCoord, vec2 resolution, float time, int octaves) {
+ float scale = max(u_Params1.z, 0.05);
+ float speed = max(u_Params1.w, 0.0);
+ float contrast = max(u_Params2.x, 0.05);
+ float breathe = clamp(u_Params2.y, 0.0, 1.0);
+ float density = max(u_Params2.w, 0.0);
+
+ vec2 uv = fragCoord / max(resolution, vec2(1.0));
+ vec2 aspect = vec2(resolution.x / max(resolution.y, 1.0), 1.0);
+ vec2 p = (uv - 0.5) * aspect * scale;
+
+ float t = time * speed;
+ vec2 warp = vec2(
+ handNoiseFbm(p * 1.3 + vec2(0.0, t * 0.32), octaves),
+ handNoiseFbm(p * 1.3 + vec2(5.2, -t * 0.26), octaves)
+ ) - 0.5;
+ float n = handNoiseFbm(p * 1.9 + warp * 1.35 + vec2(t * 0.21, -t * 0.14), octaves);
+
+ float pulse = 0.5 + 0.5 * sin(t * 1.6 + warp.x * 2.4);
+ n = clamp(n * (1.0 + breathe * (pulse - 0.5) * 0.9), 0.0, 1.0);
+ return pow(clamp(n * (0.85 + density * 0.3), 0.0, 1.0), 1.0 / contrast);
+}
+
+void main() {
+ vec4 base = texture(u_Src, v_TexCoord);
+ float mask = handMaskAt(v_TexCoord);
+
+ vec2 resolution = max(u_Params1.xy, vec2(1.0));
+ vec2 oneTexel = 1.0 / resolution;
+ float edgeWidth = max(u_Params0.x, 0.0);
+ int quality = int(u_Params0.y + 0.5);
+ int octaves = clamp(int(u_Params0.z + 0.5), 1, 3);
+ float time = u_Params0.w;
+ float glowStrength = max(u_Params2.z, 0.0);
+
+ if (mask > 0.01) {
+ float n = handNoiseField(gl_FragCoord.xy, resolution, time, octaves);
+
+ float nL = handMaskAt(v_TexCoord + vec2(-oneTexel.x, 0.0));
+ float nR = handMaskAt(v_TexCoord + vec2(oneTexel.x, 0.0));
+ float nU = handMaskAt(v_TexCoord + vec2(0.0, -oneTexel.y));
+ float nD = handMaskAt(v_TexCoord + vec2(0.0, oneTexel.y));
+ float innerEdge = clamp(1.0 - min(min(nL, nR), min(nU, nD)), 0.0, 1.0);
+ innerEdge = pow(innerEdge, 0.72);
+
+ float alpha = clamp(u_Color.a * mask, 0.0, 1.0);
+ vec3 shade = u_Color.rgb * (0.55 + 0.75 * n);
+ shade = mix(shade, u_Glow.rgb, clamp((n - 0.68) * 1.4, 0.0, 0.35));
+ vec3 outCol = mix(base.rgb, shade, alpha);
+ outCol += u_Glow.rgb * innerEdge * u_Glow.a * glowStrength * (0.18 + 0.30 * n);
+ color = vec4(clamp(outCol, 0.0, 1.0), 1.0);
+ return;
+ }
+
+ float hitAmount = 0.0;
+ float d = handNoiseEdgeDistance(oneTexel, quality, edgeWidth, hitAmount);
+ if (hitAmount > 0.01) {
+ float n = handNoiseField(gl_FragCoord.xy, resolution, time, octaves);
+ float falloff = clamp(1.0 - d, 0.0, 1.0);
+ falloff = falloff * falloff * (3.0 - 2.0 * falloff);
+ float glowA = pow(falloff, 1.3) * u_Glow.a * glowStrength * (0.30 + 0.40 * n);
+ vec3 outCol = mix(base.rgb, u_Glow.rgb, glowA * 0.5);
+ outCol += u_Glow.rgb * glowA;
+ color = vec4(clamp(outCol, 0.0, 1.0), 1.0);
+ return;
+ }
+
+ color = vec4(base.rgb, 1.0);
+}
diff --git a/combatant-client-26.2/src/main/resources/lovisual.mixins.json b/combatant-client-26.2/src/main/resources/lovisual.mixins.json
index 3d22f0f..fc3af4b 100644
--- a/combatant-client-26.2/src/main/resources/lovisual.mixins.json
+++ b/combatant-client-26.2/src/main/resources/lovisual.mixins.json
@@ -90,11 +90,13 @@
"LightmapRenderStateExtractorMixin",
"LivingEntityRendererMixin",
"LivingEntityRenderStateMixin",
+ "LocalPlayerDropMixin",
"LocalPlayerMixin",
"LocatorBarMixin",
"MinecraftMixin",
"MouseMixin",
"MultiPlayerGameModeMixin",
+ "MultiPlayerGameModeThrowMixin",
"NarratorManagerMixin",
"OptionsNarratorMixin",
"ParticleManagerMixin",
@@ -139,6 +141,7 @@
"accessors.InputAccessor",
"accessors.ItemCooldownEntryAccessor",
"accessors.ItemCooldownManagerAccessor",
+ "accessors.KeyMappingAccessor",
"accessors.LevelExtractorAccessor",
"accessors.LevelRendererAccessor",
"accessors.LivingEntityRenderStateAccessor",
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java
new file mode 100644
index 0000000..77ab7f4
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.misc;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class NoServerPackTest {
+
+ @Test
+ void declinesInDeclineModeWhenEnabled() {
+ assertTrue(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.DECLINE.id));
+ }
+
+ @Test
+ void declinesInRemoveModeWhenEnabled() {
+ assertTrue(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.REMOVE.id));
+ }
+
+ @Test
+ void neverDeclinesInAskMode() {
+ assertFalse(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.ASK.id));
+ assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.ASK.id));
+ }
+
+ @Test
+ void disabledModuleNeverDeclines() {
+ assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.DECLINE.id));
+ assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.REMOVE.id));
+ }
+
+ @Test
+ void unknownModeFallsBackToDecline() {
+ assertEquals(NoServerPack.Action.DECLINE, NoServerPack.Action.byId("bogus"));
+ assertEquals(NoServerPack.Action.DECLINE, NoServerPack.Action.byId(null));
+ }
+
+ @Test
+ void modeIdsResolveRoundTrip() {
+ for (NoServerPack.Action action : NoServerPack.Action.values()) {
+ assertEquals(action, NoServerPack.Action.byId(action.id));
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java
new file mode 100644
index 0000000..1afc3a0
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class AutoRespawnTest {
+
+ @Test
+ void deathAloneDoesNotTriggerImmediately() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, 500L));
+ }
+
+ @Test
+ void triggersOnceAfterDelay() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, 500L));
+ assertFalse(gate.shouldRequestRespawn(true, 1200L, 500L));
+ assertTrue(gate.shouldRequestRespawn(true, 1500L, 500L));
+ }
+
+ @Test
+ void requestIsSentOnlyOncePerDeath() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L));
+ gate.onRequestSent();
+
+ // Player is still locally dead until the server responds.
+ assertFalse(gate.shouldRequestRespawn(true, 1100L, 0L));
+ assertFalse(gate.shouldRequestRespawn(true, 5000L, 0L));
+ }
+
+ @Test
+ void comingBackToLifeRearmsTheGate() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L));
+ gate.onRequestSent();
+ assertFalse(gate.shouldRequestRespawn(true, 2000L, 0L));
+
+ assertFalse(gate.shouldRequestRespawn(false, 3000L, 0L));
+ assertFalse(gate.shouldRequestRespawn(true, 3100L, 500L));
+ assertTrue(gate.shouldRequestRespawn(true, 3600L, 500L));
+ }
+
+ @Test
+ void totemReviveWithoutRequestStartsFreshDeath() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, 2000L));
+ assertFalse(gate.shouldRequestRespawn(false, 1200L, 2000L));
+
+ assertFalse(gate.shouldRequestRespawn(true, 5000L, 2000L));
+ assertTrue(gate.shouldRequestRespawn(true, 7000L, 2000L));
+ }
+
+ @Test
+ void negativeDelayTreatedAsZero() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, -5L));
+ assertTrue(gate.shouldRequestRespawn(true, 1001L, -5L));
+ }
+
+ @Test
+ void missingPlayerResetsState() {
+ AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate();
+ assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L));
+ gate.onRequestSent();
+
+ gate.reset();
+ assertFalse(gate.shouldRequestRespawn(true, 2000L, 0L));
+ assertTrue(gate.shouldRequestRespawn(true, 2001L, 0L));
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java
new file mode 100644
index 0000000..af321a7
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java
@@ -0,0 +1,66 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class DeathCoordsTest {
+
+ @Test
+ void detectsAliveToDeadTransition() {
+ DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector();
+ assertFalse(detector.onTick(false));
+ assertTrue(detector.onTick(true));
+ assertFalse(detector.onTick(true));
+ assertFalse(detector.onTick(true));
+ }
+
+ @Test
+ void secondDeathIsDetectedAgainAfterRevive() {
+ DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector();
+ detector.onTick(true);
+ assertFalse(detector.onTick(false));
+ assertTrue(detector.onTick(true));
+ }
+
+ @Test
+ void resetClearsDeathState() {
+ DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector();
+ detector.onTick(true);
+ detector.reset();
+ assertTrue(detector.onTick(true));
+ }
+
+ @Test
+ void messageIncludesDimensionWhenEnabled() {
+ String message = DeathCoords.formatDeathMessage(12.25, 64.0, -789.5, "minecraft:overworld", true);
+ assertEquals("You died at 12.25, 64.00, -789.50 (minecraft:overworld)", message);
+ }
+
+ @Test
+ void messageOmitsDimensionWhenDisabled() {
+ String message = DeathCoords.formatDeathMessage(0.5, 1.5, 2.5, "minecraft:the_nether", false);
+ assertEquals("You died at 0.50, 1.50, 2.50", message);
+ }
+
+ @Test
+ void messageFallsBackWithoutDimensionName() {
+ assertEquals(
+ "You died at 1.00, 2.00, 3.00",
+ DeathCoords.formatDeathMessage(1, 2, 3, "", true)
+ );
+ assertEquals(
+ "You died at 1.00, 2.00, 3.00",
+ DeathCoords.formatDeathMessage(1, 2, 3, null, true)
+ );
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java
new file mode 100644
index 0000000..72da510
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java
@@ -0,0 +1,65 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.player;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TapeMouseTest {
+
+ @Test
+ void firstCallIsDueImmediately() {
+ TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
+ assertTrue(pacer.isDue(1000L, 1000L));
+ }
+
+ @Test
+ void clicksAreSpacedByInterval() {
+ TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
+ assertTrue(pacer.isDue(1000L, 500L));
+ pacer.mark(1000L);
+
+ assertFalse(pacer.isDue(1499L, 500L));
+ assertTrue(pacer.isDue(1500L, 500L));
+ assertTrue(pacer.isDue(4000L, 500L));
+ }
+
+ @Test
+ void changingIntervalIsRespected() {
+ TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
+ pacer.mark(1000L);
+
+ assertFalse(pacer.isDue(2499L, 2000L));
+ assertTrue(pacer.isDue(3000L, 2000L));
+ pacer.mark(3000L);
+
+ assertTrue(pacer.isDue(3100L, 100L));
+ }
+
+ @Test
+ void resetAllowsImmediateClickAgain() {
+ TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
+ pacer.mark(1000L);
+ assertFalse(pacer.isDue(1100L, 5000L));
+
+ pacer.reset();
+ assertTrue(pacer.isDue(1100L, 5000L));
+ }
+
+ @Test
+ void nonPositiveIntervalStillPacesEveryCall() {
+ TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer();
+ assertTrue(pacer.isDue(0L, 0L));
+ pacer.mark(0L);
+ assertFalse(pacer.isDue(0L, 0L));
+ assertTrue(pacer.isDue(1L, 0L));
+ assertTrue(pacer.isDue(1L, -100L));
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java
new file mode 100644
index 0000000..4d0cf7e
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java
@@ -0,0 +1,59 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.visuals;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Чистая математика AtmoDawnFog: окно времени суток и высотное затухание.
+ */
+final class AtmoDawnFogTest {
+
+ private static final float EPS = 1e-4f;
+
+ @Test
+ void dawnPeaksAtSunrise() {
+ assertEquals(1.0f, AtmoDawnFog.strengthFor(0.0f, "Dawn", 64.0f, 76.0f), EPS);
+ assertTrue(AtmoDawnFog.strengthFor(0.999f, "Dawn", 64.0f, 76.0f) > 0.98f);
+ }
+
+ @Test
+ void dawnSilentAtNoonAndDusk() {
+ assertEquals(0.0f, AtmoDawnFog.strengthFor(0.25f, "Dawn", 64.0f, 76.0f), EPS);
+ assertEquals(0.0f, AtmoDawnFog.strengthFor(0.5f, "Dawn", 64.0f, 76.0f), EPS);
+ }
+
+ @Test
+ void duskPeaksAtSunset() {
+ assertEquals(1.0f, AtmoDawnFog.strengthFor(0.5f, "Dusk", 64.0f, 76.0f), EPS);
+ assertEquals(0.0f, AtmoDawnFog.strengthFor(0.0f, "Dusk", 64.0f, 76.0f), EPS);
+ }
+
+ @Test
+ void customModeIgnoresTime() {
+ assertEquals(1.0f, AtmoDawnFog.strengthFor(0.3f, "Custom", 64.0f, 76.0f), EPS);
+ assertEquals(1.0f, AtmoDawnFog.strengthFor(0.7f, "Custom", 64.0f, 76.0f), EPS);
+ }
+
+ @Test
+ void altitudeFadesAboveScatterHeight() {
+ float below = AtmoDawnFog.strengthFor(0.0f, "Dawn", 76.0f, 76.0f);
+ float above = AtmoDawnFog.strengthFor(0.0f, "Dawn", 76.0f + 16.0f, 76.0f);
+ assertEquals(1.0f, below, EPS);
+ assertEquals(0.5f, above, EPS);
+ }
+
+ @Test
+ void altitudeClampsAtZero() {
+ assertEquals(0.0f, AtmoDawnFog.strengthFor(0.0f, "Custom", 76.0f + 1000.0f, 76.0f), EPS);
+ assertTrue(AtmoDawnFog.strengthFor(0.0f, "Custom", 40.0f, 76.0f) <= 1.0f);
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java
new file mode 100644
index 0000000..d216742
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java
@@ -0,0 +1,78 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.features.module.modules.visuals;
+
+import dev.loki.lovisual.render.engine.uniform.impl.PostFXUniforms;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Пресеты PostFX: Cinematic должен быть визуально заметным,
+ * Vanilla — нейтральным (это причина «модуль ничего не делал»).
+ */
+final class PostFxPresetTest {
+
+ private static float[] preset(String name) {
+ float[] out = new float[PostFXUniforms.FLOAT_COUNT];
+ PostFX.applyPresetTo(name, out);
+ return out;
+ }
+
+ @Test
+ void vanillaPresetIsNeutral() {
+ float[] vanilla = preset("Vanilla");
+ assertEquals(0.0f, vanilla[0], 1e-6); // exposure
+ assertEquals(1.0f, vanilla[1], 1e-6); // gamma
+ assertEquals(1.0f, vanilla[2], 1e-6); // contrast
+ assertEquals(1.0f, vanilla[3], 1e-6); // saturation
+ assertEquals(0.0f, vanilla[7], 1e-6); // LUT strength
+ assertEquals(0.0f, vanilla[15], 1e-6); // tone map off
+ }
+
+ @Test
+ void cinematicPresetIsVisible() {
+ float[] cinematic = preset("Cinematic");
+ assertNotEquals(1.0f, cinematic[2], 1e-4); // contrast изменён
+ assertTrue(cinematic[3] > 1.05f); // насыщенность выше
+ assertTrue(cinematic[7] > 0.2f); // тёплый LUT заметен
+ assertEquals(1.0f, cinematic[15], 1e-6); // filmic tone map
+ assertTrue(cinematic[19] > 0.0f); // холодные тени
+ assertTrue(cinematic[27] > 0.0f); // тёплые света
+ assertTrue(cinematic[8] > 0.1f); // виньетка заметна
+ }
+
+ @Test
+ void cinematicDiffersFromVanillaOnGrade() {
+ float[] cinematic = preset("Cinematic");
+ float[] vanilla = preset("Vanilla");
+ assertNotEquals(vanilla[2], cinematic[2]);
+ assertNotEquals(vanilla[3], cinematic[3]);
+ assertNotEquals(vanilla[7], cinematic[7]);
+ }
+
+ @Test
+ void unknownPresetFallsBackToSoft() {
+ float[] unknown = preset("Mystery");
+ float[] soft = preset("Soft");
+ for (int i = 0; i < 16; i++) {
+ assertEquals(soft[i], unknown[i], 1e-6);
+ }
+ }
+
+ @Test
+ void nullPresetFallsBackToCinematic() {
+ float[] byNull = preset(null);
+ float[] cinematic = preset("Cinematic");
+ for (int i = 0; i < 16; i++) {
+ assertEquals(cinematic[i], byNull[i], 1e-6);
+ }
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java
new file mode 100644
index 0000000..4d0c80d
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java
@@ -0,0 +1,76 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Математика ease-out-back для HoldMyItems.
+ */
+final class EaseCurvesTest {
+
+ private static final float EPS = 1e-4f;
+ private static final float DEFAULT_C1 = 1.70158f;
+
+ @Test
+ void easeOutBackStartsAtZeroAndEndsAtOne() {
+ assertEquals(0.0f, EaseCurves.easeOutBack(0.0f, DEFAULT_C1), EPS);
+ assertEquals(1.0f, EaseCurves.easeOutBack(1.0f, DEFAULT_C1), EPS);
+ }
+
+ @Test
+ void easeOutBackOvershootsPastOne() {
+ float max = 0.0f;
+ for (int i = 0; i <= 100; i++) {
+ max = Math.max(max, EaseCurves.easeOutBack(i / 100.0f, DEFAULT_C1));
+ }
+ assertTrue(max > 1.0f && max < 1.2f, "overshoot должен быть заметным, но умеренным: " + max);
+ }
+
+ @Test
+ void shapeKeepsEndpoints() {
+ assertEquals(0.0f, EaseCurves.shapeEquipProgress(0.0f, DEFAULT_C1, 1.0f), EPS);
+ assertEquals(1.0f, EaseCurves.shapeEquipProgress(1.0f, DEFAULT_C1, 1.0f), EPS);
+ }
+
+ @Test
+ void zeroIntensityFallsBackToLinear() {
+ for (int i = 0; i <= 10; i++) {
+ float x = i / 10.0f;
+ assertEquals(x, EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 0.0f), EPS);
+ }
+ }
+
+ @Test
+ void fullIntensityMatchesCurve() {
+ float x = 0.7f;
+ assertEquals(
+ EaseCurves.easeOutBack(x, DEFAULT_C1),
+ EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 1.0f),
+ EPS
+ );
+ }
+
+ @Test
+ void intensityClamps() {
+ float x = 0.5f;
+ assertEquals(
+ EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 0.0f),
+ EaseCurves.shapeEquipProgress(x, DEFAULT_C1, -5.0f),
+ EPS
+ );
+ assertEquals(
+ EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 1.0f),
+ EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 5.0f),
+ EPS
+ );
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java
new file mode 100644
index 0000000..73e5dc6
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java
@@ -0,0 +1,72 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Совместная работа HoldMyItems (кривая) и ViewModel (занижение):
+ * конечная позиция определяется занижением, кривая добавляет перелёт.
+ */
+final class EquipProgressComposerTest {
+
+ private static final float EPS = 1e-4f;
+ private static final float C1 = 1.6f;
+
+ @Test
+ void bothDisabledIsIdentity() {
+ assertEquals(0.42f, EquipProgressComposer.compose(0.42f, false, 0, 0, false, 0), EPS);
+ }
+
+ @Test
+ void loweringAloneScalesEndpoints() {
+ assertEquals(0.0f, EquipProgressComposer.compose(0.0f, false, 0, 0, true, 0.5f), EPS);
+ assertEquals(0.5f, EquipProgressComposer.compose(1.0f, false, 0, 0, true, 0.5f), EPS);
+ }
+
+ @Test
+ void curveAloneKeepsEndpoints() {
+ assertEquals(0.0f, EquipProgressComposer.compose(0.0f, true, C1, 1.0f, false, 0), EPS);
+ assertEquals(1.0f, EquipProgressComposer.compose(1.0f, true, C1, 1.0f, false, 0), EPS);
+ }
+
+ @Test
+ void togetherRestingPointIsLowered() {
+ //equip завершён (progress=1): финальная позиция = занижение, а не 1
+ float rest = EquipProgressComposer.compose(1.0f, true, C1, 1.0f, true, 0.5f);
+ assertEquals(0.5f, rest, EPS);
+ }
+
+ @Test
+ void togetherOvershootExceedsRestingPoint() {
+ float max = 0.0f;
+ for (int i = 0; i <= 100; i++) {
+ max = Math.max(max, EquipProgressComposer.compose(i / 100.0f, true, C1, 1.0f, true, 0.5f));
+ }
+ assertTrue(max > 0.5f, "перелёт кривой должен поднимать предмет выше финальной позиции: " + max);
+ assertTrue(max < 0.5f * 1.2f, "перелёт умеренный: " + max);
+ }
+
+ @Test
+ void curveDisabledButLoweringOn() {
+ assertEquals(0.25f, EquipProgressComposer.compose(0.5f, false, 0, 0, true, 0.5f), EPS);
+ }
+
+ @Test
+ void loweringDisabledButCurveOn() {
+ float x = 0.7f;
+ assertEquals(
+ EaseCurves.shapeEquipProgress(x, C1, 1.0f),
+ EquipProgressComposer.compose(x, true, C1, 1.0f, false, 0),
+ EPS
+ );
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java
new file mode 100644
index 0000000..27d089b
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java
@@ -0,0 +1,162 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Pure-logic tests for the single-update-point animator. No Minecraft types involved.
+ */
+final class SmoothValueTest {
+
+ private static final float EPS = 1.0e-4f;
+
+ @Test
+ void reachesTargetWhenMovingUp() {
+ SmoothValue value = new SmoothValue(1.5f);
+ value.setTarget(true);
+
+ for (int i = 0; i < 200; i++) {
+ value.update(0.5f);
+ }
+
+ assertEquals(1.0f, value.value(), EPS);
+ assertTrue(value.isSettled());
+ }
+
+ @Test
+ void reachesTargetWhenMovingDown() {
+ SmoothValue value = new SmoothValue(1.5f, 1.0f);
+ value.setInstant(true);
+ value.setTarget(false);
+
+ for (int i = 0; i < 200; i++) {
+ value.update(0.5f);
+ }
+
+ assertEquals(0.0f, value.value(), EPS);
+ assertTrue(value.isSettled());
+ }
+
+ @Test
+ void movesUpMonotonically() {
+ SmoothValue value = new SmoothValue(0.8f);
+ value.setTarget(true);
+
+ float previous = value.value();
+ for (int i = 0; i < 100; i++) {
+ value.update(1.0f / 3.0f);
+ assertTrue(value.value() >= previous, "value must never fall while rising");
+ assertTrue(value.value() <= 1.0f, "value must stay within 0..1");
+ previous = value.value();
+ }
+ assertEquals(1.0f, value.value(), EPS);
+ }
+
+ @Test
+ void movesDownMonotonically() {
+ SmoothValue value = new SmoothValue(0.8f);
+ value.setInstant(true);
+ value.setTarget(false);
+
+ float previous = value.value();
+ for (int i = 0; i < 100; i++) {
+ value.update(1.0f / 3.0f);
+ assertTrue(value.value() <= previous, "value must never rise while falling");
+ assertTrue(value.value() >= 0.0f, "value must stay within 0..1");
+ previous = value.value();
+ }
+ assertEquals(0.0f, value.value(), EPS);
+ }
+
+ @Test
+ void setInstantTeleportsValueAndTarget() {
+ SmoothValue value = new SmoothValue(1.0f);
+ assertEquals(0.0f, value.value(), EPS);
+
+ value.setInstant(true);
+ assertEquals(1.0f, value.value(), EPS);
+ assertEquals(1.0f, value.target(), EPS);
+ assertTrue(value.isSettled());
+
+ value.setInstant(false);
+ assertEquals(0.0f, value.value(), EPS);
+ assertEquals(0.0f, value.target(), EPS);
+ assertTrue(value.isSettled());
+ }
+
+ @Test
+ void setTargetDoesNotMoveValue() {
+ SmoothValue value = new SmoothValue(2.0f);
+ value.setTarget(true);
+ assertEquals(0.0f, value.value(), EPS, "setTarget must not advance the value");
+ assertEquals(1.0f, value.target(), EPS);
+ }
+
+ @Test
+ void frameRateIndependent() {
+ // One long step must land on the same point as many small steps of equal total time.
+ float totalTicks = 2.0f;
+ float speed = 1.1f;
+
+ SmoothValue coarse = new SmoothValue(speed);
+ coarse.setTarget(true);
+ coarse.update(totalTicks);
+
+ SmoothValue fine = new SmoothValue(speed);
+ fine.setTarget(true);
+ int steps = 240;
+ for (int i = 0; i < steps; i++) {
+ fine.update(totalTicks / steps);
+ }
+
+ assertEquals(coarse.value(), fine.value(), 5.0e-3f);
+ assertTrue(coarse.value() > 0.5f && coarse.value() < 1.0f);
+ }
+
+ @Test
+ void zeroAndBrokenDeltasAreIgnored() {
+ SmoothValue value = new SmoothValue(1.0f);
+ value.setTarget(true);
+
+ value.update(0.0f);
+ value.update(-1.0f);
+ value.update(Float.NaN);
+ assertEquals(0.0f, value.value(), EPS);
+ assertFalse(value.isSettled());
+
+ value.update(1.0f);
+ assertTrue(value.value() > 0.0f);
+ }
+
+ @Test
+ void targetFlipsMidFlightReverseDirection() {
+ SmoothValue value = new SmoothValue(1.0f);
+ value.setTarget(true);
+ value.update(1.0f);
+ assertTrue(value.value() > 0.0f);
+
+ value.setTarget(false);
+ float peak = value.value();
+ value.update(1.0f);
+ assertTrue(value.value() < peak, "falling must start immediately after the flip");
+ }
+
+ @Test
+ void rejectsBrokenSpeed() {
+ assertThrows(IllegalArgumentException.class, () -> new SmoothValue(0.0f));
+ assertThrows(IllegalArgumentException.class, () -> new SmoothValue(-1.0f));
+ assertThrows(IllegalArgumentException.class, () -> new SmoothValue(Float.NaN));
+ assertThrows(IllegalArgumentException.class, () -> new SmoothValue(Float.POSITIVE_INFINITY));
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java
new file mode 100644
index 0000000..8fbb253
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java
@@ -0,0 +1,97 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Фаза и амплитуда покачивания при ходьбе. */
+final class BobMathTest {
+
+ @Test
+ void amplitudeIsZeroWhenIdle() {
+ assertEquals(0.0f, BobMath.amplitude(0.0f), 1.0e-6f);
+ }
+
+ @Test
+ void amplitudeGrowsWithSpeed() {
+ float slow = BobMath.amplitude(0.25f);
+ float fast = BobMath.amplitude(0.9f);
+ assertTrue(slow > 0.0f && slow < fast, "амплитуда должна расти со скоростью");
+ assertTrue(slow < 1.0f && fast <= 1.0f);
+ }
+
+ @Test
+ void amplitudeClampedToOne() {
+ assertEquals(1.0f, BobMath.amplitude(2.5f), 1.0e-6f);
+ }
+
+ @Test
+ void brokenSpeedGivesZeroAmplitude() {
+ assertEquals(0.0f, BobMath.amplitude(Float.NaN), 1.0e-6f);
+ assertEquals(0.0f, BobMath.amplitude(Float.NEGATIVE_INFINITY), 1.0e-6f);
+ assertEquals(0.0f, BobMath.amplitude(-0.5f), 1.0e-6f);
+ }
+
+ @Test
+ void lateralStartsAtZeroAndIsBounded() {
+ assertEquals(0.0f, BobMath.lateral(0.0f), 1.0e-6f);
+ for (float phase = 0.0f; phase < 40.0f; phase += 0.125f) {
+ float value = BobMath.lateral(phase);
+ assertTrue(value >= -1.0f && value <= 1.0f, "латераль вне диапазона в фазе " + phase);
+ }
+ }
+
+ @Test
+ void lateralIsAntisymmetric() {
+ for (float phase = -3.0f; phase <= 3.0f; phase += 0.25f) {
+ assertEquals(-BobMath.lateral(phase), BobMath.lateral(-phase), 1.0e-5f);
+ }
+ }
+
+ @Test
+ void lateralRepeatsAfterFullCycle() {
+ float cycle = (float) (2.0 * Math.PI / BobMath.STEP_RATE);
+ assertEquals(BobMath.lateral(0.3f), BobMath.lateral(0.3f + cycle), 1.0e-4f);
+ }
+
+ @Test
+ void lateralReachesPeakAtExpectedPhase() {
+ assertEquals(1.0f, BobMath.lateral(BobMath.lateralPeakPhase()), 1.0e-4f);
+ }
+
+ @Test
+ void verticalIsNeverPositive() {
+ for (float phase = 0.0f; phase < 40.0f; phase += 0.125f) {
+ assertTrue(BobMath.vertical(phase) <= 0.0f, "вертикаль должна тянуть вниз");
+ assertTrue(BobMath.vertical(phase) >= -1.0f);
+ }
+ }
+
+ @Test
+ void verticalDipsTwicePerCycle() {
+ float cycle = (float) (2.0 * Math.PI / BobMath.STEP_RATE);
+ int dips = 0;
+ float previous = 0.0f;
+ for (float t = 0.0f; t <= cycle; t += 0.01f) {
+ float value = BobMath.vertical(t);
+ if (previous > -0.999f && value <= -0.999f) {
+ dips++;
+ }
+ previous = value;
+ }
+ assertEquals(2, dips, "за цикл должно быть два проседания (два шага)");
+ }
+
+ @Test
+ void stepRateMatchesVanillaLimbFrequency() {
+ assertEquals(0.6662f, BobMath.STEP_RATE, 1.0e-6f);
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java
new file mode 100644
index 0000000..0fe8523
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java
@@ -0,0 +1,149 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Покадровое состояние: одна правка цели за кадр, реакции на смену мира/игрока. */
+final class HandAnimStateTest {
+
+ private static final Object PLAYER_A = new Object();
+ private static final Object PLAYER_B = new Object();
+ private static final Object LEVEL_1 = new Object();
+ private static final Object LEVEL_2 = new Object();
+
+ private static final float DT = 0.5f;
+
+ @Test
+ void firstFrameDoesNotProduceSwayImpulse() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 30.0f, -15.0f, false, false, DT);
+ assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
+ state.beginFrame(PLAYER_A, LEVEL_1, 30.0f, -15.0f, false, false, DT);
+ assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
+ }
+
+ @Test
+ void swayReactsToMouseMotion() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ for (int i = 0; i < 8; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 6.0f * (i + 1), 0.0f, false, false, DT);
+ }
+ assertTrue(state.sway().yawLag() > 0.0f, "вращение камеры должно давать отставание");
+ }
+
+ @Test
+ void yawWrapMatchesEquivalentTurn() {
+ HandAnimState acrossBoundary = new HandAnimState();
+ acrossBoundary.beginFrame(PLAYER_A, LEVEL_1, 175.0f, 0.0f, false, false, DT);
+ acrossBoundary.beginFrame(PLAYER_A, LEVEL_1, -175.0f, 0.0f, false, false, DT);
+
+ HandAnimState plain = new HandAnimState();
+ plain.beginFrame(PLAYER_A, LEVEL_1, -5.0f, 0.0f, false, false, DT);
+ plain.beginFrame(PLAYER_A, LEVEL_1, 5.0f, 0.0f, false, false, DT);
+
+ assertEquals(plain.sway().yawLag(), acrossBoundary.sway().yawLag(), 1.0e-4f,
+ "переход через границу мира равен обычному повороту на те же 10 градусов");
+ }
+
+ @Test
+ void teleportResetsSwayInsteadOfImpulse() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ for (int i = 0; i < 8; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 6.0f * (i + 1), 0.0f, false, false, DT);
+ }
+ assertTrue(state.sway().yawLag() > 0.5f);
+ state.beginFrame(PLAYER_A, LEVEL_1, 180.0f, 0.0f, false, false, DT);
+ assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
+ }
+
+ @Test
+ void driftIsPerHand() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ for (int i = 0; i < 20; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ }
+ assertTrue(state.drift(true) > 0.9f, "дрейф в главной руке должен подняться к единице");
+ assertEquals(0.0f, state.drift(false), 1.0e-6f, "вторая рука не должна дёргаться");
+ }
+
+ @Test
+ void driftSettlesBackWhenUsageStops() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ for (int i = 0; i < 30; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ }
+ for (int i = 0; i < 120; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ }
+ assertEquals(0.0f, state.drift(true), 1.0e-3f, "после окончания использования дрейф должен уйти в ноль");
+ }
+
+ @Test
+ void joinProgressRisesThenStays() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ float previous = 0.0f;
+ for (int i = 0; i < 200; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ float value = state.joinProgress();
+ assertTrue(value >= previous - 1.0e-6f, "прогресс появления не должен откатываться");
+ previous = value;
+ }
+ assertEquals(1.0f, state.joinProgress(), 1.0e-3f);
+ }
+
+ @Test
+ void playerOrLevelChangeRestartsJoin() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ for (int i = 0; i < 200; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ }
+ assertEquals(1.0f, state.joinProgress(), 1.0e-3f);
+
+ state.beginFrame(PLAYER_B, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ assertTrue(state.joinProgress() < 0.1f, "смена игрока (респавн) перезапускает появление");
+ state.beginFrame(PLAYER_B, LEVEL_2, 0.0f, 0.0f, false, false, DT);
+ assertTrue(state.joinProgress() < 0.1f, "смена мира тоже перезапускает появление");
+ }
+
+ @Test
+ void nullPlayerResetsEverything() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ for (int i = 0; i < 30; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ }
+ state.beginFrame(null, null, 0.0f, 0.0f, false, false, DT);
+ assertEquals(0.0f, state.drift(true), 1.0e-6f);
+ assertEquals(0.0f, state.joinProgress(), 1.0e-6f);
+ assertTrue(state.sway().isSettled());
+ }
+
+ @Test
+ void resetReturnsToFreshState() {
+ HandAnimState state = new HandAnimState();
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ for (int i = 0; i < 30; i++) {
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT);
+ }
+ state.reset();
+ assertEquals(0.0f, state.drift(true), 1.0e-6f);
+ assertEquals(0.0f, state.joinProgress(), 1.0e-6f);
+ state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT);
+ assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f);
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java
new file mode 100644
index 0000000..fde3417
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java
@@ -0,0 +1,130 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Сборка позы руки из эффектов. */
+final class HandPoseMathTest {
+
+ private static final float EPS = 1.0e-6f;
+
+ @Test
+ void swayZeroStrengthIsZero() {
+ assertEquals(HandPoseMath.ZERO, HandPoseMath.sway(6.0f, 4.0f, 0.0f));
+ }
+
+ @Test
+ void swayStrengthScalesOutput() {
+ HandPoseMath.Pose unit = HandPoseMath.sway(3.0f, 2.0f, 1.0f);
+ HandPoseMath.Pose doubled = HandPoseMath.sway(3.0f, 2.0f, 2.0f);
+ assertEquals(unit.dx() * 2.0f, doubled.dx(), EPS);
+ assertEquals(unit.ry() * 2.0f, doubled.ry(), EPS);
+ }
+
+ @Test
+ void swaySignsFollowCameraTurn() {
+ // Поворот вправо (yaw+): предмет отстаёт влево на экране (+X) и доворачивается назад (ry-).
+ HandPoseMath.Pose yaw = HandPoseMath.sway(2.0f, 0.0f, 1.0f);
+ assertTrue(yaw.dx() > 0.0f);
+ assertTrue(yaw.ry() < 0.0f);
+ assertTrue(yaw.rz() > 0.0f);
+ // Взгляд вниз (pitch+): предмет отстаёт вверх (+Y) и задирается (rx-).
+ HandPoseMath.Pose pitch = HandPoseMath.sway(0.0f, 2.0f, 1.0f);
+ assertTrue(pitch.dy() > 0.0f);
+ assertTrue(pitch.rx() < 0.0f);
+ assertEquals(0.0f, pitch.dx(), EPS);
+ }
+
+ @Test
+ void swayMirrorsForOppositeLag() {
+ HandPoseMath.Pose positive = HandPoseMath.sway(2.0f, 1.0f, 1.0f);
+ HandPoseMath.Pose negative = HandPoseMath.sway(-2.0f, -1.0f, 1.0f);
+ assertEquals(-positive.dx(), negative.dx(), EPS);
+ assertEquals(-positive.dy(), negative.dy(), EPS);
+ assertEquals(-positive.rx(), negative.rx(), EPS);
+ assertEquals(-positive.ry(), negative.ry(), EPS);
+ assertEquals(-positive.rz(), negative.rz(), EPS);
+ }
+
+ @Test
+ void bobZeroAmplitudeIsZero() {
+ assertEquals(HandPoseMath.ZERO, HandPoseMath.bob(3.0f, 0.0f, 1.0f));
+ assertEquals(HandPoseMath.ZERO, HandPoseMath.bob(3.0f, 0.8f, 0.0f));
+ }
+
+ @Test
+ void bobIsBoundedAndScales() {
+ for (float phase = 0.0f; phase < 20.0f; phase += 0.1f) {
+ HandPoseMath.Pose pose = HandPoseMath.bob(phase, 1.0f, 1.0f);
+ assertTrue(Math.abs(pose.dx()) <= 0.045f + EPS);
+ assertTrue(Math.abs(pose.dy()) <= 0.035f + EPS);
+ assertTrue(Math.abs(pose.rz()) <= 2.5f + EPS);
+ assertTrue(pose.dy() <= 0.0f, "вертикаль боба не должна поднимать предмет");
+ }
+ HandPoseMath.Pose half = HandPoseMath.bob(0.7f, 0.5f, 1.0f);
+ HandPoseMath.Pose full = HandPoseMath.bob(0.7f, 1.0f, 1.0f);
+ assertEquals(full.dx() * 0.5f, half.dx(), EPS);
+ }
+
+ @Test
+ void driftPullsEachArmTowardCenter() {
+ HandPoseMath.Pose right = HandPoseMath.drift(1.0f, 1.0f, true);
+ assertTrue(right.dx() > 0.0f, "правая рука тянется влево на экране (+X)");
+ HandPoseMath.Pose left = HandPoseMath.drift(1.0f, 1.0f, false);
+ assertTrue(left.dx() < 0.0f, "левая рука тянется вправо на экране (-X)");
+ assertEquals(-right.dx(), left.dx(), EPS);
+ assertEquals(-right.ry(), left.ry(), EPS);
+ assertEquals(-right.rz(), left.rz(), EPS);
+ }
+
+ @Test
+ void driftZeroProgressAndStrengthGiveZero() {
+ assertEquals(HandPoseMath.ZERO, HandPoseMath.drift(0.0f, 1.0f, true));
+ assertEquals(HandPoseMath.ZERO, HandPoseMath.drift(1.0f, 0.0f, false));
+ }
+
+ @Test
+ void joinDropAtFullProgressIsZero() {
+ assertEquals(HandPoseMath.ZERO, HandPoseMath.joinDrop(1.0f));
+ }
+
+ @Test
+ void joinDropEasesOutMonotonically() {
+ float previousDrop = Float.POSITIVE_INFINITY;
+ for (float p = 0.0f; p <= 1.0f; p += 0.05f) {
+ float drop = -HandPoseMath.joinDrop(p).dy();
+ assertTrue(drop >= 0.0f && drop <= 0.85f + EPS);
+ assertTrue(drop <= previousDrop, "подъём должен замедляться к концу");
+ previousDrop = drop;
+ }
+ assertEquals(0.85f, -HandPoseMath.joinDrop(0.0f).dy(), EPS);
+ }
+
+ @Test
+ void addCombinesPosesAndZeroIsIdentity() {
+ HandPoseMath.Pose a = HandPoseMath.sway(2.0f, 1.0f, 1.0f);
+ HandPoseMath.Pose b = HandPoseMath.bob(0.5f, 1.0f, 1.0f);
+ HandPoseMath.Pose sum = HandPoseMath.add(a, b);
+ assertEquals(a.dx() + b.dx(), sum.dx(), EPS);
+ assertEquals(a.rx() + b.rx(), sum.rx(), EPS);
+ assertEquals(a, HandPoseMath.add(a, HandPoseMath.ZERO));
+ assertEquals(a, HandPoseMath.add(HandPoseMath.ZERO, a));
+ }
+
+ @Test
+ void significanceThresholdWorks() {
+ assertFalse(HandPoseMath.ZERO.isSignificant(1.0e-4f));
+ assertTrue(HandPoseMath.sway(1.0f, 0.0f, 1.0f).isSignificant(1.0e-4f));
+ assertFalse(HandPoseMath.drift(1.0f, 1.0f, true).isSignificant(20.0f));
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java
new file mode 100644
index 0000000..1a75f51
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java
@@ -0,0 +1,121 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.animation.hand;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Математика отставания предмета от поворота камеры. */
+final class SwayStateTest {
+
+ private static final float DT = 0.5f;
+
+ @Test
+ void withoutInputStaysZero() {
+ SwayState state = new SwayState();
+ for (int i = 0; i < 60; i++) {
+ state.update(0.0f, 0.0f, DT);
+ }
+ assertEquals(0.0f, state.yawLag(), 1.0e-6f);
+ assertEquals(0.0f, state.pitchLag(), 1.0e-6f);
+ assertTrue(state.isSettled());
+ }
+
+ @Test
+ void yawImpulseProducesPositiveLag() {
+ SwayState state = new SwayState();
+ state.update(4.0f, 0.0f, DT);
+ assertTrue(state.yawLag() > 0.0f, "движение мыши вправо должно давать положительное отставание");
+ assertEquals(0.0f, state.pitchLag(), 1.0e-6f);
+ }
+
+ @Test
+ void lagDecaysBackToZeroWhenMouseStill() {
+ SwayState state = new SwayState();
+ state.update(4.0f, -2.0f, DT);
+ for (int i = 0; i < 400; i++) {
+ state.update(0.0f, 0.0f, DT);
+ }
+ assertTrue(state.isSettled(), "без ввода отставание должно полностью затухнуть");
+ }
+
+ @Test
+ void lagIsClamped() {
+ SwayState state = new SwayState();
+ for (int i = 0; i < 40; i++) {
+ state.update(180.0f, -120.0f, DT);
+ }
+ assertTrue(state.yawLag() <= SwayState.MAX_YAW_LAG_DEG + 1.0e-4f);
+ assertTrue(state.pitchLag() >= -SwayState.MAX_PITCH_LAG_DEG - 1.0e-4f);
+ }
+
+ @Test
+ void mirroredInputsGiveMirroredLag() {
+ SwayState left = new SwayState();
+ SwayState right = new SwayState();
+ for (int i = 0; i < 10; i++) {
+ left.update(-3.0f, 1.5f, DT);
+ right.update(3.0f, -1.5f, DT);
+ }
+ assertEquals(left.yawLag(), -right.yawLag(), 1.0e-5f);
+ assertEquals(left.pitchLag(), -right.pitchLag(), 1.0e-5f);
+ }
+
+ @Test
+ void oppositeInputPullsBackThroughZero() {
+ SwayState state = new SwayState();
+ for (int i = 0; i < 10; i++) {
+ state.update(3.0f, 0.0f, DT);
+ }
+ float peak = state.yawLag();
+ for (int i = 0; i < 10; i++) {
+ state.update(-3.0f, 0.0f, DT);
+ }
+ assertTrue(state.yawLag() < peak, "обратное движение должно гасить отставание");
+ }
+
+ @Test
+ void nonPositiveDeltaIgnored() {
+ SwayState state = new SwayState();
+ state.update(2.0f, 1.0f, DT);
+ float yaw = state.yawLag();
+ state.update(2.0f, 1.0f, 0.0f);
+ state.update(2.0f, 1.0f, -1.0f);
+ assertEquals(yaw, state.yawLag(), 1.0e-6f);
+ assertFalse(state.isSettled());
+ }
+
+ @Test
+ void brokenInputsAreSafe() {
+ SwayState state = new SwayState();
+ state.update(Float.NaN, Float.POSITIVE_INFINITY, DT);
+ state.update(1.0f, 1.0f, Float.NaN);
+ assertEquals(0.0f, state.yawLag(), 1.0e-6f);
+ assertEquals(0.0f, state.pitchLag(), 1.0e-6f);
+ }
+
+ @Test
+ void wrapDegreesKeepsResultInHalfCircle() {
+ assertEquals(-20.0f, SwayState.wrapDegrees(340.0f), 1.0e-4f);
+ assertEquals(20.0f, SwayState.wrapDegrees(-340.0f), 1.0e-4f);
+ assertEquals(-180.0f, SwayState.wrapDegrees(180.0f), 1.0e-4f);
+ assertEquals(0.0f, SwayState.wrapDegrees(360.0f), 1.0e-4f);
+ assertEquals(5.0f, SwayState.wrapDegrees(365.0f), 1.0e-4f);
+ }
+
+ @Test
+ void resetClearsEverything() {
+ SwayState state = new SwayState();
+ state.update(4.0f, 2.0f, DT);
+ state.reset();
+ assertTrue(state.isSettled());
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java
new file mode 100644
index 0000000..1cc493a
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java
@@ -0,0 +1,86 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.protection;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class NotifyThrottleTest {
+
+ @Test
+ void firstCallIsAllowed() {
+ NotifyThrottle throttle = new NotifyThrottle(1000L);
+ assertTrue(throttle.tryAcquire(5_000L));
+ }
+
+ @Test
+ void secondCallInsideIntervalIsSuppressed() {
+ NotifyThrottle throttle = new NotifyThrottle(1_000L);
+ assertTrue(throttle.tryAcquire(5_000L));
+ assertFalse(throttle.tryAcquire(5_999L));
+ }
+
+ @Test
+ void callExactlyAtIntervalBoundaryIsAllowed() {
+ NotifyThrottle throttle = new NotifyThrottle(1_000L);
+ assertTrue(throttle.tryAcquire(5_000L));
+ assertTrue(throttle.tryAcquire(6_000L));
+ }
+
+ @Test
+ void callAfterIntervalIsAllowed() {
+ NotifyThrottle throttle = new NotifyThrottle(1_500L);
+ assertTrue(throttle.tryAcquire(10_000L));
+ assertFalse(throttle.tryAcquire(11_499L));
+ assertTrue(throttle.tryAcquire(11_500L));
+ assertTrue(throttle.tryAcquire(13_001L));
+ }
+
+ @Test
+ void remainingMsIsMonotonicAndClampedToZero() {
+ NotifyThrottle throttle = new NotifyThrottle(1_000L);
+ assertEquals(0L, throttle.remainingMs(1_000L), "never fired");
+ throttle.tryAcquire(2_000L);
+ assertEquals(1_000L, throttle.remainingMs(2_000L));
+ assertEquals(500L, throttle.remainingMs(2_500L));
+ assertEquals(0L, throttle.remainingMs(3_000L));
+ assertEquals(0L, throttle.remainingMs(10_000L));
+ }
+
+ @Test
+ void backwardsClockDoesNotLockOutCaller() {
+ NotifyThrottle throttle = new NotifyThrottle(1_000L);
+ throttle.tryAcquire(5_000L);
+ assertTrue(throttle.tryAcquire(1_000L), "moving time back should allow an emission");
+ }
+
+ @Test
+ void nonPositiveIntervalAlwaysAllows() {
+ NotifyThrottle zero = new NotifyThrottle(0L);
+ assertTrue(zero.tryAcquire(100L));
+ assertTrue(zero.tryAcquire(100L));
+ assertTrue(zero.tryAcquire(100L));
+
+ NotifyThrottle negative = new NotifyThrottle(-5L);
+ assertTrue(negative.tryAcquire(1L));
+ assertTrue(negative.tryAcquire(2L));
+ }
+
+ @Test
+ void resetRestoresInitialState() {
+ NotifyThrottle throttle = new NotifyThrottle(60_000L);
+ assertTrue(throttle.tryAcquire(1_000L));
+ assertFalse(throttle.tryAcquire(2_000L));
+ throttle.reset();
+ assertEquals(0L, throttle.remainingMs(2_000L));
+ assertTrue(throttle.tryAcquire(2_000L));
+ }
+}
diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java
new file mode 100644
index 0000000..2959caa
--- /dev/null
+++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java
@@ -0,0 +1,101 @@
+/*
+ * This file is part of the LoVisual Client distribution.
+ * Copyright (c) 2026 loki5512344.
+ *
+ * Licensed under the GNU General Public License v3.0.
+ */
+
+package dev.loki.lovisual.util.protection;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class SlotLockPolicyTest {
+
+ private static Map states(int... lockedZeroBasedSlots) {
+ Map out = SlotLockPolicy.defaultSlotStates();
+ for (int slot : lockedZeroBasedSlots) {
+ out.put(SlotLockPolicy.slotKey(slot), true);
+ }
+ return out;
+ }
+
+ @Test
+ void defaultsUnlockAllNineSlots() {
+ Map defaults = SlotLockPolicy.defaultSlotStates();
+ assertEquals(9, defaults.size());
+ for (int slot = 0; slot < 9; slot++) {
+ assertFalse(SlotLockPolicy.isSlotLocked(defaults, slot), "slot " + slot + " must be unlocked");
+ }
+ }
+
+ @Test
+ void slotKeyIsOneBased() {
+ assertEquals("slot1", SlotLockPolicy.slotKey(0));
+ assertEquals("slot5", SlotLockPolicy.slotKey(4));
+ assertEquals("slot9", SlotLockPolicy.slotKey(8));
+ }
+
+ @Test
+ void lockedSlotIsBlockedWhenNearbyCheckDisabled() {
+ assertTrue(SlotLockPolicy.isDropBlocked(states(4), 4, false, false));
+ assertTrue(SlotLockPolicy.isDropBlocked(states(4), 4, true, false));
+ }
+
+ @Test
+ void lockedSlotRespectsOnlyPlayersNearby() {
+ assertTrue(SlotLockPolicy.isDropBlocked(states(0), 0, true, true));
+ assertFalse(SlotLockPolicy.isDropBlocked(states(0), 0, false, true));
+ }
+
+ @Test
+ void unlockedSlotIsNeverBlocked() {
+ assertFalse(SlotLockPolicy.isDropBlocked(states(1, 2), 0, true, false));
+ assertFalse(SlotLockPolicy.isDropBlocked(states(1, 2), 0, false, false));
+ }
+
+ @Test
+ void hotbarEdgesAreCovered() {
+ Map edgeStates = states(0, 8);
+ assertTrue(SlotLockPolicy.isDropBlocked(edgeStates, 0, false, false));
+ assertTrue(SlotLockPolicy.isDropBlocked(edgeStates, 8, false, false));
+ assertFalse(SlotLockPolicy.isDropBlocked(edgeStates, 4, false, false));
+ }
+
+ @Test
+ void outOfRangeSlotsAreNeverBlocked() {
+ Map all = states(0, 1, 2, 3, 4, 5, 6, 7, 8);
+ assertFalse(SlotLockPolicy.isDropBlocked(all, -1, true, false));
+ assertFalse(SlotLockPolicy.isDropBlocked(all, 9, true, false));
+ assertFalse(SlotLockPolicy.isSlotLocked(all, -1));
+ assertFalse(SlotLockPolicy.isSlotLocked(all, 9));
+ }
+
+ @Test
+ void missingOrNullStatesAreNeverBlocked() {
+ assertFalse(SlotLockPolicy.isDropBlocked(null, 0, true, false));
+ assertFalse(SlotLockPolicy.isDropBlocked(new HashMap<>(), 0, true, false));
+ assertFalse(SlotLockPolicy.isSlotLocked(null, 0));
+ }
+
+ @Test
+ void unknownOrNonBooleanEntriesAreIgnored() {
+ Map odd = new HashMap<>();
+ odd.put("slot", true);
+ odd.put("slot10", true);
+ odd.put("slot0", true);
+ assertFalse(SlotLockPolicy.isSlotLocked(odd, 0));
+ assertFalse(SlotLockPolicy.isDropBlocked(odd, 0, true, false));
+ }
+
+ @Test
+ void nearbyCheckDoesNotAffectUnlockedSlots() {
+ assertFalse(SlotLockPolicy.isDropBlocked(states(8), 3, true, true));
+ }
+}