chore(history): squash 3 commit(s) from 2026-09-05
- feat: батч QoL-модулей из Delta + фиксы рендера руки - refactor: Фаза 8 — разбивка топ-гигантов по правилу ≤200 строк - refactor: Фаза 8, проход 2 — Predictions, TargetESP, HudNotifier, Module
This commit is contained in:
parent
e504859777
commit
d83f9a29ff
105 changed files with 10287 additions and 5785 deletions
2
TODO.md
2
TODO.md
|
|
@ -13,7 +13,7 @@
|
|||
## Правила (для мода, продублированы в его TODO)
|
||||
- KISS / DRY / SOLID
|
||||
- Макс. 200 строк на файл (фасады-делегаты исключение)
|
||||
- Макс. 3 файла на папку
|
||||
- Макс. 4 файла на папку
|
||||
- Никакого статика, никаких this-escape, никаких мёртвых миксинов
|
||||
- Без пустышек; каждый этап компилируется (`./gradlew build`)
|
||||
- Тесты обязательны для новых модулей (JUnit)
|
||||
|
|
|
|||
|
|
@ -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` зелёный
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<Integer> durationMs = new NumberValue<>("duration_ms", 2000, 500, 6000);
|
||||
private final NumberValue<Integer> fadeMs = new NumberValue<>("fade_ms", 250, 0, 1500);
|
||||
private final NumberValue<Double> 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<SettingDef> 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<ToastDraw> 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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ToastDraw> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String> 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DraggableHudElement> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DraggableHudElement> 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 extends DraggableHudElement> T get(Class<T> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DraggableHudElement> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<SettingDef> 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<String, ? extends Map<String, ?>> 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<String, UiBounds> 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<UUID, LerpingBossEvent> 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,
|
||||
|
|
|
|||
|
|
@ -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<String, UiBounds> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, ? extends Map<String, ?>> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UUID, LerpingBossEvent> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Setting> 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<ModuleActivationSource> 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<SettingDef> declaredSettingDefs = new ArrayList<>();
|
||||
private final Map<ConfigValue<?>, 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<String> normalizeAliases(String[] aliases) {
|
||||
if (aliases == null || aliases.length == 0) return List.of();
|
||||
|
||||
List<String> 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<Integer> num(String configName, int def, int min, int max) {
|
||||
|
|
@ -202,9 +131,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
|
|||
}
|
||||
|
||||
protected final NumberValue<Integer> num(String configName, String settingId, int def, int min, int max) {
|
||||
NumberValue<Integer> 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<Long> num(String configName, long def, long min, long max) {
|
||||
|
|
@ -212,9 +139,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
|
|||
}
|
||||
|
||||
protected final NumberValue<Long> num(String configName, String settingId, long def, long min, long max) {
|
||||
NumberValue<Long> 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<Float> num(String configName, float def, float min, float max) {
|
||||
|
|
@ -222,9 +147,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
|
|||
}
|
||||
|
||||
protected final NumberValue<Float> num(String configName, String settingId, float def, float min, float max) {
|
||||
NumberValue<Float> 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<Double> num(String configName, double def, double min, double max) {
|
||||
|
|
@ -232,9 +155,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
|
|||
}
|
||||
|
||||
protected final NumberValue<Double> num(String configName, String settingId, double def, double min, double max) {
|
||||
NumberValue<Double> 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 <E extends Enum<E>> EnumValue<E> enumMode(String configName, E def) {
|
||||
|
|
@ -252,10 +171,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
|
|||
}
|
||||
|
||||
protected final <E extends Enum<E>> EnumValue<E> enumSetting(String configName, String settingId, E def) {
|
||||
Class<E> enumClass = def.getDeclaringClass();
|
||||
EnumValue<E> 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 <E extends Enum<E>> EnumValue<E> enumSetting(String configName, String settingId, E def, Class<E> enumClass) {
|
||||
EnumValue<E> 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 <E extends Enum<E>> EnumValue<E> enumSetting(String configName, String settingId, E def, E... options) {
|
||||
EnumValue<E> 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<String> 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<String, Boolean> defaults) {
|
||||
|
|
@ -351,9 +251,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin
|
|||
}
|
||||
|
||||
protected final BooleanMapValue group(String configName, String settingId, Map<String, Boolean> 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<String, Boolean> 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 <E extends Enum<E>> EnumValue<E> 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<String> 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<String> 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<ConfigValue<?>> getConfigValues() {
|
||||
Map<String, ConfigValue<?>> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, FunctionBindSetting> actions,
|
||||
List<Setting> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ConfigValue<?>> orderedValues(BooleanValue enabledValue,
|
||||
EnumValue<ModuleActivationSource> activationSourceValue,
|
||||
BooleanValue moduleListVisibleValue,
|
||||
List<Setting> settings) {
|
||||
Map<String, ConfigValue<?>> 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<Setting> 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<Setting> 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<Setting> 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> normalizeAliases(String[] aliases) {
|
||||
if (aliases == null || aliases.length == 0) return List.of();
|
||||
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ConfigValue<?>, SettingDef> declare) {
|
||||
BooleanValue value = new BooleanValue(configName, def);
|
||||
declare.accept(value, SettingDef.bool(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public static NumberValue<Integer> num(String configName, String settingId, int def, int min, int max, BiConsumer<ConfigValue<?>, SettingDef> declare) {
|
||||
NumberValue<Integer> value = new NumberValue<>(configName, def, min, max);
|
||||
declare.accept(value, SettingDef.number(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public static NumberValue<Long> num(String configName, String settingId, long def, long min, long max, BiConsumer<ConfigValue<?>, SettingDef> declare) {
|
||||
NumberValue<Long> value = new NumberValue<>(configName, def, min, max);
|
||||
declare.accept(value, SettingDef.number(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public static NumberValue<Float> num(String configName, String settingId, float def, float min, float max, BiConsumer<ConfigValue<?>, SettingDef> declare) {
|
||||
NumberValue<Float> value = new NumberValue<>(configName, def, min, max);
|
||||
declare.accept(value, SettingDef.number(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public static NumberValue<Double> num(String configName, String settingId, double def, double min, double max, BiConsumer<ConfigValue<?>, SettingDef> declare) {
|
||||
NumberValue<Double> 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<ConfigValue<?>, SettingDef> declare, String... options) {
|
||||
ModeValue value = new ModeValue(configName, def, options);
|
||||
declare.accept(value, SettingDef.mode(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public static <E extends Enum<E>> EnumValue<E> enumSetting(String configName, String settingId, E def, BiConsumer<ConfigValue<?>, SettingDef> declare) {
|
||||
Class<E> enumClass = def.getDeclaringClass();
|
||||
EnumValue<E> value = new EnumValue<>(configName, def, enumClass);
|
||||
declare.accept(value, SettingDef.mode(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public static <E extends Enum<E>> EnumValue<E> enumSetting(String configName, String settingId, E def, Class<E> enumClass, BiConsumer<ConfigValue<?>, SettingDef> declare) {
|
||||
EnumValue<E> value = new EnumValue<>(configName, def, enumClass);
|
||||
declare.accept(value, SettingDef.mode(settingId, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <E extends Enum<E>> EnumValue<E> enumSetting(String configName, String settingId, E def, BiConsumer<ConfigValue<?>, SettingDef> declare, E... options) {
|
||||
EnumValue<E> 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<ConfigValue<?>, 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<ConfigValue<?>, 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<ConfigValue<?>, 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<ConfigValue<?>, 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<String> defaults, BiConsumer<ConfigValue<?>, 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<ConfigValue<?>, 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<String, Boolean> defaults, BiConsumer<ConfigValue<?>, 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<ConfigValue<?>, 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<ConfigValue<?>, SettingDef> declare) {
|
||||
Map<String, Boolean> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TimerPlate> 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<TrajectoryResult> 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<TrajectoryResult> 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<Vec3> 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<Vec3> points, double flightTicks, long impactTick, ItemStack iconStack,
|
||||
int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity,
|
||||
boolean hitBlock) {
|
||||
private TrajectoryResult(List<Vec3> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Integer> maxSimTicks,
|
||||
NumberValue<Integer> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Vec3> points, double flightTicks, long impactTick, ItemStack iconStack,
|
||||
int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity,
|
||||
boolean hitBlock) {
|
||||
public TrajectoryResult(List<Vec3> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> maxSimTicks,
|
||||
NumberValue<Integer> 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<Vec3> 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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> 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<Integer> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> 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<Integer> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Float> density =
|
||||
num("atmoDawnFogDensity", "density", 0.35f, 0.05f, 0.8f);
|
||||
private final NumberValue<Float> scatterHeight =
|
||||
num("atmoDawnFogScatterHeight", "scatter_height", 76.0f, 60.0f, 120.0f);
|
||||
private final NumberValue<Float> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Float> smokeDensity =
|
||||
visibleWhen(num("handChamsSmokeDensity", SETTING_SMOKE_DENSITY, 1.0f, 0.2f, 2.5f), this::isSmokeMode);
|
||||
private final NumberValue<Float> noiseAlpha =
|
||||
visibleWhen(num("handChamsNoiseAlpha", SETTING_NOISE_ALPHA, 0.85f, 0.0f, 1.0f), this::isNoiseMode);
|
||||
private final NumberValue<Float> metallicIntensity =
|
||||
visibleWhen(num("handChamsMetallicIntensity", SETTING_METALLIC_INTENSITY, 1.35f, 0.0f, 5.0f), this::isMetallicMode);
|
||||
private final NumberValue<Float> 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
|
||||
|
|
|
|||
|
|
@ -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<Float> swayIntensity = num("sway_intensity", 1.0f, 0.0f, 2.0f);
|
||||
public final NumberValue<Float> 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<Float> bobStrength = visibleWhen(num("bob_strength", 1.0f, 0.0f, 2.0f), walkBob::get);
|
||||
public final BooleanValue useDrift = bool("use_drift", true);
|
||||
public final NumberValue<Float> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Float> lineWidth =
|
||||
num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f);
|
||||
private final NumberValue<Float> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Float> 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<ItemStack> 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<ItemStack> collectContents(ItemStack stack) {
|
||||
ItemContainerContents container = stack.get(DataComponents.CONTAINER);
|
||||
if (container == null) return null;
|
||||
|
||||
NonNullList<ItemStack> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer> 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()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ItemEntity, ItemEntityRenderState> {
|
||||
|
|
@ -51,19 +52,26 @@ public abstract class ItemEntityRendererMixin extends EntityRenderer<ItemEntity,
|
|||
CameraRenderState cameraState,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
WorldTweaks module = Modules.get(WorldTweaks.class);
|
||||
if (module == null || !module.isItemPhysicsEnabled() || state.item.isEmpty()) {
|
||||
ItemPhysic physics = Modules.get(ItemPhysic.class);
|
||||
if (physics == null || !physics.isEnabled() || state.item.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
matrices.pushPose();
|
||||
if (physics.shouldReduceSize()) {
|
||||
matrices.scale(0.5F, 0.5F, 0.5F);
|
||||
}
|
||||
if (!physics.shouldCancelBob()) {
|
||||
float bob = Mth.sin(state.ageInTicks / 10.0F + state.bobOffset) * 0.1F + 0.1F;
|
||||
matrices.translate(0.0F, bob, 0.0F);
|
||||
}
|
||||
AABB box = state.item.getModelBoundingBox();
|
||||
float baseY = -((float) box.minY) + 0.0625F;
|
||||
matrices.translate(0.0F, baseY, 0.0F);
|
||||
|
||||
if (((IItemEntityRenderState) state).lovisual$isOnGround()) {
|
||||
if (((IItemEntityRenderState) state).lovisual$isOnGround() && physics.shouldLayFlat()) {
|
||||
matrices.mulPose(Axis.XP.rotationDegrees(90.0F));
|
||||
} else {
|
||||
} else if (physics.shouldSpinInAir()) {
|
||||
float rotation = ItemEntity.getSpin(state.ageInTicks, state.bobOffset);
|
||||
matrices.mulPose(Axis.XP.rotationDegrees(rotation * 300.0F));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,15 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
import dev.loki.lovisual.features.module.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.HoldMyItems;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.ViewModel;
|
||||
import dev.loki.lovisual.util.animation.EquipProgressComposer;
|
||||
import dev.loki.lovisual.util.animation.hand.HandAnimState;
|
||||
|
||||
@Environment(EnvType.CLIENT)
|
||||
@Mixin(ItemInHandRenderer.class)
|
||||
public abstract class ItemInHandRendererMixin {
|
||||
|
||||
@Unique private boolean lovisual$handPosePushed;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$skipTickWithoutPlayer(CallbackInfo ci) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
|
|
@ -41,6 +45,36 @@ public abstract class ItemInHandRendererMixin {
|
|||
}
|
||||
}
|
||||
|
||||
// Динамика рук (sway/боб/дрейф/появление): состояние шагает ровно раз за
|
||||
// кадр в HoldMyItems.onFrame — вне рендер-пути, поэтому повторный проход
|
||||
// руки (захват маски Chams) видит те же значения, что и основной проход.
|
||||
// Здесь состояние только читается и применяется до ванильных трансформов.
|
||||
@Inject(method = "submitArmWithItem", at = @At("HEAD"))
|
||||
private void lovisual$applyHandDynamics(
|
||||
AbstractClientPlayer player, float tickDelta, float pitch, InteractionHand hand,
|
||||
float swingProgress, ItemStack item, float equipProgress, PoseStack matrices,
|
||||
SubmitNodeCollector queue, int light, CallbackInfo ci
|
||||
) {
|
||||
HoldMyItems holdMyItems = Modules.get(HoldMyItems.class);
|
||||
if (holdMyItems == null || !holdMyItems.isEnabled()) return;
|
||||
if (hand == InteractionHand.OFF_HAND && !holdMyItems.offhandToo.get()) return;
|
||||
HandAnimState handAnim = holdMyItems.handAnim();
|
||||
if (handAnim == null) return;
|
||||
|
||||
lovisual$handPosePushed = HandDynamics.apply(holdMyItems, handAnim, player, tickDelta, hand, matrices);
|
||||
}
|
||||
|
||||
@Inject(method = "submitArmWithItem", at = @At("RETURN"))
|
||||
private void lovisual$restoreHandDynamics(
|
||||
AbstractClientPlayer player, float tickDelta, float pitch, InteractionHand hand,
|
||||
float swingProgress, ItemStack item, float equipProgress, PoseStack matrices,
|
||||
SubmitNodeCollector queue, int light, CallbackInfo ci
|
||||
) {
|
||||
if (!lovisual$handPosePushed) return;
|
||||
lovisual$handPosePushed = false;
|
||||
matrices.popPose();
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "submitArmWithItem",
|
||||
at = @At("HEAD")
|
||||
|
|
@ -68,15 +102,32 @@ public abstract class ItemInHandRendererMixin {
|
|||
);
|
||||
}
|
||||
|
||||
// Четвёртый float среди аргументов (tickDelta, pitch, swingProgress, equipProgress) —
|
||||
// equipProgress; индекс 7 указывал бы на matrices (PoseStack), из-за чего модификаторы
|
||||
// не применялись. ViewModel и HoldMyItems сводятся в одну цепочку:
|
||||
// кривая на полном диапазоне, затем занижение амплитуды (EquipProgressComposer).
|
||||
@ModifyVariable(
|
||||
method = "submitArmWithItem",
|
||||
at = @At("HEAD"),
|
||||
argsOnly = true,
|
||||
index = 7)
|
||||
private float lovisual$scaleEquipProgress(float equipProgress) {
|
||||
ordinal = 3)
|
||||
private float lovisual$composeEquipProgress(
|
||||
float equipProgress, AbstractClientPlayer player, float tickDelta,
|
||||
float pitch, InteractionHand hand) {
|
||||
ViewModel viewModel = Modules.get(ViewModel.class);
|
||||
if (viewModel == null || !viewModel.isEnabled()) return equipProgress;
|
||||
return equipProgress * viewModel.equipLowering.get();
|
||||
HoldMyItems holdMyItems = Modules.get(HoldMyItems.class);
|
||||
boolean lowerEnabled = viewModel != null && viewModel.isEnabled();
|
||||
boolean curveEnabled = holdMyItems != null && holdMyItems.isEnabled()
|
||||
&& !(hand == InteractionHand.OFF_HAND && !holdMyItems.offhandToo.get());
|
||||
if (!lowerEnabled && !curveEnabled) return equipProgress;
|
||||
return EquipProgressComposer.compose(
|
||||
equipProgress,
|
||||
curveEnabled,
|
||||
curveEnabled ? holdMyItems.easeStrength.get() : 1.0f,
|
||||
curveEnabled ? holdMyItems.swayIntensity.get() : 1.0f,
|
||||
lowerEnabled,
|
||||
lowerEnabled ? viewModel.equipLowering.get() : 1.0f
|
||||
);
|
||||
}
|
||||
|
||||
@Unique
|
||||
|
|
@ -84,20 +135,6 @@ public abstract class ItemInHandRendererMixin {
|
|||
return player != null && (player.isInWater() || player.isInLava());
|
||||
}
|
||||
|
||||
@ModifyVariable(
|
||||
method = "submitArmWithItem",
|
||||
at = @At("HEAD"),
|
||||
argsOnly = true,
|
||||
index = 7)
|
||||
private float lovisual$holdMyItemsEquipCurve(float equipProgress, AbstractClientPlayer player, float tickDelta,
|
||||
float pitch, InteractionHand hand) {
|
||||
HoldMyItems hmi = Modules.get(HoldMyItems.class);
|
||||
if (hmi == null || !hmi.isEnabled()) return equipProgress;
|
||||
if (hand == InteractionHand.OFF_HAND && !hmi.offhandToo.get()) return equipProgress;
|
||||
|
||||
return hmi.shapeEquipProgress(equipProgress);
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemDisplayContext;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V",
|
||||
at = @At(
|
||||
|
|
|
|||
|
|
@ -10,16 +10,30 @@ package dev.loki.lovisual.mixins;
|
|||
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import dev.loki.lovisual.render.SeeInvisiblesState;
|
||||
import dev.loki.lovisual.render.ViewObstructionFadeState;
|
||||
|
||||
@Mixin(LivingEntityRenderState.class)
|
||||
public abstract class LivingEntityRenderStateMixin implements ViewObstructionFadeState {
|
||||
public abstract class LivingEntityRenderStateMixin implements ViewObstructionFadeState, SeeInvisiblesState {
|
||||
@Unique
|
||||
private boolean lovisual$viewObstructionFadeActive;
|
||||
|
||||
@Unique
|
||||
private float lovisual$viewObstructionFadeAlpha = 1.0f;
|
||||
|
||||
@Unique
|
||||
private boolean lovisual$seeInvisRevealed;
|
||||
|
||||
@Override
|
||||
public boolean lovisual$isSeeInvisRevealed() {
|
||||
return lovisual$seeInvisRevealed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lovisual$setSeeInvisRevealed(boolean revealed) {
|
||||
this.lovisual$seeInvisRevealed = revealed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lovisual$isViewObstructionFadeActive() {
|
||||
return lovisual$viewObstructionFadeActive;
|
||||
|
|
|
|||
|
|
@ -32,8 +32,11 @@ import org.spongepowered.asm.mixin.Shadow;
|
|||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import dev.loki.lovisual.features.module.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.InvisEsp;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.NameTags;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
|
||||
import dev.loki.lovisual.render.SeeInvisiblesReveal;
|
||||
import dev.loki.lovisual.render.SeeInvisiblesState;
|
||||
import dev.loki.lovisual.render.ViewObstructionFadeContext;
|
||||
import dev.loki.lovisual.render.ViewObstructionFadeState;
|
||||
import dev.loki.lovisual.render.iris.IrisRuntime;
|
||||
|
|
@ -60,6 +63,24 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
|
|||
}
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V",
|
||||
at = @At("TAIL")
|
||||
)
|
||||
private void lovisual$seeInvisiblesReveal(T entity, S state, float tickProgress, CallbackInfo ci) {
|
||||
SeeInvisiblesReveal.reveal(entity, state);
|
||||
}
|
||||
@ModifyExpressionValue(
|
||||
method = "submit",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/LivingEntityRenderer;getRenderType(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;ZZZ)Lnet/minecraft/client/renderer/rendertype/RenderType;")
|
||||
)
|
||||
private RenderType lovisual$seeInvisiblesTranslucent(RenderType original, @Local(argsOnly = true) S state) {
|
||||
if (!(state instanceof SeeInvisiblesState seeInvis) || !seeInvis.lovisual$isSeeInvisRevealed()) {
|
||||
return original;
|
||||
}
|
||||
return SeeInvisiblesReveal.renderTypeFor(original, getTextureLocation(state));
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V",
|
||||
at = @At("TAIL")
|
||||
|
|
|
|||
|
|
@ -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.mixins;
|
||||
|
||||
import dev.loki.lovisual.features.module.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.player.LockSlot;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
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.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Cancels the Q / Ctrl+Q drop of the selected hotbar slot
|
||||
* while LockSlot protects it.
|
||||
*/
|
||||
@Mixin(LocalPlayer.class)
|
||||
public class LocalPlayerDropMixin {
|
||||
|
||||
@Inject(method = "drop(Z)Z", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$lockSlot$blockSelectedDrop(boolean wholeStack, CallbackInfoReturnable<Boolean> cir) {
|
||||
LockSlot lockSlot = Modules.get(LockSlot.class);
|
||||
if (lockSlot != null && lockSlot.shouldBlockSelectedDrop()) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 <T extends LivingEntity> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* Чистая математика синусоидального покачивания предмета при ходьбе/беге.
|
||||
* <p>
|
||||
* Фаза берётся из накопителя шагов игрока ({@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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}), рендер только читает значения — это исключает
|
||||
* двойные шаги, когда кадр рендерит обе руки.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 от обзора, боб от
|
||||
* ходьбы, дрейф к центру при использовании предмета и «въезд» при входе в мир.
|
||||
* <p>
|
||||
* Конвенции знаков соответствуют пространству ванильного рендера руки:
|
||||
* +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
* <p>
|
||||
* Модель: дельты взгляда за кадр (в градусах) накапливаются в отставание
|
||||
* {@code lag}, ограниченное сверху, и экспоненциально затухают к нулю
|
||||
* (RELAX). Отображаемое значение догоняет накопленное отставание
|
||||
* экспонентой (RESPONSE). Всё в градусах, время — в тиках, поэтому
|
||||
* поведение не зависит от частоты кадров.
|
||||
* <p>
|
||||
* Класс чистый: без 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p>The caller supplies the current time, so the class has no clock
|
||||
* dependency and stays unit-testable.</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Boolean> defaultSlotStates() {
|
||||
Map<String, Boolean> 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<String, Boolean> 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<String, Boolean> slotStates,
|
||||
int selectedSlot,
|
||||
boolean playersNearby,
|
||||
boolean onlyNearby
|
||||
) {
|
||||
if (!isSlotLocked(slotStates, selectedSlot)) return false;
|
||||
return !onlyNearby || playersNearby;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "Плавное появление при входе"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue