From d83f9a29ff099ec8c7a599bbed703d61e97e1500 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sat, 5 Sep 2026 23:07:43 +0200 Subject: [PATCH] chore(history): squash 3 commit(s) from 2026-09-05 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat: батч QoL-модулей из Delta + фиксы рендера руки - refactor: Фаза 8 — разбивка топ-гигантов по правилу ≤200 строк - refactor: Фаза 8, проход 2 — Predictions, TargetESP, HudNotifier, Module --- TODO.md | 2 +- combatant-client-26.2/TODO.md | 66 +- .../hud/draggable/DraggableHudElement.java | 3 +- .../DraggableHudElementRegistry.java | 1392 +------------- .../gui/hud/draggable/impl/HudNotifier.java | 615 +----- .../impl/hudnotifier/ToastCardRenderer.java | 193 ++ .../impl/hudnotifier/ToastGeometry.java | 200 ++ .../impl/hudnotifier/ToastModel.java | 142 ++ .../impl/hudnotifier/ToastStyle.java | 190 ++ .../interaction/RegistryDragController.java | 168 ++ .../interaction/RegistryDragSession.java | 200 ++ .../registry/RegistryAnchorLayout.java | 200 ++ .../registry/RegistrySnapSupport.java | 199 ++ .../hud/draggable/registry/RegistryState.java | 92 + .../registry/RegistryWidgetStore.java | 196 ++ .../render/RegistryOverlayRenderer.java | 200 ++ .../render/RegistryRenderPipeline.java | 171 ++ .../render/RegistryResizeHandler.java | 130 ++ .../render/RegistryWidgetRenderPath.java | 110 ++ .../hud/nondraggable/impl/CustomHotbar.java | 16 +- .../hud/nondraggable/impl/DynamicIsland.java | 773 +------- .../dynamicisland/IslandBoundsPatches.java | 141 ++ .../impl/dynamicisland/IslandPatchOps.java | 139 ++ .../dynamicisland/IslandRuntimePatches.java | 127 ++ .../impl/dynamicisland/IslandUiUtil.java | 199 ++ .../controls/IslandControls.java | 123 ++ .../loki/lovisual/features/module/Module.java | 276 +-- .../features/module/module/ModuleBinds.java | 57 + .../module/module/ModuleConfigValues.java | 106 + .../features/module/module/ModuleNames.java | 111 ++ .../module/module/ModuleSettingValues.java | 156 ++ .../module/modules/combat/NoFriendDamage.java | 53 + .../module/modules/combat/Predictions.java | 727 +------ .../module/modules/combat/TargetESP.java | 659 +------ .../combat/predictions/PredictionsAim.java | 190 ++ .../predictions/PredictionsGeometry.java | 194 ++ .../combat/predictions/PredictionsModel.java | 157 ++ .../predictions/PredictionsSimulation.java | 196 ++ .../combat/targetesp/TargetEspCrystals.java | 179 ++ .../combat/targetesp/TargetEspEffects.java | 178 ++ .../combat/targetesp/TargetEspModes.java | 199 ++ .../combat/targetesp/TargetEspRender.java | 199 ++ .../module/modules/misc/NoServerPack.java | 105 + .../module/modules/player/AutoRespawn.java | 88 + .../module/modules/player/DeathCoords.java | 91 + .../module/modules/player/LockSlot.java | 92 + .../module/modules/player/TapeMouse.java | 92 + .../module/modules/visuals/Animations.java | 110 ++ .../module/modules/visuals/AtmoDawnFog.java | 145 ++ .../module/modules/visuals/Chams.java | 48 +- .../module/modules/visuals/HoldMyItems.java | 59 +- .../module/modules/visuals/InvisEsp.java | 66 +- .../module/modules/visuals/ItemPhysic.java | 52 + .../module/modules/visuals/PostFX.java | 209 +- .../modules/visuals/ShulkerPreview.java | 154 ++ .../module/modules/visuals/WorldTweaks.java | 6 - .../mixins/AbstractContainerScreenMixin.java | 78 + .../AtmosphericFogEnvironmentMixin.java | 7 + .../ClientCommonPacketListenerImplMixin.java | 20 + .../lovisual/mixins/FogRendererMixin.java | 5 + .../loki/lovisual/mixins/HandDynamics.java | 93 + .../mixins/ItemEntityRendererMixin.java | 18 +- .../mixins/ItemInHandRendererMixin.java | 73 +- .../mixins/LivingEntityRenderStateMixin.java | 16 +- .../mixins/LivingEntityRendererMixin.java | 21 + .../lovisual/mixins/LocalPlayerDropMixin.java | 32 + .../mixins/MultiPlayerGameModeMixin.java | 9 +- .../mixins/MultiPlayerGameModeThrowMixin.java | 36 + .../mixins/accessors/KeyMappingAccessor.java | 20 + .../lovisual/render/SeeInvisiblesReveal.java | 55 + .../lovisual/render/SeeInvisiblesState.java | 18 + .../pipeline/LoVisualRenderPipelines.java | 1708 +++-------------- .../pipeline/pipelines/PipelineTemplates.java | 60 + .../pipeline/pipelines/ScreenPipelines.java | 72 + .../pipeline/pipelines/UiPipelines.java | 96 + .../pipeline/pipelines/WorldPipelines.java | 151 ++ .../uniform/impl/HandNoiseUniforms.java | 88 + .../lovisual/util/animation/EaseCurves.java | 36 + .../util/animation/EquipProgressComposer.java | 32 + .../lovisual/util/animation/SmoothValue.java | 119 ++ .../lovisual/util/animation/hand/BobMath.java | 60 + .../util/animation/hand/HandAnimState.java | 125 ++ .../util/animation/hand/HandPoseMath.java | 160 ++ .../util/animation/hand/SwayState.java | 117 ++ .../util/protection/NotifyThrottle.java | 61 + .../util/protection/SlotLockPolicy.java | 71 + .../resources/assets/lovisual/lang/en_us.json | 60 +- .../resources/assets/lovisual/lang/ru_ru.json | 60 +- .../assets/lovisual/shaders/hand_noise.frag | 156 ++ .../src/main/resources/lovisual.mixins.json | 3 + .../module/modules/misc/NoServerPackTest.java | 52 + .../modules/player/AutoRespawnTest.java | 81 + .../modules/player/DeathCoordsTest.java | 66 + .../module/modules/player/TapeMouseTest.java | 65 + .../modules/visuals/AtmoDawnFogTest.java | 59 + .../modules/visuals/PostFxPresetTest.java | 78 + .../util/animation/EaseCurvesTest.java | 76 + .../animation/EquipProgressComposerTest.java | 72 + .../util/animation/SmoothValueTest.java | 162 ++ .../util/animation/hand/BobMathTest.java | 97 + .../animation/hand/HandAnimStateTest.java | 149 ++ .../util/animation/hand/HandPoseMathTest.java | 130 ++ .../util/animation/hand/SwayStateTest.java | 121 ++ .../util/protection/NotifyThrottleTest.java | 86 + .../util/protection/SlotLockPolicyTest.java | 101 + 105 files changed, 10287 insertions(+), 5785 deletions(-) create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/LocalPlayerDropMixin.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java create mode 100644 combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java create mode 100644 combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java create mode 100644 combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java diff --git a/TODO.md b/TODO.md index 2ff03e1..bad0021 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,7 @@ ## Правила (для мода, продублированы в его TODO) - KISS / DRY / SOLID - Макс. 200 строк на файл (фасады-делегаты исключение) -- Макс. 3 файла на папку +- Макс. 4 файла на папку - Никакого статика, никаких this-escape, никаких мёртвых миксинов - Без пустышек; каждый этап компилируется (`./gradlew build`) - Тесты обязательны для новых модулей (JUnit) diff --git a/combatant-client-26.2/TODO.md b/combatant-client-26.2/TODO.md index 1a7a576..725a713 100644 --- a/combatant-client-26.2/TODO.md +++ b/combatant-client-26.2/TODO.md @@ -5,7 +5,7 @@ ## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат) - **KISS / DRY / SOLID** — простая структура, без оверинжиниринга - **Макс. 200 строк на файл** (было >1500) — превышение = разбить на модули -- **Макс. 3 файла на папку** — больше = новая подпапка по смыслу +- **Макс. 4 файла на папку** — больше = новая подпапка по смыслу - **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле - **Никаких this-escape** — конструктор не публикует `this` - **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel @@ -148,4 +148,68 @@ - [ ] Юнит-тесты: EventBus, конфиг, утилиты (игровые классы — mock) - [ ] По мере переписи модулей — тест на каждый +## Фаза 7: QoL-модули из Delta (легит) — ПЛАН, обсуждаем +> Референс: `ref/delta-26.2-main` (чит-клиент, код обфусцирован — берём только идеи, +> переписываем под наши правила; у Delta 114 модулей, у нас 63). +> Уже перенесено: SeeInvisibles→InvisESP, HandsShader→Chams/Noise, AtmoDawnFog, +> ShulkerPreview, Animations, ItemPhysic (из WorldTweaks), HoldMyItems (фикс ordinal-бага, +> математика в EaseCurves). Реф-исходники: ref/hmi-src, ref/soupvisuals-src. + +### Решения по обсуждению (2026-09-05) +- [x] Wasted — НЕ делаем +- [x] TapeMouse — делаем (пользователь: AFK-фермы, не чит-софт) +- [x] DeathCoords — делаем +- [x] ItemScroller / Arrows — НЕ делаем +- [x] ColorPlus ≈ наш PostFX — PostFX восстановлен и починен (дефолтный пресет был near-identity → добавлен Cinematic + тесты) +- [x] Правило папок: максимум 4 файла (было 3) + +### Перенос (список пользователя) +- [ ] **StreamerMode** (misc, ~80 у них) — при записи/стриме скрывает: ники игроков, + имена друзей, названия серверов («Анархия-N») в чате/таб-листе/скорборде; скрытие скинов. + Интеграция: наши BetterChat + CustomTabList + Scoreboard-виджет + рендер скинов. +- [x] **LockSlot** (player, ~36) — запрет выброса из выбранных слотов 1–9 (анти-случайный Q), + опция «только в PVP», предупреждение в чат. Точка: перехват drop-пакета. +- [x] **AutoRespawn** (player, ~28) — авто-возрождение после смерти (tick на death screen). +- [ ] **AutoLeave** (player, ~61) — выход в хаб по триггерам: малое ХП, игроки рядом (N, радиус), задержка. +- [ ] **AutoAccept** (player, ~67) — авто-принятие запросов: клан/ТП; опция «только от друзей» + через наш PlayerRelations. Точка: перехват chat-пакетов. +- [ ] **SoundReducer** (player, ~42) — приглушение выбранных звуков/категорий (громкость 0..N), + в отличие от нашего NoSound (он только вкл/выкл всего). Вопрос: новый модуль или развитие NoSound. +- [ ] **Wasted** (misc, ~100) — ОТМЕНЕНО (решение пользователя) +- [ ] **ThirdPerson** (player, ~77) — свободный обзор от 3-го лица без поворота движения + (по нажатию/зажатию бинда). Не дублирует Freecam (он про отдельную камеру-сущность). + Точка: наш CameraMixin. +- [ ] **UseTracker** (player, ~86) — уведомления об использованиях: тотем, зелья, предметы. + Вывод через наш HudNotifier. Точка: пакеты + инвентарь. +- [x] **NoServerPack** (misc) — у Delta ПУСТЫШКА (пустой класс). Сделаем по-настоящему: + авто-отклонение принудительного серверного ресурспака (спрашивать/отклонять всегда). + Точка: наш ClientCommonPacketListenerImplMixin. +- [x] **NoFriendDamage** (combat, ~19) — отмена атаки, если цель — друг (PlayerRelations). + Точка: перехват атаки (AttackEvent у них; у нас миксин на attack/interact-пакет). +- [x] **TapeMouse** (combat, ~66) — автоклик по таймеру. ⚠️ ПРОТИВОРЕЧИТ легит-политике Фазы 3.5 + (тот же класс, что удалённые AutoAttack/TriggerBot). По умолчанию НЕ делаем / делаем + легит-вариант «напоминание о клике» без автоматического клика — РЕШИТЬ. + +### Кандидаты из отчёта (не в списке пользователя — обсудить) +- [x] **DeathCoords** (~27) — координаты последней смерти в чат. Дёшево, полезно. +- [ ] **ItemScroller** — перекладывание предметов прокруткой в инвентарях. Крупный QoL (~200+). +- [ ] **ColorPlus** — ⚠️ это по сути наш удалённый PostFX (экспозиция/гамма/контраст/насыщенность/ + виньетка/резкость/LUT). Восстанавливать под новым именем или забыть — РЕШИТЬ. +- [ ] **Arrows / off-screen индикаторы** — дополнение к Pointers (у нас их нет вообще). + +### Проверка после каждого модуля: `./gradlew build` ✓, тесты на чистую логику ✓ + +## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН +> Правило ≤200 уже есть (Фаза 1–3), но долг: ~240 файлов >200 строк. +> У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) / +> inject(accessors, invokers, mixin). Наша пакетная структура сопоставима — проблема в размере файлов. + +- [ ] Топ-гигантов к разбивке (после Фазы 3): LoVisualRenderPipelines 1527, + DraggableHudElementRegistry 1449, Predictions 1363, DynamicIsland 1254, Module 1181, + HudNotifier 1116, AltManager 1115, Triangulator 1102, TargetESP 1061, UiPrimitiveRenderer 1042 +- [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤200, 0 изменений вызывающих, + `./gradlew build` после каждого; по мере касания файлов в Фазе 7 — разбивать на ходу +- [ ] Facades >200 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам) +- [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша + ### Проверка: `./gradlew test` зелёный diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java index 9274a83..752ba72 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java @@ -195,7 +195,8 @@ public abstract class DraggableHudElement extends BaseHudElement { ); } - void markRendered(long order) { + /** public: вызывается хелперами реестра в подпакетах registry/render (JLS: package-private недоступен из подпакета). */ + public void markRendered(long order) { this.renderOrder = order; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java index 9c52001..f38d689 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java @@ -7,98 +7,44 @@ package dev.loki.lovisual.features.gui.hud.draggable; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.features.gui.hud.*; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore; +import dev.loki.lovisual.features.gui.hud.draggable.render.RegistryRenderPipeline; +import dev.loki.lovisual.features.gui.hud.draggable.render.RegistryWidgetRenderPath; import dev.loki.lovisual.features.module.HudPhase; -import dev.loki.lovisual.render.engine.profiler.ProfilerPhase; import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.ChatScreen; -import org.lwjgl.glfw.GLFW; -import dev.loki.lovisual.features.gui.chat.BetterChatRenderer; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D; -import dev.loki.lovisual.render.engine.text.FontInfo; -import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.TextRenderer; -import dev.loki.lovisual.Lifecycle; -import dev.loki.lovisual.render.helpers.SystemCursor; -import dev.loki.lovisual.util.input.KeyManager; -import dev.loki.lovisual.util.logging.DebugLog; -import java.util.*; +import java.util.List; /** * Реестр HUD-виджетов + drag-and-drop при открытом чате. + *

+ * Публичный фасад: логика делегирована хелперам + * ({@code registry/} — состояние, поиск, якоря и snap; + * {@code interaction/} — drag-and-drop; {@code render/} — пайплайн рендера и оверлей). */ public enum DraggableHudElementRegistry { ; - private static final List WIDGETS = new ArrayList<>(); - private static final float SNAP_DIST = 8f; - private static final float PARENT_SNAP_DIST = 10f; - private static final float PARENT_ALIGN_SNAP_DIST = 5f; - private static final float PARENT_GAP = 6f; - private static final int SNAP_PREVIEW_LINE = 0xFFFFFFFF; - private static final float DRAG_COORD_TEXT_SCALE = 0.85f; - private static final float DRAG_COORD_MARGIN = 4f; - private static final float LINK_WIRE_THICKNESS = 1.05f; - private static final float LINK_PREVIEW_THICKNESS = 1.55f; - private static DraggableHudElement dragging; - private static float dragOffsetX; - private static float dragOffsetY; - private static boolean draggingWithLeft; - private static boolean prevLeft; - private static boolean prevMiddle; - private static long renderOrderSeq; - private static boolean forceVisible; - private static boolean snapPreviewActive; - private static boolean snapPreviewHasX; - private static boolean snapPreviewHasY; - private static float snapPreviewLineX; - private static float snapPreviewLineY; - private static String editorWidgetId; - private static boolean hudCursorActive; - private static int lastScreenW = -1; - private static int lastScreenH = -1; - private static Themes.Theme cachedLinkTheme; - private static LinkWireColors cachedLinkColors; - public static void register(DraggableHudElement widget) { - if (!WIDGETS.contains(widget)) { - widget.postInit(); - WIDGETS.add(widget); - DebugLog.config("[HUD] Registered widget: %s", widget.getId()); - } + RegistryWidgetStore.register(widget); } public static List getWidgets() { - HudElements.ensureLoaded(); - return Collections.unmodifiableList(WIDGETS); + return RegistryWidgetStore.getWidgets(); } public static DraggableHudElement getById(String id) { - HudElements.ensureLoaded(); - return findById(id); + return RegistryWidgetStore.getById(id); } public static boolean isEnabled(String id) { - if (!Lifecycle.canRunHud()) return false; - DraggableHudElement w = findById(id); - return w != null && w.isEnabled(); + return RegistryWidgetStore.isEnabled(id); } public static T get(Class type) { - if (type == null) return null; - for (DraggableHudElement w : WIDGETS) { - if (type.isInstance(w)) { - return type.cast(w); - } - } - return null; + return RegistryWidgetStore.get(type); } public static T widget(Class type) { @@ -106,49 +52,31 @@ public enum DraggableHudElementRegistry { } public static boolean isEnabled(Class type) { - if (!Lifecycle.canRunHud()) return false; - DraggableHudElement w = get(type); - return w != null && w.isEnabled(); + return RegistryWidgetStore.isEnabled(type); } public static void tickAll() { - for (DraggableHudElement w : WIDGETS) { - w.onTick(); - } + RegistryWidgetStore.tickAll(); } public static boolean isForceVisible() { - return forceVisible; + return RegistryState.forceVisible; } public static void setForceVisible(boolean value) { - forceVisible = value; + RegistryState.forceVisible = value; } public static void setEditorWidgetId(String id) { - editorWidgetId = id; + RegistryState.editorWidgetId = id; } public static boolean hasEngineWork(HudPhase phase, boolean foreground) { - if (!Lifecycle.canRunHud()) return false; - if (foreground && dragging != null) return true; - - for (DraggableHudElement w : WIDGETS) { - if (isRenderableCandidate(w, phase)) return true; - } - return false; + return RegistryRenderPipeline.hasEngineWork(phase, foreground); } public static boolean hasEngineWidgetWork(String id) { - if (!Lifecycle.canRunHud()) return false; - if (id == null || id.isBlank()) return false; - if (editorWidgetId != null && !editorWidgetId.equals(id)) return false; - - DraggableHudElement w = findById(id); - if (w == null) return false; - if (!w.usesEngineRenderer()) return false; - boolean bypassEnabled = forceVisible && editorWidgetId != null && editorWidgetId.equals(id); - return bypassEnabled || w.isEnabled(); + return RegistryWidgetRenderPath.hasEngineWidgetWork(id); } public static void renderAllEngine(HudPhase phase, @@ -156,67 +84,7 @@ public enum DraggableHudElementRegistry { TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { - if (!Lifecycle.canRunHud()) { - cancelDragging(); - return; - } - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getWindow() == null) return; - boolean allowOnlyModuleList = false; - - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); - int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); - handleScreenResize(screenW, screenH); - - List renderList = new ArrayList<>(); - for (DraggableHudElement w : WIDGETS) { - boolean singleEditorPreview = editorWidgetId != null; - if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) { - continue; - } - boolean bypassEnabled = singleEditorPreview && forceVisible; - if (!bypassEnabled && !w.isEnabled()) { - continue; - } - if (!w.usesEngineRenderer()) { - continue; - } - if (w.getHudPhase() != phase) { - continue; - } - if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) { - continue; - } - ensurePositionInitialized(w, screenW, screenH); - if (allowOnlyModuleList && !"module_list".equals(w.getId())) { - continue; - } - renderList.add(w); - } - - for (DraggableHudElement w : renderList) { - if (w == dragging) continue; - if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) { - applyScreenAnchor(w, screenW, screenH); - } - } - for (DraggableHudElement w : renderList) { - if (w == dragging) continue; - if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) { - applyAnchors(w, screenW, screenH, new HashSet<>()); - } - } - - for (DraggableHudElement w : renderList) { - noteRendered(w); - try (ProfilerPhase.Scope widgetScope = - ProfilerPhase.scope("2d:widget:" + w.getId()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) { - w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH); - } - } + RegistryRenderPipeline.renderAllEngine(phase, renderer, textRenderer, ctx, tickDelta); } public static void renderAllEngineForeground(HudPhase phase, @@ -224,54 +92,7 @@ public enum DraggableHudElementRegistry { TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { - if (!Lifecycle.canRunHud()) { - cancelDragging(); - return; - } - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getWindow() == null) return; - boolean allowOnlyModuleList = false; - - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); - int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); - handleScreenResize(screenW, screenH); - - for (DraggableHudElement w : WIDGETS) { - boolean singleEditorPreview = editorWidgetId != null; - if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) { - continue; - } - boolean bypassEnabled = singleEditorPreview && forceVisible; - if (!bypassEnabled && !w.isEnabled()) { - continue; - } - if (!w.usesEngineRenderer()) { - continue; - } - if (w.getHudPhase() != phase) { - continue; - } - if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) { - continue; - } - if (allowOnlyModuleList && !"module_list".equals(w.getId())) { - continue; - } - try (ProfilerPhase.Scope widgetScope = - ProfilerPhase.scope("2d:widget_fg:" + w.getId()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) { - w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH); - } - } - - renderDragOverlay(renderer, textRenderer, screenW, screenH); - BetterChatRenderer.renderCapturedTooltip(ctx); - if (phase == HudPhase.AFTER_SUBTITLES) { - float uiScale = HudScale.scale(fbw, fbh); - handleDragging(mc, screenW, screenH, uiScale); - } + RegistryRenderPipeline.renderAllEngineForeground(phase, renderer, textRenderer, ctx, tickDelta); } public static void renderEngineWidget(String id, @@ -279,1171 +100,6 @@ public enum DraggableHudElementRegistry { TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { - if (!Lifecycle.canRunHud()) { - cancelDragging(); - return; - } - if (editorWidgetId != null && (!editorWidgetId.equals(id))) return; - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getWindow() == null) return; - boolean isChat = ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; - boolean isInventory = ClientScreen.current() instanceof net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; - boolean allowOnlyModuleList = isInventory && ClientScreen.current() != null; - if (forceVisible) { - allowOnlyModuleList = false; - } - boolean shouldHideHud = !forceVisible && ClientScreen.current() != null && !isChat && !allowOnlyModuleList; - if (shouldHideHud) { - return; - } - - DraggableHudElement w = findById(id); - if (w == null) return; - if (!w.usesEngineRenderer()) return; - boolean bypassEnabled = forceVisible && editorWidgetId != null && editorWidgetId.equals(id); - if (!bypassEnabled && !w.isEnabled()) { - return; - } - if (allowOnlyModuleList && !"module_list".equals(w.getId())) { - return; - } - - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); - int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); - handleScreenResize(screenW, screenH); - - ensurePositionInitialized(w, screenW, screenH); - if (w != dragging) { - if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) { - applyScreenAnchor(w, screenW, screenH); - } else { - applyAnchors(w, screenW, screenH, new HashSet<>()); - } - } - - noteRendered(w); - try (ProfilerPhase.Scope widgetScope = - ProfilerPhase.scope("2d:widget:" + w.getId()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) { - w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH); - } - try (ProfilerPhase.Scope widgetScope = - ProfilerPhase.scope("2d:widget_fg:" + w.getId()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) { - w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH); - } - } - - private static void ensurePositionInitialized(DraggableHudElement w, int screenW, int screenH) { - if (w.isPositionInitialized()) return; - DraggableLayoutValue.State saved = w.layoutValue().get(); - if (saved != null) { - DraggableLayoutValue.State resolved = saved; - if (!saved.logical()) { - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.getWindow() != null) { - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - float uiScale = HudScale.scale(fbw, fbh); - float nx = HudScale.toVirtual(saved.x(), uiScale); - float ny = HudScale.toVirtual(saved.y(), uiScale); - float npx = HudScale.toVirtual(saved.parentOffsetX(), uiScale); - float npy = HudScale.toVirtual(saved.parentOffsetY(), uiScale); - resolved = new DraggableLayoutValue.State( - nx, - ny, - saved.anchorX(), - saved.anchorY(), - saved.parentId(), - saved.parentSide(), - npx, - npy, - true - ); - w.layoutValue().set(resolved); - w.saveConfig(); - } - } - float safeW = w.getWidth() > 0f ? w.getWidth() : 20f; - float safeH = w.getHeight() > 0f ? w.getHeight() : 20f; - float clampedX = saved.anchorX() == HudAnchorX.FREE - ? Math.max(0, Math.min(resolved.x(), screenW - safeW)) - : resolved.x(); - float clampedY = saved.anchorY() == HudAnchorY.FREE - ? Math.max(0, Math.min(resolved.y(), screenH - safeH)) - : resolved.y(); - if (saved.anchorX() == HudAnchorX.FREE && saved.anchorY() == HudAnchorY.FREE - && (clampedX != resolved.x() || clampedY != resolved.y())) { - DebugLog.config("[HUD] Saved position clamped for %s: (%.1f, %.1f) -> (%.1f, %.1f)", - w.getId(), resolved.x(), resolved.y(), clampedX, clampedY); - } else { - DebugLog.config("[HUD] Loaded saved position for %s: (%.1f, %.1f)", w.getId(), resolved.x(), resolved.y()); - } - w.moveTo(clampedX, clampedY); - w.setAnchors(resolved.anchorX(), resolved.anchorY(), clampedX, clampedY); - if (resolved.parentId() != null && resolved.parentSide() != WidgetAnchorSide.NONE) { - w.setParentAnchor(resolved.parentId(), resolved.parentSide(), resolved.parentOffsetX(), resolved.parentOffsetY()); - } else { - w.clearParentAnchor(); - } - } else { - w.applyDefaultPosition(screenW, screenH); - w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); - w.clearParentAnchor(); - DebugLog.config("[HUD] Applied default position for %s: (%.1f, %.1f)", w.getId(), w.getX(), w.getY()); - } - w.markPositionInitialized(); - } - - private static void handleDragging(Minecraft mc, int screenW, int screenH, float uiScale) { - boolean chatOpen = ClientScreen.current() instanceof ChatScreen; - boolean dragEditorOpen = forceVisible && ClientScreen.current() != null; - boolean allowDragging = chatOpen || dragEditorOpen; - float fbX = (float) mc.mouseHandler.xpos(); - float fbY = (float) mc.mouseHandler.ypos(); - float mx = HudScale.toVirtual(fbX, uiScale); - float my = HudScale.toVirtual(fbY, uiScale); - - boolean left = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_1) == GLFW.GLFW_PRESS; - boolean middle = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_MIDDLE) == GLFW.GLFW_PRESS; - - if (!allowDragging) { - if (dragging != null) { - //DebugLog.config("[HUD] Drag cancelled outside chat for %s", dragging.getId()); - } - if (left && !prevLeft) { - // DebugLog.config("[HUD] Mouse down ignored (chat not open) at (%.1f, %.1f)", mx, my); - } - if (!left && prevLeft) { - // DebugLog.config("[HUD] Mouse up ignored (chat not open) at (%.1f, %.1f)", mx, my); - } - dragging = null; - draggingWithLeft = false; - clearSnapPreview(); - if (hudCursorActive) { - SystemCursor.reset(); - hudCursorActive = false; - } - prevLeft = left; - prevMiddle = middle; - return; - } - - if (dragging != null && !isWidgetVisibleForEditor(dragging)) { - dragging = null; - draggingWithLeft = false; - clearSnapPreview(); - } - - if (left && !prevLeft) { - //DebugLog.config("[HUD] Mouse down in chat at (%.1f, %.1f)", mx, my); - for (DraggableHudElement w : WIDGETS) { - /*DebugLog.config("[HUD] Widget %s bounds x=%.1f y=%.1f w=%.1f h=%.1f contains=%s", - w.getId(), w.getX(), w.getY(), w.getWidth(), w.getHeight(), w.contains(mx, my));*/ - } - DraggableHudElement clickTarget = null; - long bestClickOrder = Long.MIN_VALUE; - for (DraggableHudElement w : WIDGETS) { - if (!isWidgetVisibleForEditor(w)) continue; - if (!w.contains(mx, my)) continue; - if (!w.isMouseOverInteractive(mx, my)) continue; - long order = w.getRuntimeRenderOrder(); - if (order > bestClickOrder) { - bestClickOrder = order; - clickTarget = w; - } - } - if (clickTarget != null) { - if (clickTarget.onMouseClicked(mx, my, GLFW.GLFW_MOUSE_BUTTON_1)) { - dragging = null; - draggingWithLeft = false; - clearSnapPreview(); - prevLeft = left; - prevMiddle = middle; - return; - } - // interactive hitbox: do not start drag on left click - prevLeft = left; - prevMiddle = middle; - return; - } else { - // start drag with left click if no interactive hitbox - DraggableHudElement candidate = null; - long bestOrder = Long.MIN_VALUE; - for (DraggableHudElement w : WIDGETS) { - if (!isWidgetVisibleForEditor(w)) continue; - if (!w.isDraggable() || !w.contains(mx, my)) continue; - long order = w.getRuntimeRenderOrder(); - if (order > bestOrder) { - bestOrder = order; - candidate = w; - } - } - if (candidate == null) { - for (int i = WIDGETS.size() - 1; i >= 0; i--) { - DraggableHudElement w = WIDGETS.get(i); - if (!isWidgetVisibleForEditor(w)) continue; - if (w.contains(mx, my) && w.isDraggable()) { - candidate = w; - break; - } - } - } - if (candidate != null) { - dragging = candidate; - draggingWithLeft = true; - dragOffsetX = mx - candidate.getX(); - dragOffsetY = my - candidate.getY(); - if (isWidgetAnchorModifierDown()) { - detachChildren(candidate); - } - dragging.clearParentAnchor(); - dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, dragging.getX(), dragging.getY()); - SystemCursor.set(SystemCursor.CursorType.MOVE); - hudCursorActive = true; - } - } - } - - if (middle && !prevMiddle) { - // start drag - DraggableHudElement candidate = null; - long bestOrder = Long.MIN_VALUE; - for (DraggableHudElement w : WIDGETS) { - if (!isWidgetVisibleForEditor(w)) continue; - if (!w.isDraggable() || !w.contains(mx, my)) continue; - long order = w.getRuntimeRenderOrder(); - if (order > bestOrder) { - bestOrder = order; - candidate = w; - } - } - if (candidate == null) { - for (int i = WIDGETS.size() - 1; i >= 0; i--) { - DraggableHudElement w = WIDGETS.get(i); - if (!isWidgetVisibleForEditor(w)) continue; - if (w.contains(mx, my) && w.isDraggable()) { - candidate = w; - break; - } - } - } - if (candidate != null) { - dragging = candidate; - draggingWithLeft = false; - dragOffsetX = mx - candidate.getX(); - dragOffsetY = my - candidate.getY(); - if (isWidgetAnchorModifierDown()) { - detachChildren(candidate); - } - dragging.clearParentAnchor(); - dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, dragging.getX(), dragging.getY()); - //DebugLog.config("[HUD] Drag start for %s at (%.1f, %.1f)", dragging.getId(), dragging.getX(), dragging.getY()); - SystemCursor.set(SystemCursor.CursorType.MOVE); - hudCursorActive = true; - } - if (dragging == null) { - //DebugLog.config("[HUD] No widget found under cursor at (%.1f, %.1f); widgets=%d", mx, my, WIDGETS.size()); - } - } else if (!middle && prevMiddle && dragging != null && !draggingWithLeft) { - stopDragging(screenW, screenH); - } else if (!middle && prevMiddle) { - //DebugLog.config("[HUD] Mouse up with no active drag at (%.1f, %.1f)", mx, my); - } - - if (draggingWithLeft && !left && prevLeft && dragging != null) { - stopDragging(screenW, screenH); - } - - if (((draggingWithLeft && left) || (!draggingWithLeft && middle)) && dragging != null) { - moveDraggedWidget(dragging, mx, my, screenW, screenH); - if (!isWidgetAnchorModifierDown()) { - updateSnapPreview(dragging, screenW, screenH); - } else { - clearSnapPreview(); - } - //DebugLog.config("[HUD] Dragging %s -> (%.1f, %.1f)", dragging.getId(), dragging.getX(), dragging.getY()); - SystemCursor.set(SystemCursor.CursorType.MOVE); - hudCursorActive = true; - } else if (dragging == null) { - clearSnapPreview(); - requestHoverCursor(mx, my); - } - - prevLeft = left; - prevMiddle = middle; - } - - private static void stopDragging(int screenW, int screenH) { - if (dragging == null) return; - if (!isWidgetAnchorModifierDown()) { - applyStickySnap(dragging, screenW, screenH); - } - dragging.clearParentAnchor(); - Minecraft mc = Minecraft.getInstance(); - boolean allowWidgetAnchoring = mc == null || !isWidgetAnchorModifierDown(); - updateDragAnchors(dragging, screenW, screenH, allowWidgetAnchoring); - dragging.layoutValue().set(dragging.snapshotLayout()); - dragging.saveConfig(); - dragging = null; - draggingWithLeft = false; - clearSnapPreview(); - if (hudCursorActive) { - SystemCursor.reset(); - hudCursorActive = false; - } - } - - private static void cancelDragging() { - dragging = null; - draggingWithLeft = false; - clearSnapPreview(); - if (hudCursorActive) { - SystemCursor.reset(); - hudCursorActive = false; - } - prevLeft = false; - prevMiddle = false; - } - - private static void requestHoverCursor(float mx, float my) { - DraggableHudElement interactive = findTopVisibleWidget(mx, my, true); - if (interactive != null) { - SystemCursor.set(SystemCursor.CursorType.HAND); - hudCursorActive = true; - return; - } - - DraggableHudElement draggable = findTopVisibleWidget(mx, my, false); - if (draggable != null) { - SystemCursor.set(SystemCursor.CursorType.MOVE); - hudCursorActive = true; - } - } - - private static DraggableHudElement findTopVisibleWidget(float mx, float my, boolean interactiveOnly) { - DraggableHudElement best = null; - long bestOrder = Long.MIN_VALUE; - for (DraggableHudElement w : WIDGETS) { - if (!isWidgetVisibleForEditor(w)) continue; - if (!w.contains(mx, my)) continue; - - if (interactiveOnly) { - if (!w.isMouseOverInteractive(mx, my)) continue; - } else if (!w.isDraggable()) { - continue; - } - - long order = w.getRuntimeRenderOrder(); - if (order > bestOrder) { - bestOrder = order; - best = w; - } - } - - if (best != null) return best; - for (int i = WIDGETS.size() - 1; i >= 0; i--) { - DraggableHudElement w = WIDGETS.get(i); - if (!isWidgetVisibleForEditor(w)) continue; - if (!w.contains(mx, my)) continue; - - if (interactiveOnly) { - if (!w.isMouseOverInteractive(mx, my)) continue; - } else if (!w.isDraggable()) { - continue; - } - - return w; - } - - return null; - } - - private static boolean isWidgetVisibleForEditor(DraggableHudElement w) { - if (w == null) return false; - - boolean singleEditorPreview = editorWidgetId != null; - if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) { - return false; - } - - boolean bypassEnabled = singleEditorPreview && forceVisible; - if (!bypassEnabled && !w.isEnabled()) { - return false; - } - - if (!w.usesEngineRenderer()) return false; - if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) return false; - return w.getRuntimeRenderOrder() > 0L; - } - - private static boolean isRenderableCandidate(DraggableHudElement w, HudPhase phase) { - if (w == null) return false; - - boolean singleEditorPreview = editorWidgetId != null; - if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) { - return false; - } - - boolean bypassEnabled = singleEditorPreview && forceVisible; - if (!bypassEnabled && !w.isEnabled()) { - return false; - } - - if (!w.usesEngineRenderer()) return false; - if (w.getHudPhase() != phase) return false; - return w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL; - } - - private static boolean isWidgetVisibleForLinkOverlay(DraggableHudElement w) { - if (w == null) return false; - - boolean singleEditorPreview = editorWidgetId != null; - if (singleEditorPreview && (editorWidgetId.isEmpty() || !editorWidgetId.equals(w.getId()))) { - return false; - } - - boolean bypassEnabled = singleEditorPreview && forceVisible; - if (!bypassEnabled && !w.isEnabled()) { - return false; - } - - return w.usesEngineRenderer() && w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL; - } - - private static void applyAnchors(DraggableHudElement w, int screenW, int screenH) { - applyAnchors(w, screenW, screenH, new HashSet<>()); - } - - private static void applyAnchors(DraggableHudElement w, int screenW, int screenH, Set visiting) { - if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) { - applyScreenAnchor(w, screenW, screenH); - return; - } - DraggableHudElement parent = findById(w.getParentId()); - if (parent == null) { - w.clearParentAnchor(); - applyScreenAnchor(w, screenW, screenH); - return; - } - if (!visiting.add(w.getId())) { - w.clearParentAnchor(); - applyScreenAnchor(w, screenW, screenH); - return; - } - if (parent != dragging) { - applyAnchors(parent, screenW, screenH, visiting); - } - if (isDescendantOf(parent, w)) { - w.clearParentAnchor(); - applyScreenAnchor(w, screenW, screenH); - } else { - applyWidgetAnchor(w, parent, screenW, screenH); - } - visiting.remove(w.getId()); - } - - private static void applyScreenAnchor(DraggableHudElement w, int screenW, int screenH) { - float x = w.getAnchorOffsetX(); - float y = w.getAnchorOffsetY(); - switch (w.getAnchorX()) { - case LEFT -> x = w.getAnchorOffsetX(); - case CENTER -> x = screenW * 0.5f - w.getWidth() * 0.5f + w.getAnchorOffsetX(); - case RIGHT -> x = screenW - w.getWidth() - w.getAnchorOffsetX(); - case FREE -> x = w.getAnchorOffsetX(); - } - switch (w.getAnchorY()) { - case TOP -> y = w.getAnchorOffsetY(); - case CENTER -> y = screenH * 0.5f - w.getHeight() * 0.5f + w.getAnchorOffsetY(); - case BOTTOM -> y = screenH - w.getHeight() - w.getAnchorOffsetY(); - case FREE -> y = w.getAnchorOffsetY(); - } - w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH)); - } - - private static void applyWidgetAnchor(DraggableHudElement w, DraggableHudElement parent, int screenW, int screenH) { - float x = w.getX(); - float y = w.getY(); - switch (w.getParentSide()) { - case TOP -> { - x = parent.getX() + w.getParentOffsetX(); - y = parent.getY() - w.getHeight() - PARENT_GAP + w.getParentOffsetY(); - } - case BOTTOM -> { - x = parent.getX() + w.getParentOffsetX(); - y = parent.getY() + parent.getHeight() + PARENT_GAP + w.getParentOffsetY(); - } - case LEFT -> { - x = parent.getX() - w.getWidth() - PARENT_GAP + w.getParentOffsetX(); - y = parent.getY() + w.getParentOffsetY(); - } - case RIGHT -> { - x = parent.getX() + parent.getWidth() + PARENT_GAP + w.getParentOffsetX(); - y = parent.getY() + w.getParentOffsetY(); - } - case NONE -> { - } - } - w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH)); - } - - private static void updateDragAnchors(DraggableHudElement w, int screenW, int screenH, boolean allowWidgetAnchoring) { - if (w.getWidth() <= 0 || w.getHeight() <= 0) { - w.clearParentAnchor(); - w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); - return; - } - - if (!allowWidgetAnchoring) { - w.clearParentAnchor(); - w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); - return; - } - - if (w.supportsWidgetAnchoring()) { - WidgetAnchorCandidate candidate = findWidgetAnchorCandidate(w, screenW, screenH); - if (candidate != null) { - w.setParentAnchor(candidate.parent.getId(), candidate.side, candidate.offsetX, candidate.offsetY); - return; - } - } - w.clearParentAnchor(); - - float x = w.getX(); - float y = w.getY(); - - HudAnchorX ax = HudAnchorX.FREE; - float offX = x; - float leftDist = Math.abs(x); - float rightDist = Math.abs((x + w.getWidth()) - screenW); - float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f); - if (leftDist <= SNAP_DIST) { - ax = HudAnchorX.LEFT; - offX = x; - } else if (rightDist <= SNAP_DIST) { - ax = HudAnchorX.RIGHT; - offX = rightDist; - } else if (centerDistX <= SNAP_DIST) { - ax = HudAnchorX.CENTER; - offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f; - } - - HudAnchorY ay = HudAnchorY.FREE; - float offY = y; - float topDist = Math.abs(y); - float bottomDist = Math.abs((y + w.getHeight()) - screenH); - float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f); - if (topDist <= SNAP_DIST) { - ay = HudAnchorY.TOP; - offY = y; - } else if (bottomDist <= SNAP_DIST) { - ay = HudAnchorY.BOTTOM; - offY = bottomDist; - } else if (centerDistY <= SNAP_DIST) { - ay = HudAnchorY.CENTER; - offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f; - } - - w.setAnchors(ax, ay, offX, offY); - } - - private static void handleScreenResize(int screenW, int screenH) { - if (screenW <= 0 || screenH <= 0) return; - if (lastScreenW <= 0 || lastScreenH <= 0) { - lastScreenW = screenW; - lastScreenH = screenH; - return; - } - if (lastScreenW == screenW && lastScreenH == screenH) return; - - int oldW = lastScreenW; - int oldH = lastScreenH; - lastScreenW = screenW; - lastScreenH = screenH; - - for (DraggableHudElement w : WIDGETS) { - if (!w.isPositionInitialized()) continue; - if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) { - // Parent-anchored widgets will be handled when anchors are applied. - continue; - } - if (w.getWidth() <= 0 || w.getHeight() <= 0) continue; - - boolean changed = false; - if (w.getAnchorX() == HudAnchorX.FREE && w.getAnchorY() == HudAnchorY.FREE) { - changed |= inferScreenAnchors(w, oldW, oldH); - } - - applyScreenAnchor(w, screenW, screenH); - changed |= clampToScreenIfFree(w, screenW, screenH); - - if (changed) { - w.layoutValue().set(w.snapshotLayout()); - w.saveConfig(); - } - } - } - - private static boolean inferScreenAnchors(DraggableHudElement w, int screenW, int screenH) { - float x = w.getX(); - float y = w.getY(); - boolean changed = false; - - HudAnchorX ax = w.getAnchorX(); - float offX = w.getAnchorOffsetX(); - float leftDist = Math.abs(x); - float rightDist = Math.abs((x + w.getWidth()) - screenW); - float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f); - if (leftDist <= SNAP_DIST) { - ax = HudAnchorX.LEFT; - offX = x; - } else if (rightDist <= SNAP_DIST) { - ax = HudAnchorX.RIGHT; - offX = rightDist; - } else if (centerDistX <= SNAP_DIST) { - ax = HudAnchorX.CENTER; - offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f; - } - - HudAnchorY ay = w.getAnchorY(); - float offY = w.getAnchorOffsetY(); - float topDist = Math.abs(y); - float bottomDist = Math.abs((y + w.getHeight()) - screenH); - float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f); - if (topDist <= SNAP_DIST) { - ay = HudAnchorY.TOP; - offY = y; - } else if (bottomDist <= SNAP_DIST) { - ay = HudAnchorY.BOTTOM; - offY = bottomDist; - } else if (centerDistY <= SNAP_DIST) { - ay = HudAnchorY.CENTER; - offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f; - } - - if (ax != w.getAnchorX() || ay != w.getAnchorY() - || offX != w.getAnchorOffsetX() || offY != w.getAnchorOffsetY()) { - w.setAnchors(ax, ay, offX, offY); - changed = true; - } - return changed; - } - - private static boolean clampToScreenIfFree(DraggableHudElement w, int screenW, int screenH) { - boolean changed = false; - float newX = w.getX(); - float newY = w.getY(); - if (w.getAnchorX() == HudAnchorX.FREE) { - float maxX = Math.max(0f, screenW - w.getWidth()); - newX = Math.max(0f, Math.min(newX, maxX)); - } - if (w.getAnchorY() == HudAnchorY.FREE) { - float maxY = Math.max(0f, screenH - w.getHeight()); - newY = Math.max(0f, Math.min(newY, maxY)); - } - if (newX != w.getX() || newY != w.getY()) { - w.moveTo(newX, newY); - if (w.getAnchorX() == HudAnchorX.FREE || w.getAnchorY() == HudAnchorY.FREE) { - w.setAnchors(w.getAnchorX(), w.getAnchorY(), - w.getAnchorX() == HudAnchorX.FREE ? newX : w.getAnchorOffsetX(), - w.getAnchorY() == HudAnchorY.FREE ? newY : w.getAnchorOffsetY()); - } - changed = true; - } - return changed; - } - - private static boolean isWidgetAnchorModifierDown() { - return KeyManager.isComboHeldAllowScreen("LEFT_ALT") - || KeyManager.isComboHeldAllowScreen("RIGHT_ALT"); - } - - private static void detachChildren(DraggableHudElement parent) { - String parentId = parent.getId(); - for (DraggableHudElement w : WIDGETS) { - if (w == parent) continue; - if (parentId.equals(w.getParentId()) && w.getParentSide() != WidgetAnchorSide.NONE) { - w.clearParentAnchor(); - w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); - w.layoutValue().set(w.snapshotLayout()); - w.saveConfig(); - } - } - } - - private static WidgetAnchorCandidate findWidgetAnchorCandidate(DraggableHudElement dragging, int screenW, int screenH) { - float bestDist = Float.MAX_VALUE; - WidgetAnchorCandidate best = null; - for (DraggableHudElement target : WIDGETS) { - if (target == dragging) continue; - if (!isWidgetVisibleForLinkOverlay(target)) continue; - if (isDescendantOf(target, dragging)) continue; - if (target.getWidth() <= 0 || target.getHeight() <= 0) continue; - - float dxOverlap = overlap(dragging.getX(), dragging.getX() + dragging.getWidth(), - target.getX(), target.getX() + target.getWidth()); - float dyOverlap = overlap(dragging.getY(), dragging.getY() + dragging.getHeight(), - target.getY(), target.getY() + target.getHeight()); - - float distTop = Math.abs((dragging.getY() + dragging.getHeight()) - (target.getY() - PARENT_GAP)); - if (dxOverlap > 0 && distTop <= PARENT_SNAP_DIST && distTop < bestDist) { - float offsetX = parentAxisOffset(dragging.getX() - target.getX()); - float desiredY = target.getY() - PARENT_GAP - dragging.getHeight(); - float desiredX = target.getX() + offsetX; - if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; - float offsetY = 0f; - bestDist = distTop; - best = new WidgetAnchorCandidate(target, WidgetAnchorSide.TOP, offsetX, offsetY); - } - - float distBottom = Math.abs(dragging.getY() - (target.getY() + target.getHeight() + PARENT_GAP)); - if (dxOverlap > 0 && distBottom <= PARENT_SNAP_DIST && distBottom < bestDist) { - float offsetX = parentAxisOffset(dragging.getX() - target.getX()); - float desiredY = target.getY() + target.getHeight() + PARENT_GAP; - float desiredX = target.getX() + offsetX; - if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; - float offsetY = 0f; - bestDist = distBottom; - best = new WidgetAnchorCandidate(target, WidgetAnchorSide.BOTTOM, offsetX, offsetY); - } - - float distLeft = Math.abs((dragging.getX() + dragging.getWidth()) - (target.getX() - PARENT_GAP)); - if (dyOverlap > 0 && distLeft <= PARENT_SNAP_DIST && distLeft < bestDist) { - float offsetY = parentAxisOffset(dragging.getY() - target.getY()); - float desiredX = target.getX() - PARENT_GAP - dragging.getWidth(); - float desiredY = target.getY() + offsetY; - if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; - float offsetX = 0f; - bestDist = distLeft; - best = new WidgetAnchorCandidate(target, WidgetAnchorSide.LEFT, offsetX, offsetY); - } - - float distRight = Math.abs(dragging.getX() - (target.getX() + target.getWidth() + PARENT_GAP)); - if (dyOverlap > 0 && distRight <= PARENT_SNAP_DIST && distRight < bestDist) { - float offsetY = parentAxisOffset(dragging.getY() - target.getY()); - float desiredX = target.getX() + target.getWidth() + PARENT_GAP; - float desiredY = target.getY() + offsetY; - if (!fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; - float offsetX = 0f; - bestDist = distRight; - best = new WidgetAnchorCandidate(target, WidgetAnchorSide.RIGHT, offsetX, offsetY); - } - } - return best; - } - - private static float parentAxisOffset(float offset) { - return Math.abs(offset) <= PARENT_ALIGN_SNAP_DIST ? 0f : offset; - } - - private static float overlap(float a0, float a1, float b0, float b1) { - float left = Math.max(a0, b0); - float right = Math.min(a1, b1); - return right - left; - } - - private static DraggableHudElement findById(String id) { - if (id == null) return null; - for (DraggableHudElement w : WIDGETS) { - if (id.equals(w.getId())) return w; - } - return null; - } - - private static void moveDraggedWidget(DraggableHudElement w, float mx, float my, int screenW, int screenH) { - float newX = mx - dragOffsetX; - float newY = my - dragOffsetY; - Bounds groupBounds = collectLinkedBounds(w, screenW, screenH); - float dx = newX - w.getX(); - float dy = newY - w.getY(); - - if (groupBounds != null && groupBounds.width() <= screenW && groupBounds.height() <= screenH) { - dx = clamp(dx, -groupBounds.minX(), screenW - groupBounds.maxX()); - dy = clamp(dy, -groupBounds.minY(), screenH - groupBounds.maxY()); - w.moveTo(w.getX() + dx, w.getY() + dy); - applyDescendantAnchors(w, screenW, screenH, new HashSet<>()); - return; - } - - w.moveTo(clampX(newX, w, screenW), clampY(newY, w, screenH)); - applyDescendantAnchors(w, screenW, screenH, new HashSet<>()); - } - - private static void updateSnapPreview(DraggableHudElement w, int screenW, int screenH) { - if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) { - clearSnapPreview(); - return; - } - - StickySnap snap = findStickySnap(w, screenW, screenH); - if (!snap.hasX && !snap.hasY) { - clearSnapPreview(); - return; - } - - snapPreviewActive = true; - snapPreviewHasX = snap.hasX; - snapPreviewHasY = snap.hasY; - snapPreviewLineX = snap.x; - snapPreviewLineY = snap.y; - } - - private static void applyStickySnap(DraggableHudElement w, int screenW, int screenH) { - if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) return; - - StickySnap snap = findStickySnap(w, screenW, screenH); - float newX = w.getX(); - float newY = w.getY(); - if (snap.hasX) newX = snap.x; - if (snap.hasY) newY = snap.y; - w.moveTo(clampX(newX, w, screenW), clampY(newY, w, screenH)); - } - - private static StickySnap findStickySnap(DraggableHudElement dragging, int screenW, int screenH) { - float bestX = 0f; - float bestY = 0f; - float bestXDist = Float.MAX_VALUE; - float bestYDist = Float.MAX_VALUE; - boolean hasX = false; - boolean hasY = false; - - boolean rightSide = dragging.getX() > screenW * 0.5f; - for (DraggableHudElement target : WIDGETS) { - if (target == dragging) continue; - if (!target.isEnabled()) continue; - if (target.getWidth() <= 0 || target.getHeight() <= 0) continue; - - if (rightSide) { - float targetEdge = target.getX() + target.getWidth(); - float dragEdge = dragging.getX() + dragging.getWidth(); - float dist = Math.abs(targetEdge - dragEdge); - if (dist <= PARENT_SNAP_DIST && dist < bestXDist) { - bestXDist = dist; - bestX = targetEdge - dragging.getWidth(); - hasX = true; - } - } else { - float dist = Math.abs(target.getX() - dragging.getX()); - if (dist <= PARENT_SNAP_DIST && dist < bestXDist) { - bestXDist = dist; - bestX = target.getX(); - hasX = true; - } - } - - float distY = Math.abs(target.getY() - dragging.getY()); - if (distY <= PARENT_SNAP_DIST && distY < bestYDist) { - bestYDist = distY; - bestY = target.getY(); - hasY = true; - } - } - - float distTop = Math.abs(dragging.getY()); - if (distTop <= PARENT_SNAP_DIST && distTop < bestYDist) { - bestY = 0f; - hasY = true; - } - - return new StickySnap(hasX, hasY, bestX, bestY); - } - - private static void clearSnapPreview() { - snapPreviewActive = false; - snapPreviewHasX = false; - snapPreviewHasY = false; - } - - private static void renderDragOverlay(Renderer2D renderer, - TextRenderer fallbackTextRenderer, - int screenW, - int screenH) { - renderLinkWires(renderer, screenW, screenH); - - if (snapPreviewActive) { - if (snapPreviewHasX) { - renderer.line(snapPreviewLineX, 0f, snapPreviewLineX, screenH, SNAP_PREVIEW_LINE); - } - if (snapPreviewHasY) { - renderer.line(0f, snapPreviewLineY, screenW, snapPreviewLineY, SNAP_PREVIEW_LINE); - } - } - - if (dragging == null) return; - TextRenderer coordRenderer = Fonts.renderer("Comfortaa", FontInfo.Type.Regular, - fallbackTextRenderer != null ? fallbackTextRenderer : TextRenderer.get()); - if (coordRenderer == null) return; - - String coords = "x: " + Math.round(dragging.getX()) + " y: " + Math.round(dragging.getY()); - coordRenderer.begin(DRAG_COORD_TEXT_SCALE, false, false); - try { - float textW = (float) coordRenderer.getWidth(coords, true); - float textH = (float) coordRenderer.getHeight(true); - float textX = dragging.getX(); - float textY = dragging.getY() - textH - DRAG_COORD_MARGIN; - if (textY < DRAG_COORD_MARGIN) { - textY = dragging.getY() + dragging.getHeight() + DRAG_COORD_MARGIN; - } - textX = clamp(textX, DRAG_COORD_MARGIN, Math.max(DRAG_COORD_MARGIN, screenW - textW - DRAG_COORD_MARGIN)); - textY = clamp(textY, DRAG_COORD_MARGIN, Math.max(DRAG_COORD_MARGIN, screenH - textH - DRAG_COORD_MARGIN)); - coordRenderer.render(coords, textX, textY, new RenderColor(0xFFFFFFFF), true); - } finally { - coordRenderer.end(); - } - } - - private static void renderLinkWires(Renderer2D renderer, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getWindow() == null || !(ClientScreen.current() instanceof ChatScreen)) - return; - - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - float uiScale = HudScale.scale(fbw, fbh); - float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale); - float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale); - - LinkWireColors colors = linkWireColors(); - for (DraggableHudElement child : WIDGETS) { - if (!isWidgetVisibleForLinkOverlay(child)) continue; - if (child == dragging) continue; - if (child.getParentId() == null || child.getParentSide() == WidgetAnchorSide.NONE) continue; - - DraggableHudElement parent = findById(child.getParentId()); - if (!isWidgetVisibleForLinkOverlay(parent)) continue; - if (!parent.contains(mx, my) && !child.contains(mx, my)) continue; - renderLinkWire(renderer, parent, child, child.getParentSide(), - LINK_WIRE_THICKNESS, colors.wireStart(), colors.wireEnd()); - } - - if (dragging == null || isWidgetAnchorModifierDown()) return; - WidgetAnchorCandidate candidate = findWidgetAnchorCandidate(dragging, screenW, screenH); - if (candidate == null) return; - renderLinkWire(renderer, candidate.parent(), dragging, candidate.side(), - LINK_PREVIEW_THICKNESS, colors.previewStart(), colors.previewEnd()); - } - - private static LinkWireColors linkWireColors() { - Themes.Theme theme = Theme.theme(); - if (cachedLinkTheme == theme && cachedLinkColors != null) { - return cachedLinkColors; - } - - int wireStart = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme.textMuted(), theme.strokeSoft(), 0.45f), 0x86); - int wireEnd = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme.accentSoft(), theme.windowStroke(), 0.24f), 0x78); - int previewStart = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme.textPrimary(), theme.accentSoft(), 0.38f), 0xC8); - int previewEnd = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme.accent(), theme.accentSoft(), 0.28f), 0xD8); - - cachedLinkTheme = theme; - cachedLinkColors = new LinkWireColors(wireStart, wireEnd, previewStart, previewEnd); - return cachedLinkColors; - } - - private static void renderLinkWire(Renderer2D renderer, - DraggableHudElement parent, - DraggableHudElement child, - WidgetAnchorSide side, - double thickness, - int startArgb, - int endArgb) { - if (parent == null || child == null || side == null || side == WidgetAnchorSide.NONE) return; - if (parent.getWidth() <= 0f || parent.getHeight() <= 0f || child.getWidth() <= 0f || child.getHeight() <= 0f) { - return; - } - - double parentX = 0.0; - double parentY = 0.0; - double childX = 0.0; - double childY = 0.0; - double parentCx = 0.0; - double parentCy = 0.0; - double childCx = 0.0; - double childCy = 0.0; - - switch (side) { - case TOP -> { - parentX = parent.getX() + parent.getWidth() * 0.5; - parentY = parent.getY(); - childX = child.getX() + child.getWidth() * 0.5; - childY = child.getY() + child.getHeight(); - double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45); - parentCx = parentX; - parentCy = parentY - c; - childCx = childX; - childCy = childY + c; - } - case BOTTOM -> { - parentX = parent.getX() + parent.getWidth() * 0.5; - parentY = parent.getY() + parent.getHeight(); - childX = child.getX() + child.getWidth() * 0.5; - childY = child.getY(); - double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45); - parentCx = parentX; - parentCy = parentY + c; - childCx = childX; - childCy = childY - c; - } - case LEFT -> { - parentX = parent.getX(); - parentY = parent.getY() + parent.getHeight() * 0.5; - childX = child.getX() + child.getWidth(); - childY = child.getY() + child.getHeight() * 0.5; - double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45); - parentCx = parentX - c; - parentCy = parentY; - childCx = childX + c; - childCy = childY; - } - case RIGHT -> { - parentX = parent.getX() + parent.getWidth(); - parentY = parent.getY() + parent.getHeight() * 0.5; - childX = child.getX(); - childY = child.getY() + child.getHeight() * 0.5; - double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45); - parentCx = parentX + c; - parentCy = parentY; - childCx = childX - c; - childCy = childY; - } - case NONE -> { - return; - } - } - - renderer.bezierConnectorGradient(childX, childY, childCx, childCy, parentCx, parentCy, parentX, parentY, - 18, thickness, startArgb, endArgb); - } - - private static void applyDragAnchorLock(DraggableHudElement w, int screenW, int screenH) { - if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) { - DraggableHudElement parent = findById(w.getParentId()); - if (parent != null) { - applyWidgetAnchor(w, parent, screenW, screenH); - } - return; - } - if (w.getAnchorX() != HudAnchorX.FREE || w.getAnchorY() != HudAnchorY.FREE) { - applyScreenAnchor(w, screenW, screenH); - } - } - - private static float clampX(float x, DraggableHudElement w, int screenW) { - float maxX = Math.max(0f, screenW - Math.max(0f, w.getWidth())); - return clamp(x, 0f, maxX); - } - - private static float clampY(float y, DraggableHudElement w, int screenH) { - float maxY = Math.max(0f, screenH - Math.max(0f, w.getHeight())); - return clamp(y, 0f, maxY); - } - - private static float clamp(float value, float min, float max) { - if (max < min) return min; - return Math.max(min, Math.min(value, max)); - } - - private static void applyDescendantAnchors(DraggableHudElement parent, int screenW, int screenH, Set visited) { - if (parent == null || !visited.add(parent.getId())) return; - String parentId = parent.getId(); - for (DraggableHudElement child : WIDGETS) { - if (child == parent) continue; - if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue; - applyWidgetAnchor(child, parent, screenW, screenH); - applyDescendantAnchors(child, screenW, screenH, visited); - } - } - - private static Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH) { - if (root == null) return null; - return collectLinkedBounds(root, screenW, screenH, new HashSet<>()); - } - - private static Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH, Set visited) { - if (root == null || !visited.add(root.getId())) return null; - Bounds bounds = Bounds.of(root); - String parentId = root.getId(); - for (DraggableHudElement child : WIDGETS) { - if (child == root) continue; - if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue; - applyWidgetAnchor(child, root, screenW, screenH); - bounds = Bounds.union(bounds, collectLinkedBounds(child, screenW, screenH, visited)); - } - return bounds; - } - - private static boolean fitsLinkedBoundsAt(DraggableHudElement root, - float desiredRootX, - float desiredRootY, - int screenW, - int screenH) { - Bounds bounds = collectLinkedBounds(root, screenW, screenH); - if (bounds == null) return false; - float dx = desiredRootX - root.getX(); - float dy = desiredRootY - root.getY(); - return bounds.minX() + dx >= 0f - && bounds.minY() + dy >= 0f - && bounds.maxX() + dx <= screenW - && bounds.maxY() + dy <= screenH; - } - - private static boolean isDescendantOf(DraggableHudElement possibleDescendant, DraggableHudElement ancestor) { - if (possibleDescendant == null || ancestor == null) return false; - String ancestorId = ancestor.getId(); - Set visited = new HashSet<>(); - DraggableHudElement current = possibleDescendant; - while (current != null && current.getParentId() != null && current.getParentSide() != WidgetAnchorSide.NONE) { - if (!visited.add(current.getId())) return false; - if (ancestorId.equals(current.getParentId())) return true; - current = findById(current.getParentId()); - } - return false; - } - - private static void noteRendered(DraggableHudElement w) { - if (w == null) return; - w.markRendered(++renderOrderSeq); - } - - private record StickySnap(boolean hasX, boolean hasY, float x, float y) { - } - - private record WidgetAnchorCandidate(DraggableHudElement parent, WidgetAnchorSide side, float offsetX, - float offsetY) { - } - - private record LinkWireColors(int wireStart, int wireEnd, int previewStart, int previewEnd) { - } - - private record Bounds(float minX, float minY, float maxX, float maxY) { - static Bounds of(DraggableHudElement w) { - return new Bounds(w.getX(), w.getY(), w.getX() + w.getWidth(), w.getY() + w.getHeight()); - } - - static Bounds union(Bounds a, Bounds b) { - if (a == null) return b; - if (b == null) return a; - return new Bounds( - Math.min(a.minX, b.minX), - Math.min(a.minY, b.minY), - Math.max(a.maxX, b.maxX), - Math.max(a.maxY, b.maxY) - ); - } - - float width() { - return maxX - minX; - } - - float height() { - return maxY - minY; - } + RegistryWidgetRenderPath.renderEngineWidget(id, renderer, textRenderer, ctx, tickDelta); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java index 0f972d1..fef1ae2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java @@ -7,24 +7,27 @@ package dev.loki.lovisual.features.gui.hud.draggable.impl; -import dev.loki.lovisual.features.theme.Theme; import com.mojang.blaze3d.platform.Window; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.resources.Identifier; import dev.loki.lovisual.config.SettingDef; -import dev.loki.lovisual.events.impl.RenderPrewarmCollectEvent; import dev.loki.lovisual.config.values.BooleanValue; import dev.loki.lovisual.config.values.ModeValue; import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.events.impl.RenderPrewarmCollectEvent; import dev.loki.lovisual.features.gui.hud.HudElementRegister; import dev.loki.lovisual.features.gui.hud.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.HudRenderUtil; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastCardRenderer; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.MessengerSide; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastPosition; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.PreviewToast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastDraw; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastVisual; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastStyle; import dev.loki.lovisual.features.module.HudPhase; import dev.loki.lovisual.features.module.Module; import dev.loki.lovisual.features.module.ModuleManager; @@ -33,11 +36,15 @@ import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.SvgRenderOptions; import dev.loki.lovisual.render.engine.text.FontInfo; import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; import dev.loki.lovisual.util.wav.CustomSoundEngine; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.resources.Identifier; import java.util.ArrayList; import java.util.Iterator; @@ -52,43 +59,9 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat private static final float BASE_MARGIN = 9f; private static final float BASE_GAP = 3f; private static final float BASE_BOTTOM_OFFSET = 160f; - - private static final float BASE_RADIUS = 4f; - private static final float BASE_BOX_HEIGHT = 16f; - private static final float BASE_LEFT_PAD = 7f; - private static final float BASE_RIGHT_PAD = 9f; - private static final float BASE_ICON_PIPE_GAP = 5f; - private static final float BASE_TOGGLE_PIPE_GAP = 4.5f; - private static final float BASE_PIPE_TEXT_GAP = 6f; - private static final float BASE_TOGGLE_W = 20f; - private static final float BASE_TOGGLE_H = 12f; - private static final float BASE_PROGRESS_H = 1.35f; - private static final float GLASS_BLUR_ALPHA = 255f; - private static final float GLASS_PASS_ALPHA = 255f; - private static final float TEXT_ALPHA_FACTOR = 0.88f; - private static final float PIPE_ALPHA_FACTOR = 0.72f; - private static final float TEXT_DARKEN_MIN = 0.78f; - private static final float TEXT_DARKEN_MAX = 1.00f; - private static final float TEXT_ANIM_SPEED = 2.2f; private static final float LAYOUT_SMOOTH_SPEED = 14.0f; - private static final float BASE_TOAST_SLIDE = 7.0f; private static final float LAYOUT_SNAP_EPS = 0.12f; - private static final String ICON_NO = "I"; - private static final String ICON_INFO = "J"; - private static final String ICON_YES = "K"; - private static final String ICON_WARN = "L"; - - private static final int ENTER_MS = 400; - private static final int DEFAULT_EXIT_MS = 260; - private static final long PREVIEW_ROTATE_MS = 1000L; - private static final int TOGGLE_TRANSITION_MS = 420; - private static final String PREVIEW_KEY = "preview"; - private static final PreviewToast[] PREVIEW_EXAMPLES = new PreviewToast[]{ - new PreviewToast("Module - enabled!", NotifyType.YES, ToastVisual.MODULE_TOGGLE, true), - new PreviewToast("Module - disabled!", NotifyType.NO, ToastVisual.MODULE_TOGGLE, false) - }; - private final NumberValue durationMs = new NumberValue<>("duration_ms", 2000, 500, 6000); private final NumberValue fadeMs = new NumberValue<>("fade_ms", 250, 0, 1500); private final NumberValue scaleValue = new NumberValue<>("scale", 1.35, 0.5, 3.0); @@ -149,196 +122,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat INSTANCE.renderPreviewInternal(renderer, textRenderer, x, y, w, h); } - private static ToastPosition toastSlideOffset(MessengerSide side, float anim, float baseScale) { - float offset = (1.0f - clamp01(anim)) * BASE_TOAST_SLIDE * baseScale; - return switch (side) { - case LEFT -> new ToastPosition(-offset, 0f); - case TOP -> new ToastPosition(0f, -offset); - case BOTTOM -> new ToastPosition(0f, offset); - default -> new ToastPosition(offset, 0f); - }; - } - - private static PreviewToast previewExample(long now) { - int index = (int) ((Math.max(0L, now) / PREVIEW_ROTATE_MS) % PREVIEW_EXAMPLES.length); - return PREVIEW_EXAMPLES[index]; - } - - private static long previewStart(long now) { - long safeNow = Math.max(0L, now); - return safeNow - safeNow % PREVIEW_ROTATE_MS; - } - - private static ToastMetrics measureToast(Toast toast, - TextRenderer lineRenderer, - TextRenderer iconRenderer, - float lineScale, - float iconScale, - float baseScale) { - boolean moduleToggle = toast.visual == ToastVisual.MODULE_TOGGLE; - String icon = iconFor(toast.type); - float iconW = moduleToggle ? BASE_TOGGLE_W * baseScale : measureWidth(iconRenderer, icon, iconScale); - float iconH = moduleToggle ? BASE_TOGGLE_H * baseScale : measureHeight(iconRenderer, iconScale); - float pipeW = measureWidth(lineRenderer, "|", lineScale); - float textW = measureWidth(lineRenderer, toast.line, lineScale); - if (PREVIEW_KEY.equals(toast.key)) { - textW = Math.max(textW, previewMaxTextWidth(lineRenderer, lineScale)); - } - float textH = measureHeight(lineRenderer, lineScale); - - float leftPad = BASE_LEFT_PAD * baseScale; - float rightPad = BASE_RIGHT_PAD * baseScale; - float iconPipeGap = (moduleToggle ? BASE_TOGGLE_PIPE_GAP : BASE_ICON_PIPE_GAP) * baseScale; - float pipeTextGap = BASE_PIPE_TEXT_GAP * baseScale; - - float boxW = leftPad + iconW + iconPipeGap + pipeW + pipeTextGap + textW + rightPad; - float boxH = Math.max(BASE_BOX_HEIGHT * baseScale, Math.max(textH, iconH) + (6.0f * baseScale)); - float radius = boxH * 0.5f; - float iconX = leftPad; - float iconY = (boxH - iconH) * 0.5f; - float pipeX = iconX + iconW + iconPipeGap; - float textX = pipeX + pipeW + pipeTextGap; - float textY = (boxH - textH) * 0.5f + (0.8f * baseScale); - - return new ToastMetrics(boxW, boxH, radius, textScale(lineScale), iconScale, iconX, iconY, iconW, iconH, pipeX, textX, textY); - } - - private static float previewMaxTextWidth(TextRenderer renderer, float scale) { - float max = 0f; - for (PreviewToast preview : PREVIEW_EXAMPLES) { - max = Math.max(max, measureWidth(renderer, preview.line(), scale)); - } - return max; - } - - private static float textScale(float value) { - return value; - } - - private static ToastPalette paletteFor() { - int progressTrack = 0x33FFFFFF; - int progressFill = 0xC4FFFFFF; - int bg = 0x52000000; - return new ToastPalette( - bg, - bg, - 0xFFF7FAFF, - progressTrack, - progressFill, - 0xFFFFFFFF - ); - } - - private static NotifyType resolveType(String text, Boolean enabled) { - if (enabled != null) return enabled ? NotifyType.YES : NotifyType.NO; - String lower = text.toLowerCase(); - if (lower.contains("warn") || lower.contains("warning") || lower.contains("cooldown")) { - return NotifyType.WARN; - } - if (lower.contains("error") || lower.contains("failed") - || lower.contains("fail") || lower.contains("denied") - || lower.contains("blocked") || lower.contains("disabled")) { - return NotifyType.NO; - } - return NotifyType.INFO; - } - - private static String iconFor(NotifyType type) { - return switch (type) { - case YES -> ICON_YES; - case NO -> ICON_NO; - case WARN -> ICON_WARN; - case INFO -> ICON_INFO; - }; - } - - private static int colorFor(NotifyType type) { - return switch (type) { - case YES -> 0xFF6DFF56; - case NO -> 0xFFFF5656; - case WARN -> 0xFFF5FF5E; - case INFO -> 0xFF4AA3FF; - }; - } - - private static int moduleToggleColor(float progress, float anim) { - Themes.Theme theme = Theme.theme(); - int off = theme != null ? HudRenderUtil.mixColor(theme.textMuted(), theme.textPrimary(), 0.18f) : 0xFFC6D2CD; - int on = theme != null ? theme.accent() : 0xFF5CC8E7; - int color = HudRenderUtil.mixColor(off, on, AnimationUtility.clamp01(progress)); - return applyAlpha(color, anim); - } - - private static String resolveLine(String text, Boolean enabled) { - if (enabled != null) { - return text + " - " + (enabled ? "enabled!" : "disabled!"); - } - return text; - } - - private static float easeOutBack(float t) { - float c1 = 1.70158f; - float c3 = c1 + 1f; - float p = t - 1f; - return 1f + c3 * p * p * p + c1 * p * p; - } - - private static float clamp01(float v) { - return Math.max(0f, Math.min(1f, v)); - } - - private static int applyAlpha(int argb, float alpha) { - int a = (int) (((argb >>> 24) & 0xFF) * alpha); - return (argb & 0x00FFFFFF) | ((a & 0xFF) << 24); - } - - private static float measureWidth(TextRenderer renderer, String text, float scale) { - if (renderer == null || text == null || text.isEmpty()) return 0f; - renderer.begin(scale, true, false); - float width = (float) renderer.getWidth(text, false); - renderer.end(); - return width; - } - - private static float measureHeight(TextRenderer renderer, float scale) { - if (renderer == null) return 0f; - renderer.begin(scale, true, false); - float height = (float) renderer.getHeight(false); - renderer.end(); - return height; - } - - private static int scaleAlpha(int argb, float factor) { - factor = AnimationUtility.clamp01(factor); - int a = (argb >>> 24) & 0xFF; - int scaledA = Math.round(a * factor); - return (argb & 0x00FFFFFF) | ((scaledA & 0xFF) << 24); - } - - private static int darken(int argb, float factor) { - factor = AnimationUtility.clamp(factor, 0f, 1f); - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - r = Math.round(r * factor); - g = Math.round(g * factor); - b = Math.round(b * factor); - - return ((a & 0xFF) << 24) - | ((r & 0xFF) << 16) - | ((g & 0xFF) << 8) - | (b & 0xFF); - } - - private static void applyColor(RenderColor out, int argb) { - out.a = (argb >>> 24) & 0xFF; - out.r = (argb >>> 16) & 0xFF; - out.g = (argb >>> 8) & 0xFF; - out.b = argb & 0xFF; - } - @Override protected void defineSettings(List defs) { defs.add(SettingDef.number(durationMs)); @@ -360,7 +143,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat float margin = BASE_MARGIN * scale; float width = 170f * scale; - float height = BASE_BOX_HEIGHT * scale; + float height = ToastGeometry.BASE_BOX_HEIGHT * scale; // Position side is intentionally automatic now; the draggable position itself defines slide direction. this.x = Math.max(margin, screenW - margin - width); this.y = Math.max(margin, screenH - margin - BASE_BOTTOM_OFFSET * scale - height); @@ -440,7 +223,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat float anim = sample ? 1.0f : toast.anim(now, duration, fade); if (anim <= 0.001f) continue; - ToastMetrics metrics = measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); + ToastMetrics metrics = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); float remaining = sample ? 0.45f : toast.remaining(now, duration); draws.add(new ToastDraw(toast, metrics, remaining, anim, 0f, 0f)); maxW = Math.max(maxW, metrics.boxW); @@ -472,17 +255,18 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat }; float targetY = cursorY; ToastPosition pos = resolveToastPosition(draw.toast, targetX, targetY, sample, baseScale); - ToastPosition slide = toastSlideOffset(side, draw.anim, baseScale); + ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale); draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, pos.x() + slide.x(), pos.y() + slide.y())); cursorY += (metrics.boxH + gap) * draw.anim; } setBounds(x, y, layoutW, Math.max(1f, layoutH)); - drawGlassBlurBatch(draws, baseScale); + ToastGeometry.drawGlassBlurBatch(draws, baseScale); for (ToastDraw draw : draws) { - drawToastCard( + ToastCardRenderer.drawToastCard( renderer, + colorTmp, draw.toast, lineRenderer, iconRenderer, @@ -559,9 +343,9 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat } private Toast previewToast(long now) { - PreviewToast preview = previewExample(now); - long start = previewStart(now); - return new Toast(PREVIEW_KEY, preview.line(), preview.type(), start, preview.visual(), preview.enabled()); + PreviewToast preview = ToastModel.previewExample(now); + long start = ToastModel.previewStart(now); + return new Toast(ToastModel.PREVIEW_KEY, preview.line(), preview.type(), start, preview.visual(), preview.enabled()); } private void renderPreviewInternal(Renderer2D renderer, @@ -608,7 +392,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat float anim = sampleMode ? 1.0f : toast.anim(now, duration, fade); if (anim <= 0.001f) continue; - ToastMetrics metrics = measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); + ToastMetrics metrics = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); float remaining = sampleMode ? 0.45f : toast.remaining(now, duration); maxW = Math.max(maxW, metrics.boxW); draws.add(new ToastDraw(toast, metrics, remaining, anim, 0f, 0f)); @@ -623,15 +407,16 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat default -> originX + screenW - margin - metrics.boxW; }; float targetY = originY + cursorY; - ToastPosition slide = toastSlideOffset(side, draw.anim, baseScale); + ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale); draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, targetX + slide.x(), targetY + slide.y())); cursorY += (metrics.boxH + gap) * draw.anim; } - drawGlassBlurBatch(draws, baseScale); + ToastGeometry.drawGlassBlurBatch(draws, baseScale); for (ToastDraw draw : draws) { - drawToastCard( + ToastCardRenderer.drawToastCard( renderer, + colorTmp, draw.toast, lineRenderer, iconRenderer, @@ -665,7 +450,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat } private void enqueue(String key, String text, Boolean enabled, NotifyType forcedType) { - NotifyType type = forcedType != null ? forcedType : resolveType(text, enabled); + NotifyType type = forcedType != null ? forcedType : ToastStyle.resolveType(text, enabled); enqueueInternal(key, text, enabled, type, ToastVisual.STANDARD); } @@ -677,7 +462,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat if (!enabledValue().get()) return; if (hud.getHeaderMode() == HudGlobalConfig.HeaderMode.NEVER) return; - String line = resolveLine(text, enabled); + String line = ToastStyle.resolveLine(text, enabled); long now = System.currentTimeMillis(); if (key != null) { @@ -703,225 +488,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat } } - private void drawToastCard(Renderer2D renderer, - Toast toast, - TextRenderer lineRenderer, - TextRenderer iconRenderer, - float x, - float y, - ToastMetrics metrics, - float remaining, - float anim, - float baseScale) { - ToastPalette palette = paletteFor(); - - int glassTint = applyAlpha(palette.glassTint, anim); - int bgLeft = applyAlpha(palette.bgLeft, anim); - int bgRight = applyAlpha(palette.bgRight, anim); - long now = System.currentTimeMillis(); - float wave = 0.5f + 0.5f * (float) Math.sin(now / 1000.0f * TEXT_ANIM_SPEED); - float darkenFactor = AnimationUtility.lerp(TEXT_DARKEN_MIN, TEXT_DARKEN_MAX, wave); - - int text = scaleAlpha(applyAlpha(palette.text, anim), TEXT_ALPHA_FACTOR); - text = darken(text, darkenFactor); - - int progressTrack = applyAlpha(palette.progressTrack, anim); - int progress = applyAlpha(palette.progressFill, anim); - int accent = applyAlpha(colorFor(toast.type), anim); - - int pipe = scaleAlpha(applyAlpha(HudRenderUtil.mixColor(palette.text, palette.progressTrack, 0.55f), anim), PIPE_ALPHA_FACTOR); - pipe = darken(pipe, darkenFactor * 0.96f); - float sideBottomR = Math.max(1f, metrics.boxH * 0.5f); - float sideTopR = 0f; - - drawGlassPill( - renderer, - x, - y, - metrics.boxW, - metrics.boxH, - sideTopR, - sideTopR, - sideBottomR, - sideBottomR, - glassTint, - anim - ); - - int bgMixed = HudRenderUtil.mixColor(bgLeft, bgRight, 0.5f); - int bgA = (bgMixed >>> 24) & 0xFF; - int bgSolid = HudRenderUtil.glassBackground(anim); - renderer.roundedRectCornersQuad( - x, - y, - metrics.boxW, - metrics.boxH, - sideTopR, - sideTopR, - sideBottomR, - sideBottomR, - 1.0f, - bgSolid, - bgSolid, - bgSolid, - bgSolid - ); - - float progressInsetX = 0.45f * baseScale; - float progressInsetY = 0.08f * baseScale; - float progressH = Math.max(1f, BASE_PROGRESS_H * baseScale); - float progressY = y + progressInsetY; - float progressX = x + progressInsetX; - float progressAreaW = Math.max(0f, metrics.boxW - progressInsetX * 2f); - float progressW = Math.max(0f, progressAreaW * clamp01(remaining)); - float progressTopR = 0f; - float progressBottomR = 0.02f * baseScale; - renderer.roundedRectCornersQuad( - progressX, - progressY, - progressAreaW, - progressH, - progressTopR, - progressTopR, - progressBottomR, - progressBottomR, - 1.0f, - progressTrack, - progressTrack, - progressTrack, - progressTrack - ); - if (progressW > 0f) { - renderer.roundedRectCornersQuad( - progressX, - progressY, - progressW, - progressH, - progressTopR, - progressTopR, - progressBottomR, - progressBottomR, - 1.0f, - progress, - progress, - progress, - progress - ); - } - - if (toast.visual == ToastVisual.MODULE_TOGGLE) { - drawModuleToggle(renderer, toast, x + metrics.iconX, y + metrics.iconY, metrics.iconW, metrics.iconH, accent, anim, now); - } else { - iconRenderer.begin(metrics.iconScale, false, false); - applyColor(colorTmp, accent); - iconRenderer.render(iconFor(toast.type), x + metrics.iconX, y + metrics.iconY, colorTmp, false); - iconRenderer.end(); - } - - lineRenderer.begin(metrics.textScale, false, false); - applyColor(colorTmp, pipe); - lineRenderer.render("|", x + metrics.pipeX, y + metrics.textY, colorTmp, false); - applyColor(colorTmp, text); - lineRenderer.render(toast.line, x + metrics.textX, y + metrics.textY, colorTmp, false); - lineRenderer.end(); - } - - private void drawModuleToggle(Renderer2D renderer, - Toast toast, - float x, - float y, - float w, - float h, - int fallbackAccent, - float anim, - long now) { - if (renderer == null || w <= 0.1f || h <= 0.1f) return; - - float progress = toast.toggleProgress(now); - int tint = moduleToggleColor(progress, anim); - if (((tint >>> 24) & 0xFF) <= 0) { - tint = fallbackAccent; - } - - float svgY = y + (h - w) * 0.5f; - SvgRenderOptions options = SvgRenderOptions.overrideColor(tint); - if (progress <= 0.001f) { - renderer.svg("toggle-left", x, svgY, w, w, options); - return; - } - if (progress >= 0.999f) { - renderer.svg("toggle-right", x, svgY, w, w, options); - return; - } - - renderer.svg("toggle-track", x, svgY, w, w, options); - - float thumbTravel = w * (3f / 24f); - float thumbX = x + AnimationUtility.lerp(-thumbTravel, thumbTravel, progress); - renderer.svg("toggle-thumb", thumbX, svgY, w, w, options); - } - - private void drawGlassBlurBatch(List draws, float baseScale) { - if (draws == null || draws.isEmpty()) return; - HudGlobalConfig hud = HudGlobalConfig.get(); - if (hud == null) return; - - Renderer2D.COLOR.blurComposite(composite -> { - for (ToastDraw draw : draws) { - if (draw == null || draw.metrics == null || draw.anim <= 0.001f) continue; - float a = clamp01(draw.anim); - float sideBottomR = Math.max(1f, draw.metrics.boxH * 0.5f); - composite.roundedRectCorners( - draw.x, - draw.y, - draw.metrics.boxW, - draw.metrics.boxH, - 0f, - 0f, - sideBottomR, - sideBottomR, - hud.getBlurRadius(), - 1.0f, - (GLASS_BLUR_ALPHA / 255.0f) * a, - 0xFFFFFF - ); - } - }); - } - - private void drawGlassPill(Renderer2D renderer, - float x, - float y, - float w, - float h, - float radiusTL, - float radiusTR, - float radiusBR, - float radiusBL, - int tintArgb, - float anim) { - if (renderer == null) return; - float a = clamp01(anim); - if (a <= 0.001f) return; - - float glassAlpha = (GLASS_PASS_ALPHA / 255.0f) * a; - float glassScale = Math.max(0.50f, Math.min(1.25f, h / 24.0f)); - HudRenderUtil.drawLiquidGlassCorners( - x, - y, - w, - h, - radiusTL, - radiusTR, - radiusBR, - radiusBL, - glassScale, - false, - glassAlpha, - tintArgb - ); - } - private int getDurationMs() { return durationMs.get(); } @@ -978,23 +544,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat return Identifier.fromNamespaceAndPath("lovisual", "sounds/" + base + ".wav"); } - private enum MessengerSide { - RIGHT, - LEFT, - TOP, - BOTTOM; - - static MessengerSide from(String name) { - if (name == null) return RIGHT; - return switch (name.toLowerCase()) { - case "left" -> LEFT; - case "top" -> TOP; - case "bottom" -> BOTTOM; - default -> RIGHT; - }; - } - } - public enum NotifyType { YES, NO, @@ -1002,107 +551,9 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat INFO } - private enum ToastVisual { - STANDARD, - MODULE_TOGGLE - } - - private static final class Toast { - private final String key; - private String line; - private NotifyType type; - private ToastVisual visual; - private Boolean stateEnabled; - private long startMs; - private boolean layoutReady; - private float layoutX; - private float layoutY; - - private Toast(String key, String line, NotifyType type, long startMs) { - this(key, line, type, startMs, ToastVisual.STANDARD, null); - } - - private Toast(String key, String line, NotifyType type, long startMs, ToastVisual visual, Boolean stateEnabled) { - this.key = key; - this.line = line; - this.type = type; - this.visual = visual == null ? ToastVisual.STANDARD : visual; - this.stateEnabled = stateEnabled; - this.startMs = startMs; - } - - boolean isExpired(long now, int duration) { - return now - startMs > duration; - } - - float remaining(long now, int duration) { - if (duration <= 0) return 0f; - long age = Math.max(0L, now - startMs); - return 1f - clamp01((float) age / (float) duration); - } - - float anim(long now, int duration, int fade) { - long age = Math.max(0L, now - startMs); - if (age >= duration) return 0f; - - float in = clamp01((float) age / (float) ENTER_MS); - float enter = easeOutBack(in); - - int outMs = Math.max(1, fade > 0 ? fade : DEFAULT_EXIT_MS); - float out = 1f; - if (age > duration - outMs) { - out = clamp01((float) (duration - age) / (float) outMs); - } - - return clamp01(enter * out); - } - - float toggleProgress(long now) { - boolean enabled = stateEnabled != null ? stateEnabled : type == NotifyType.YES; - long age = Math.max(0L, now - startMs); - float t = AnimationUtility.easeInOutCubic((float) age / (float) TOGGLE_TRANSITION_MS); - return enabled ? t : 1f - t; - } - } - - private record PreviewToast(String line, NotifyType type, ToastVisual visual, Boolean enabled) { - } - private record LayoutSize(float width, float height) { } - private record ToastPosition(float x, float y) { - } - - private record ToastDraw(Toast toast, - ToastMetrics metrics, - float remaining, - float anim, - float x, - float y) { - } - - private record ToastMetrics(float boxW, - float boxH, - float radius, - float textScale, - float iconScale, - float iconX, - float iconY, - float iconW, - float iconH, - float pipeX, - float textX, - float textY) { - } - - private record ToastPalette(int bgLeft, - int bgRight, - int text, - int progressTrack, - int progressFill, - int glassTint) { - } @Override public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { super.collectRenderPrewarm(event); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java new file mode 100644 index 0000000..9113e3b --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java @@ -0,0 +1,193 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier; + +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastVisual; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastStyle.ToastPalette; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.TextRenderer; + +/** + * Card drawing routines for HudNotifier toasts: glass pill background, progress bar + * and the icon/pipe/text layout of a single toast card. + */ +public final class ToastCardRenderer { + + public static final float BASE_PROGRESS_H = 1.35f; + public static final float GLASS_PASS_ALPHA = 255f; + public static final float TEXT_ALPHA_FACTOR = 0.88f; + public static final float PIPE_ALPHA_FACTOR = 0.72f; + public static final float TEXT_DARKEN_MIN = 0.78f; + public static final float TEXT_DARKEN_MAX = 1.00f; + public static final float TEXT_ANIM_SPEED = 2.2f; + + private ToastCardRenderer() { + } + + public static void drawToastCard(Renderer2D renderer, + RenderColor colorTmp, + Toast toast, + TextRenderer lineRenderer, + TextRenderer iconRenderer, + float x, + float y, + ToastMetrics metrics, + float remaining, + float anim, + float baseScale) { + ToastPalette palette = ToastStyle.paletteFor(); + + int glassTint = ToastStyle.applyAlpha(palette.glassTint, anim); + int bgLeft = ToastStyle.applyAlpha(palette.bgLeft, anim); + int bgRight = ToastStyle.applyAlpha(palette.bgRight, anim); + long now = System.currentTimeMillis(); + float wave = 0.5f + 0.5f * (float) Math.sin(now / 1000.0f * TEXT_ANIM_SPEED); + float darkenFactor = AnimationUtility.lerp(TEXT_DARKEN_MIN, TEXT_DARKEN_MAX, wave); + + int text = ToastStyle.scaleAlpha(ToastStyle.applyAlpha(palette.text, anim), TEXT_ALPHA_FACTOR); + text = ToastStyle.darken(text, darkenFactor); + + int progressTrack = ToastStyle.applyAlpha(palette.progressTrack, anim); + int progress = ToastStyle.applyAlpha(palette.progressFill, anim); + int accent = ToastStyle.applyAlpha(ToastStyle.colorFor(toast.type), anim); + + int pipe = ToastStyle.scaleAlpha(ToastStyle.applyAlpha(HudRenderUtil.mixColor(palette.text, palette.progressTrack, 0.55f), anim), PIPE_ALPHA_FACTOR); + pipe = ToastStyle.darken(pipe, darkenFactor * 0.96f); + float sideBottomR = Math.max(1f, metrics.boxH * 0.5f); + float sideTopR = 0f; + + drawGlassPill( + renderer, + x, + y, + metrics.boxW, + metrics.boxH, + sideTopR, + sideTopR, + sideBottomR, + sideBottomR, + glassTint, + anim + ); + + int bgMixed = HudRenderUtil.mixColor(bgLeft, bgRight, 0.5f); + int bgA = (bgMixed >>> 24) & 0xFF; + int bgSolid = HudRenderUtil.glassBackground(anim); + renderer.roundedRectCornersQuad( + x, + y, + metrics.boxW, + metrics.boxH, + sideTopR, + sideTopR, + sideBottomR, + sideBottomR, + 1.0f, + bgSolid, + bgSolid, + bgSolid, + bgSolid + ); + + float progressInsetX = 0.45f * baseScale; + float progressInsetY = 0.08f * baseScale; + float progressH = Math.max(1f, BASE_PROGRESS_H * baseScale); + float progressY = y + progressInsetY; + float progressX = x + progressInsetX; + float progressAreaW = Math.max(0f, metrics.boxW - progressInsetX * 2f); + float progressW = Math.max(0f, progressAreaW * ToastModel.clamp01(remaining)); + float progressTopR = 0f; + float progressBottomR = 0.02f * baseScale; + renderer.roundedRectCornersQuad( + progressX, + progressY, + progressAreaW, + progressH, + progressTopR, + progressTopR, + progressBottomR, + progressBottomR, + 1.0f, + progressTrack, + progressTrack, + progressTrack, + progressTrack + ); + if (progressW > 0f) { + renderer.roundedRectCornersQuad( + progressX, + progressY, + progressW, + progressH, + progressTopR, + progressTopR, + progressBottomR, + progressBottomR, + 1.0f, + progress, + progress, + progress, + progress + ); + } + + if (toast.visual == ToastVisual.MODULE_TOGGLE) { + ToastStyle.drawModuleToggle(renderer, toast, x + metrics.iconX, y + metrics.iconY, metrics.iconW, metrics.iconH, accent, anim, now); + } else { + iconRenderer.begin(metrics.iconScale, false, false); + ToastStyle.applyColor(colorTmp, accent); + iconRenderer.render(ToastStyle.iconFor(toast.type), x + metrics.iconX, y + metrics.iconY, colorTmp, false); + iconRenderer.end(); + } + + lineRenderer.begin(metrics.textScale, false, false); + ToastStyle.applyColor(colorTmp, pipe); + lineRenderer.render("|", x + metrics.pipeX, y + metrics.textY, colorTmp, false); + ToastStyle.applyColor(colorTmp, text); + lineRenderer.render(toast.line, x + metrics.textX, y + metrics.textY, colorTmp, false); + lineRenderer.end(); + } + + public static void drawGlassPill(Renderer2D renderer, + float x, + float y, + float w, + float h, + float radiusTL, + float radiusTR, + float radiusBR, + float radiusBL, + int tintArgb, + float anim) { + if (renderer == null) return; + float a = ToastModel.clamp01(anim); + if (a <= 0.001f) return; + + float glassAlpha = (GLASS_PASS_ALPHA / 255.0f) * a; + float glassScale = Math.max(0.50f, Math.min(1.25f, h / 24.0f)); + HudRenderUtil.drawLiquidGlassCorners( + x, + y, + w, + h, + radiusTL, + radiusTR, + radiusBR, + radiusBL, + glassScale, + false, + glassAlpha, + tintArgb + ); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java new file mode 100644 index 0000000..1761926 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java @@ -0,0 +1,200 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier; + +import dev.loki.lovisual.features.gui.hud.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.PreviewToast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastDraw; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.ToastVisual; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import java.util.List; + +/** + * Layout and measurement math for HudNotifier toasts: box metrics, text measurement, + * slide-in offsets and the per-frame blur backdrop pass over the toast draw list. + */ +public final class ToastGeometry { + + public static final float BASE_RADIUS = 4f; + public static final float BASE_BOX_HEIGHT = 16f; + public static final float BASE_LEFT_PAD = 7f; + public static final float BASE_RIGHT_PAD = 9f; + public static final float BASE_ICON_PIPE_GAP = 5f; + public static final float BASE_TOGGLE_PIPE_GAP = 4.5f; + public static final float BASE_PIPE_TEXT_GAP = 6f; + public static final float BASE_TOGGLE_W = 20f; + public static final float BASE_TOGGLE_H = 12f; + public static final float BASE_TOAST_SLIDE = 7.0f; + public static final float GLASS_BLUR_ALPHA = 255f; + + private ToastGeometry() { + } + + public static ToastPosition toastSlideOffset(MessengerSide side, float anim, float baseScale) { + float offset = (1.0f - ToastModel.clamp01(anim)) * BASE_TOAST_SLIDE * baseScale; + return switch (side) { + case LEFT -> new ToastPosition(-offset, 0f); + case TOP -> new ToastPosition(0f, -offset); + case BOTTOM -> new ToastPosition(0f, offset); + default -> new ToastPosition(offset, 0f); + }; + } + + public static ToastMetrics measureToast(Toast toast, + TextRenderer lineRenderer, + TextRenderer iconRenderer, + float lineScale, + float iconScale, + float baseScale) { + boolean moduleToggle = toast.visual == ToastVisual.MODULE_TOGGLE; + String icon = ToastStyle.iconFor(toast.type); + float iconW = moduleToggle ? BASE_TOGGLE_W * baseScale : measureWidth(iconRenderer, icon, iconScale); + float iconH = moduleToggle ? BASE_TOGGLE_H * baseScale : measureHeight(iconRenderer, iconScale); + float pipeW = measureWidth(lineRenderer, "|", lineScale); + float textW = measureWidth(lineRenderer, toast.line, lineScale); + if (ToastModel.PREVIEW_KEY.equals(toast.key)) { + textW = Math.max(textW, previewMaxTextWidth(lineRenderer, lineScale)); + } + float textH = measureHeight(lineRenderer, lineScale); + + float leftPad = BASE_LEFT_PAD * baseScale; + float rightPad = BASE_RIGHT_PAD * baseScale; + float iconPipeGap = (moduleToggle ? BASE_TOGGLE_PIPE_GAP : BASE_ICON_PIPE_GAP) * baseScale; + float pipeTextGap = BASE_PIPE_TEXT_GAP * baseScale; + + float boxW = leftPad + iconW + iconPipeGap + pipeW + pipeTextGap + textW + rightPad; + float boxH = Math.max(BASE_BOX_HEIGHT * baseScale, Math.max(textH, iconH) + (6.0f * baseScale)); + float radius = boxH * 0.5f; + float iconX = leftPad; + float iconY = (boxH - iconH) * 0.5f; + float pipeX = iconX + iconW + iconPipeGap; + float textX = pipeX + pipeW + pipeTextGap; + float textY = (boxH - textH) * 0.5f + (0.8f * baseScale); + + return new ToastMetrics(boxW, boxH, radius, textScale(lineScale), iconScale, iconX, iconY, iconW, iconH, pipeX, textX, textY); + } + + public static float previewMaxTextWidth(TextRenderer renderer, float scale) { + float max = 0f; + for (PreviewToast preview : ToastModel.PREVIEW_EXAMPLES) { + max = Math.max(max, measureWidth(renderer, preview.line(), scale)); + } + return max; + } + + public static float textScale(float value) { + return value; + } + + public static float measureWidth(TextRenderer renderer, String text, float scale) { + if (renderer == null || text == null || text.isEmpty()) return 0f; + renderer.begin(scale, true, false); + float width = (float) renderer.getWidth(text, false); + renderer.end(); + return width; + } + + public static float measureHeight(TextRenderer renderer, float scale) { + if (renderer == null) return 0f; + renderer.begin(scale, true, false); + float height = (float) renderer.getHeight(false); + renderer.end(); + return height; + } + + public static void drawGlassBlurBatch(List draws, float baseScale) { + if (draws == null || draws.isEmpty()) return; + HudGlobalConfig hud = HudGlobalConfig.get(); + if (hud == null) return; + + Renderer2D.COLOR.blurComposite(composite -> { + for (ToastDraw draw : draws) { + if (draw == null || draw.metrics == null || draw.anim <= 0.001f) continue; + float a = ToastModel.clamp01(draw.anim); + float sideBottomR = Math.max(1f, draw.metrics.boxH * 0.5f); + composite.roundedRectCorners( + draw.x, + draw.y, + draw.metrics.boxW, + draw.metrics.boxH, + 0f, + 0f, + sideBottomR, + sideBottomR, + hud.getBlurRadius(), + 1.0f, + (GLASS_BLUR_ALPHA / 255.0f) * a, + 0xFFFFFF + ); + } + }); + } + + public enum MessengerSide { + RIGHT, + LEFT, + TOP, + BOTTOM; + + public static MessengerSide from(String name) { + if (name == null) return RIGHT; + return switch (name.toLowerCase()) { + case "left" -> LEFT; + case "top" -> TOP; + case "bottom" -> BOTTOM; + default -> RIGHT; + }; + } + } + + public record ToastPosition(float x, float y) { + } + + public static final class ToastMetrics { + public final float boxW; + public final float boxH; + public final float radius; + public final float textScale; + public final float iconScale; + public final float iconX; + public final float iconY; + public final float iconW; + public final float iconH; + public final float pipeX; + public final float textX; + public final float textY; + + public ToastMetrics(float boxW, + float boxH, + float radius, + float textScale, + float iconScale, + float iconX, + float iconY, + float iconW, + float iconH, + float pipeX, + float textX, + float textY) { + this.boxW = boxW; + this.boxH = boxH; + this.radius = radius; + this.textScale = textScale; + this.iconScale = iconScale; + this.iconX = iconX; + this.iconY = iconY; + this.iconW = iconW; + this.iconH = iconH; + this.pipeX = pipeX; + this.textX = textX; + this.textY = textY; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java new file mode 100644 index 0000000..41ebec3 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastModel.java @@ -0,0 +1,142 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier; + +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudNotifier; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +/** + * Toast state model for HudNotifier: lifecycle timing, preview rotation data, the + * mutable toast entries rendered by the notifier and the per-frame draw items. + */ +public final class ToastModel { + + public static final int ENTER_MS = 400; + public static final int DEFAULT_EXIT_MS = 260; + public static final long PREVIEW_ROTATE_MS = 1000L; + public static final int TOGGLE_TRANSITION_MS = 420; + public static final String PREVIEW_KEY = "preview"; + public static final PreviewToast[] PREVIEW_EXAMPLES = new PreviewToast[]{ + new PreviewToast("Module - enabled!", HudNotifier.NotifyType.YES, ToastVisual.MODULE_TOGGLE, true), + new PreviewToast("Module - disabled!", HudNotifier.NotifyType.NO, ToastVisual.MODULE_TOGGLE, false) + }; + + private ToastModel() { + } + + public static PreviewToast previewExample(long now) { + int index = (int) ((Math.max(0L, now) / PREVIEW_ROTATE_MS) % PREVIEW_EXAMPLES.length); + return PREVIEW_EXAMPLES[index]; + } + + public static long previewStart(long now) { + long safeNow = Math.max(0L, now); + return safeNow - safeNow % PREVIEW_ROTATE_MS; + } + + public static float easeOutBack(float t) { + float c1 = 1.70158f; + float c3 = c1 + 1f; + float p = t - 1f; + return 1f + c3 * p * p * p + c1 * p * p; + } + + public static float clamp01(float v) { + return Math.max(0f, Math.min(1f, v)); + } + + public enum ToastVisual { + STANDARD, + MODULE_TOGGLE + } + + public record PreviewToast(String line, HudNotifier.NotifyType type, ToastVisual visual, Boolean enabled) { + } + + public static final class Toast { + public final String key; + public String line; + public HudNotifier.NotifyType type; + public ToastVisual visual; + public Boolean stateEnabled; + public long startMs; + public boolean layoutReady; + public float layoutX; + public float layoutY; + + public Toast(String key, String line, HudNotifier.NotifyType type, long startMs) { + this(key, line, type, startMs, ToastVisual.STANDARD, null); + } + + public Toast(String key, String line, HudNotifier.NotifyType type, long startMs, ToastVisual visual, Boolean stateEnabled) { + this.key = key; + this.line = line; + this.type = type; + this.visual = visual == null ? ToastVisual.STANDARD : visual; + this.stateEnabled = stateEnabled; + this.startMs = startMs; + } + + public boolean isExpired(long now, int duration) { + return now - startMs > duration; + } + + public float remaining(long now, int duration) { + if (duration <= 0) return 0f; + long age = Math.max(0L, now - startMs); + return 1f - clamp01((float) age / (float) duration); + } + + public float anim(long now, int duration, int fade) { + long age = Math.max(0L, now - startMs); + if (age >= duration) return 0f; + + float in = clamp01((float) age / (float) ENTER_MS); + float enter = easeOutBack(in); + + int outMs = Math.max(1, fade > 0 ? fade : DEFAULT_EXIT_MS); + float out = 1f; + if (age > duration - outMs) { + out = clamp01((float) (duration - age) / (float) outMs); + } + + return clamp01(enter * out); + } + + public float toggleProgress(long now) { + boolean enabled = stateEnabled != null ? stateEnabled : type == HudNotifier.NotifyType.YES; + long age = Math.max(0L, now - startMs); + float t = AnimationUtility.easeInOutCubic((float) age / (float) TOGGLE_TRANSITION_MS); + return enabled ? t : 1f - t; + } + } + + public static final class ToastDraw { + public final Toast toast; + public final ToastMetrics metrics; + public final float remaining; + public final float anim; + public final float x; + public final float y; + + public ToastDraw(Toast toast, + ToastMetrics metrics, + float remaining, + float anim, + float x, + float y) { + this.toast = toast; + this.metrics = metrics; + this.remaining = remaining; + this.anim = anim; + this.x = x; + this.y = y; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java new file mode 100644 index 0000000..227a05e --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastStyle.java @@ -0,0 +1,190 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier; + +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudNotifier; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudNotifier.NotifyType; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastModel.Toast; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.SvgRenderOptions; + +/** + * Color and icon styling for HudNotifier toasts: palettes, notification type colors, + * the small ARGB helpers shared by the toast drawing routines and the module-toggle + * switch artwork. + */ +public final class ToastStyle { + + public static final String ICON_NO = "I"; + public static final String ICON_INFO = "J"; + public static final String ICON_YES = "K"; + public static final String ICON_WARN = "L"; + + private ToastStyle() { + } + + public static ToastPalette paletteFor() { + int progressTrack = 0x33FFFFFF; + int progressFill = 0xC4FFFFFF; + int bg = 0x52000000; + return new ToastPalette( + bg, + bg, + 0xFFF7FAFF, + progressTrack, + progressFill, + 0xFFFFFFFF + ); + } + + public static NotifyType resolveType(String text, Boolean enabled) { + if (enabled != null) return enabled ? NotifyType.YES : NotifyType.NO; + String lower = text.toLowerCase(); + if (lower.contains("warn") || lower.contains("warning") || lower.contains("cooldown")) { + return NotifyType.WARN; + } + if (lower.contains("error") || lower.contains("failed") + || lower.contains("fail") || lower.contains("denied") + || lower.contains("blocked") || lower.contains("disabled")) { + return NotifyType.NO; + } + return NotifyType.INFO; + } + + public static String iconFor(NotifyType type) { + return switch (type) { + case YES -> ICON_YES; + case NO -> ICON_NO; + case WARN -> ICON_WARN; + case INFO -> ICON_INFO; + }; + } + + public static int colorFor(NotifyType type) { + return switch (type) { + case YES -> 0xFF6DFF56; + case NO -> 0xFFFF5656; + case WARN -> 0xFFF5FF5E; + case INFO -> 0xFF4AA3FF; + }; + } + + public static int moduleToggleColor(float progress, float anim) { + Themes.Theme theme = Theme.theme(); + int off = theme != null ? HudRenderUtil.mixColor(theme.textMuted(), theme.textPrimary(), 0.18f) : 0xFFC6D2CD; + int on = theme != null ? theme.accent() : 0xFF5CC8E7; + int color = HudRenderUtil.mixColor(off, on, AnimationUtility.clamp01(progress)); + return applyAlpha(color, anim); + } + + public static String resolveLine(String text, Boolean enabled) { + if (enabled != null) { + return text + " - " + (enabled ? "enabled!" : "disabled!"); + } + return text; + } + + public static int applyAlpha(int argb, float alpha) { + int a = (int) (((argb >>> 24) & 0xFF) * alpha); + return (argb & 0x00FFFFFF) | ((a & 0xFF) << 24); + } + + public static int scaleAlpha(int argb, float factor) { + factor = AnimationUtility.clamp01(factor); + int a = (argb >>> 24) & 0xFF; + int scaledA = Math.round(a * factor); + return (argb & 0x00FFFFFF) | ((scaledA & 0xFF) << 24); + } + + public static int darken(int argb, float factor) { + factor = AnimationUtility.clamp(factor, 0f, 1f); + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + + r = Math.round(r * factor); + g = Math.round(g * factor); + b = Math.round(b * factor); + + return ((a & 0xFF) << 24) + | ((r & 0xFF) << 16) + | ((g & 0xFF) << 8) + | (b & 0xFF); + } + + public static void applyColor(RenderColor out, int argb) { + out.a = (argb >>> 24) & 0xFF; + out.r = (argb >>> 16) & 0xFF; + out.g = (argb >>> 8) & 0xFF; + out.b = argb & 0xFF; + } + + public static void drawModuleToggle(Renderer2D renderer, + Toast toast, + float x, + float y, + float w, + float h, + int fallbackAccent, + float anim, + long now) { + if (renderer == null || w <= 0.1f || h <= 0.1f) return; + + float progress = toast.toggleProgress(now); + int tint = moduleToggleColor(progress, anim); + if (((tint >>> 24) & 0xFF) <= 0) { + tint = fallbackAccent; + } + + float svgY = y + (h - w) * 0.5f; + SvgRenderOptions options = SvgRenderOptions.overrideColor(tint); + if (progress <= 0.001f) { + renderer.svg("toggle-left", x, svgY, w, w, options); + return; + } + if (progress >= 0.999f) { + renderer.svg("toggle-right", x, svgY, w, w, options); + return; + } + + renderer.svg("toggle-track", x, svgY, w, w, options); + + float thumbTravel = w * (3f / 24f); + float thumbX = x + AnimationUtility.lerp(-thumbTravel, thumbTravel, progress); + renderer.svg("toggle-thumb", thumbX, svgY, w, w, options); + } + + public static final class ToastPalette { + public final int bgLeft; + public final int bgRight; + public final int text; + public final int progressTrack; + public final int progressFill; + public final int glassTint; + + public ToastPalette(int bgLeft, + int bgRight, + int text, + int progressTrack, + int progressFill, + int glassTint) { + this.bgLeft = bgLeft; + this.bgRight = bgRight; + this.text = text; + this.progressTrack = progressTrack; + this.progressFill = progressFill; + this.glassTint = glassTint; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java new file mode 100644 index 0000000..77a3742 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java @@ -0,0 +1,168 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.interaction; + +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.HudAnchorX; +import dev.loki.lovisual.features.gui.hud.HudAnchorY; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.helpers.SystemCursor; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ChatScreen; +import org.lwjgl.glfw.GLFW; + +/** + * Обработка ввода drag-and-drop виджетов при открытом чате / drag-редакторе. + */ +public final class RegistryDragController { + + private RegistryDragController() { + } + + public static void handleDragging(Minecraft mc, int screenW, int screenH, float uiScale) { + boolean chatOpen = ClientScreen.current() instanceof ChatScreen; + boolean dragEditorOpen = RegistryState.forceVisible && ClientScreen.current() != null; + boolean allowDragging = chatOpen || dragEditorOpen; + float fbX = (float) mc.mouseHandler.xpos(); + float fbY = (float) mc.mouseHandler.ypos(); + float mx = HudScale.toVirtual(fbX, uiScale); + float my = HudScale.toVirtual(fbY, uiScale); + + boolean left = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_1) == GLFW.GLFW_PRESS; + boolean middle = GLFW.glfwGetMouseButton(mc.getWindow().handle(), GLFW.GLFW_MOUSE_BUTTON_MIDDLE) == GLFW.GLFW_PRESS; + + if (!allowDragging) { + if (RegistryState.dragging != null) { + //DebugLog.config("[HUD] Drag cancelled outside chat for %s", RegistryState.dragging.getId()); + } + if (left && !RegistryState.prevLeft) { + // DebugLog.config("[HUD] Mouse down ignored (chat not open) at (%.1f, %.1f)", mx, my); + } + if (!left && RegistryState.prevLeft) { + // DebugLog.config("[HUD] Mouse up ignored (chat not open) at (%.1f, %.1f)", mx, my); + } + RegistryState.dragging = null; + RegistryState.draggingWithLeft = false; + RegistryDragSession.clearSnapPreview(); + if (RegistryState.hudCursorActive) { + SystemCursor.reset(); + RegistryState.hudCursorActive = false; + } + RegistryState.prevLeft = left; + RegistryState.prevMiddle = middle; + return; + } + + if (RegistryState.dragging != null && !RegistryWidgetStore.isWidgetVisibleForEditor(RegistryState.dragging)) { + RegistryState.dragging = null; + RegistryState.draggingWithLeft = false; + RegistryDragSession.clearSnapPreview(); + } + + if (left && !RegistryState.prevLeft) { + //DebugLog.config("[HUD] Mouse down in chat at (%.1f, %.1f)", mx, my); + for (DraggableHudElement w : RegistryState.WIDGETS) { + /*DebugLog.config("[HUD] Widget %s bounds x=%.1f y=%.1f w=%.1f h=%.1f contains=%s", + w.getId(), w.getX(), w.getY(), w.getWidth(), w.getHeight(), w.contains(mx, my));*/ + } + DraggableHudElement clickTarget = null; + long bestClickOrder = Long.MIN_VALUE; + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (!RegistryWidgetStore.isWidgetVisibleForEditor(w)) continue; + if (!w.contains(mx, my)) continue; + if (!w.isMouseOverInteractive(mx, my)) continue; + long order = w.getRuntimeRenderOrder(); + if (order > bestClickOrder) { + bestClickOrder = order; + clickTarget = w; + } + } + if (clickTarget != null) { + if (clickTarget.onMouseClicked(mx, my, GLFW.GLFW_MOUSE_BUTTON_1)) { + RegistryState.dragging = null; + RegistryState.draggingWithLeft = false; + RegistryDragSession.clearSnapPreview(); + RegistryState.prevLeft = left; + RegistryState.prevMiddle = middle; + return; + } + // interactive hitbox: do not start drag on left click + RegistryState.prevLeft = left; + RegistryState.prevMiddle = middle; + return; + } else { + // start drag with left click if no interactive hitbox + DraggableHudElement candidate = RegistryWidgetStore.pickDragCandidate(mx, my); + if (candidate != null) { + RegistryState.dragging = candidate; + RegistryState.draggingWithLeft = true; + RegistryState.dragOffsetX = mx - candidate.getX(); + RegistryState.dragOffsetY = my - candidate.getY(); + if (RegistryWidgetStore.isWidgetAnchorModifierDown()) { + RegistryWidgetStore.detachChildren(candidate); + } + RegistryState.dragging.clearParentAnchor(); + RegistryState.dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, RegistryState.dragging.getX(), RegistryState.dragging.getY()); + SystemCursor.set(SystemCursor.CursorType.MOVE); + RegistryState.hudCursorActive = true; + } + } + } + + if (middle && !RegistryState.prevMiddle) { + // start drag + DraggableHudElement candidate = RegistryWidgetStore.pickDragCandidate(mx, my); + if (candidate != null) { + RegistryState.dragging = candidate; + RegistryState.draggingWithLeft = false; + RegistryState.dragOffsetX = mx - candidate.getX(); + RegistryState.dragOffsetY = my - candidate.getY(); + if (RegistryWidgetStore.isWidgetAnchorModifierDown()) { + RegistryWidgetStore.detachChildren(candidate); + } + RegistryState.dragging.clearParentAnchor(); + RegistryState.dragging.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, RegistryState.dragging.getX(), RegistryState.dragging.getY()); + //DebugLog.config("[HUD] Drag start for %s at (%.1f, %.1f)", RegistryState.dragging.getId(), RegistryState.dragging.getX(), RegistryState.dragging.getY()); + SystemCursor.set(SystemCursor.CursorType.MOVE); + RegistryState.hudCursorActive = true; + } + if (RegistryState.dragging == null) { + //DebugLog.config("[HUD] No widget found under cursor at (%.1f, %.1f); widgets=%d", mx, my, RegistryState.WIDGETS.size()); + } + } else if (!middle && RegistryState.prevMiddle && RegistryState.dragging != null && !RegistryState.draggingWithLeft) { + RegistryDragSession.stopDragging(screenW, screenH); + } else if (!middle && RegistryState.prevMiddle) { + //DebugLog.config("[HUD] Mouse up with no active drag at (%.1f, %.1f)", mx, my); + } + + if (RegistryState.draggingWithLeft && !left && RegistryState.prevLeft && RegistryState.dragging != null) { + RegistryDragSession.stopDragging(screenW, screenH); + } + + if (((RegistryState.draggingWithLeft && left) || (!RegistryState.draggingWithLeft && middle)) && RegistryState.dragging != null) { + RegistryDragSession.moveDraggedWidget(RegistryState.dragging, mx, my, screenW, screenH); + if (!RegistryWidgetStore.isWidgetAnchorModifierDown()) { + RegistryDragSession.updateSnapPreview(RegistryState.dragging, screenW, screenH); + } else { + RegistryDragSession.clearSnapPreview(); + } + //DebugLog.config("[HUD] Dragging %s -> (%.1f, %.1f)", RegistryState.dragging.getId(), RegistryState.dragging.getX(), RegistryState.dragging.getY()); + SystemCursor.set(SystemCursor.CursorType.MOVE); + RegistryState.hudCursorActive = true; + } else if (RegistryState.dragging == null) { + RegistryDragSession.clearSnapPreview(); + RegistryDragSession.requestHoverCursor(mx, my); + } + + RegistryState.prevLeft = left; + RegistryState.prevMiddle = middle; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java new file mode 100644 index 0000000..1ecacee --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java @@ -0,0 +1,200 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.interaction; + +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistrySnapSupport; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout; +import dev.loki.lovisual.render.helpers.SystemCursor; +import net.minecraft.client.Minecraft; + +import java.util.HashSet; + +/** Жизненный цикл перетаскивания: завершение, перемещение, snap-превью, курсор. */ +public final class RegistryDragSession { + + private RegistryDragSession() { + } + + public static void stopDragging(int screenW, int screenH) { + if (RegistryState.dragging == null) return; + if (!RegistryWidgetStore.isWidgetAnchorModifierDown()) { + applyStickySnap(RegistryState.dragging, screenW, screenH); + } + RegistryState.dragging.clearParentAnchor(); + Minecraft mc = Minecraft.getInstance(); + boolean allowWidgetAnchoring = mc == null || !RegistryWidgetStore.isWidgetAnchorModifierDown(); + RegistrySnapSupport.updateDragAnchors(RegistryState.dragging, screenW, screenH, allowWidgetAnchoring); + RegistryState.dragging.layoutValue().set(RegistryState.dragging.snapshotLayout()); + RegistryState.dragging.saveConfig(); + RegistryState.dragging = null; + RegistryState.draggingWithLeft = false; + clearSnapPreview(); + if (RegistryState.hudCursorActive) { + SystemCursor.reset(); + RegistryState.hudCursorActive = false; + } + } + + public static void cancelDragging() { + RegistryState.dragging = null; + RegistryState.draggingWithLeft = false; + clearSnapPreview(); + if (RegistryState.hudCursorActive) { + SystemCursor.reset(); + RegistryState.hudCursorActive = false; + } + RegistryState.prevLeft = false; + RegistryState.prevMiddle = false; + } + + public static void moveDraggedWidget(DraggableHudElement w, float mx, float my, int screenW, int screenH) { + float newX = mx - RegistryState.dragOffsetX; + float newY = my - RegistryState.dragOffsetY; + RegistryState.Bounds groupBounds = collectLinkedBounds(w, screenW, screenH); + float dx = newX - w.getX(); + float dy = newY - w.getY(); + + if (groupBounds != null && groupBounds.width() <= screenW && groupBounds.height() <= screenH) { + dx = RegistryAnchorLayout.clamp(dx, -groupBounds.minX(), screenW - groupBounds.maxX()); + dy = RegistryAnchorLayout.clamp(dy, -groupBounds.minY(), screenH - groupBounds.maxY()); + w.moveTo(w.getX() + dx, w.getY() + dy); + RegistryAnchorLayout.applyDescendantAnchors(w, screenW, screenH, new HashSet<>()); + return; + } + + w.moveTo(RegistryAnchorLayout.clampX(newX, w, screenW), RegistryAnchorLayout.clampY(newY, w, screenH)); + RegistryAnchorLayout.applyDescendantAnchors(w, screenW, screenH, new HashSet<>()); + } + + public static void updateSnapPreview(DraggableHudElement w, int screenW, int screenH) { + if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) { + clearSnapPreview(); + return; + } + + RegistryState.StickySnap snap = RegistrySnapSupport.findStickySnap(w, screenW, screenH); + if (!snap.hasX() && !snap.hasY()) { + clearSnapPreview(); + return; + } + + RegistryState.snapPreviewActive = true; + RegistryState.snapPreviewHasX = snap.hasX(); + RegistryState.snapPreviewHasY = snap.hasY(); + RegistryState.snapPreviewLineX = snap.x(); + RegistryState.snapPreviewLineY = snap.y(); + } + + public static void applyStickySnap(DraggableHudElement w, int screenW, int screenH) { + if (w == null || w.getWidth() <= 0 || w.getHeight() <= 0) return; + + RegistryState.StickySnap snap = RegistrySnapSupport.findStickySnap(w, screenW, screenH); + float newX = w.getX(); + float newY = w.getY(); + if (snap.hasX()) newX = snap.x(); + if (snap.hasY()) newY = snap.y(); + w.moveTo(RegistryAnchorLayout.clampX(newX, w, screenW), RegistryAnchorLayout.clampY(newY, w, screenH)); + } + + public static void clearSnapPreview() { + RegistryState.snapPreviewActive = false; + RegistryState.snapPreviewHasX = false; + RegistryState.snapPreviewHasY = false; + } + + public static void requestHoverCursor(float mx, float my) { + DraggableHudElement interactive = findTopVisibleWidget(mx, my, true); + if (interactive != null) { + SystemCursor.set(SystemCursor.CursorType.HAND); + RegistryState.hudCursorActive = true; + return; + } + + DraggableHudElement draggable = findTopVisibleWidget(mx, my, false); + if (draggable != null) { + SystemCursor.set(SystemCursor.CursorType.MOVE); + RegistryState.hudCursorActive = true; + } + } + + public static DraggableHudElement findTopVisibleWidget(float mx, float my, boolean interactiveOnly) { + DraggableHudElement best = null; + long bestOrder = Long.MIN_VALUE; + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (!RegistryWidgetStore.isWidgetVisibleForEditor(w)) continue; + if (!w.contains(mx, my)) continue; + + if (interactiveOnly) { + if (!w.isMouseOverInteractive(mx, my)) continue; + } else if (!w.isDraggable()) { + continue; + } + + long order = w.getRuntimeRenderOrder(); + if (order > bestOrder) { + bestOrder = order; + best = w; + } + } + + if (best != null) return best; + for (int i = RegistryState.WIDGETS.size() - 1; i >= 0; i--) { + DraggableHudElement w = RegistryState.WIDGETS.get(i); + if (!RegistryWidgetStore.isWidgetVisibleForEditor(w)) continue; + if (!w.contains(mx, my)) continue; + + if (interactiveOnly) { + if (!w.isMouseOverInteractive(mx, my)) continue; + } else if (!w.isDraggable()) { + continue; + } + + return w; + } + + return null; + } + + public static RegistryState.Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH) { + if (root == null) return null; + return collectLinkedBounds(root, screenW, screenH, new HashSet<>()); + } + + public static RegistryState.Bounds collectLinkedBounds(DraggableHudElement root, int screenW, int screenH, + java.util.Set visited) { + if (root == null || !visited.add(root.getId())) return null; + RegistryState.Bounds bounds = RegistryState.Bounds.of(root); + String parentId = root.getId(); + for (DraggableHudElement child : RegistryState.WIDGETS) { + if (child == root) continue; + if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue; + RegistryAnchorLayout.applyWidgetAnchor(child, root, screenW, screenH); + bounds = RegistryState.Bounds.union(bounds, collectLinkedBounds(child, screenW, screenH, visited)); + } + return bounds; + } + + public static boolean fitsLinkedBoundsAt(DraggableHudElement root, + float desiredRootX, + float desiredRootY, + int screenW, + int screenH) { + RegistryState.Bounds bounds = collectLinkedBounds(root, screenW, screenH); + if (bounds == null) return false; + float dx = desiredRootX - root.getX(); + float dy = desiredRootY - root.getY(); + return bounds.minX() + dx >= 0f + && bounds.minY() + dy >= 0f + && bounds.maxX() + dx <= screenW + && bounds.maxY() + dy <= screenH; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java new file mode 100644 index 0000000..53debfd --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java @@ -0,0 +1,200 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.registry; + +import dev.loki.lovisual.features.gui.hud.*; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableLayoutValue; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.util.logging.DebugLog; +import net.minecraft.client.Minecraft; + +import java.util.*; + +/** Применение якорей и инициализация сохранённых позиций виджетов. */ +public final class RegistryAnchorLayout { + + private RegistryAnchorLayout() { + } + + public static void ensurePositionInitialized(DraggableHudElement w, int screenW, int screenH) { + if (w.isPositionInitialized()) return; + DraggableLayoutValue.State saved = w.layoutValue().get(); + if (saved != null) { + DraggableLayoutValue.State resolved = saved; + if (!saved.logical()) { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.getWindow() != null) { + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + float uiScale = HudScale.scale(fbw, fbh); + float nx = HudScale.toVirtual(saved.x(), uiScale); + float ny = HudScale.toVirtual(saved.y(), uiScale); + float npx = HudScale.toVirtual(saved.parentOffsetX(), uiScale); + float npy = HudScale.toVirtual(saved.parentOffsetY(), uiScale); + resolved = new DraggableLayoutValue.State( + nx, + ny, + saved.anchorX(), + saved.anchorY(), + saved.parentId(), + saved.parentSide(), + npx, + npy, + true + ); + w.layoutValue().set(resolved); + w.saveConfig(); + } + } + float safeW = w.getWidth() > 0f ? w.getWidth() : 20f; + float safeH = w.getHeight() > 0f ? w.getHeight() : 20f; + float clampedX = saved.anchorX() == HudAnchorX.FREE + ? Math.max(0, Math.min(resolved.x(), screenW - safeW)) + : resolved.x(); + float clampedY = saved.anchorY() == HudAnchorY.FREE + ? Math.max(0, Math.min(resolved.y(), screenH - safeH)) + : resolved.y(); + if (saved.anchorX() == HudAnchorX.FREE && saved.anchorY() == HudAnchorY.FREE + && (clampedX != resolved.x() || clampedY != resolved.y())) { + DebugLog.config("[HUD] Saved position clamped for %s: (%.1f, %.1f) -> (%.1f, %.1f)", + w.getId(), resolved.x(), resolved.y(), clampedX, clampedY); + } else { + DebugLog.config("[HUD] Loaded saved position for %s: (%.1f, %.1f)", w.getId(), resolved.x(), resolved.y()); + } + w.moveTo(clampedX, clampedY); + w.setAnchors(resolved.anchorX(), resolved.anchorY(), clampedX, clampedY); + if (resolved.parentId() != null && resolved.parentSide() != WidgetAnchorSide.NONE) { + w.setParentAnchor(resolved.parentId(), resolved.parentSide(), resolved.parentOffsetX(), resolved.parentOffsetY()); + } else { + w.clearParentAnchor(); + } + } else { + w.applyDefaultPosition(screenW, screenH); + w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); + w.clearParentAnchor(); + DebugLog.config("[HUD] Applied default position for %s: (%.1f, %.1f)", w.getId(), w.getX(), w.getY()); + } + w.markPositionInitialized(); + } + + public static void applyAnchors(DraggableHudElement w, int screenW, int screenH) { + applyAnchors(w, screenW, screenH, new HashSet<>()); + } + + public static void applyAnchors(DraggableHudElement w, int screenW, int screenH, Set visiting) { + if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) { + applyScreenAnchor(w, screenW, screenH); + return; + } + DraggableHudElement parent = RegistryWidgetStore.findById(w.getParentId()); + if (parent == null) { + w.clearParentAnchor(); + applyScreenAnchor(w, screenW, screenH); + return; + } + if (!visiting.add(w.getId())) { + w.clearParentAnchor(); + applyScreenAnchor(w, screenW, screenH); + return; + } + if (parent != RegistryState.dragging) { + applyAnchors(parent, screenW, screenH, visiting); + } + if (RegistryWidgetStore.isDescendantOf(parent, w)) { + w.clearParentAnchor(); + applyScreenAnchor(w, screenW, screenH); + } else { + applyWidgetAnchor(w, parent, screenW, screenH); + } + visiting.remove(w.getId()); + } + + public static void applyScreenAnchor(DraggableHudElement w, int screenW, int screenH) { + float x = w.getAnchorOffsetX(); + float y = w.getAnchorOffsetY(); + switch (w.getAnchorX()) { + case LEFT -> x = w.getAnchorOffsetX(); + case CENTER -> x = screenW * 0.5f - w.getWidth() * 0.5f + w.getAnchorOffsetX(); + case RIGHT -> x = screenW - w.getWidth() - w.getAnchorOffsetX(); + case FREE -> x = w.getAnchorOffsetX(); + } + switch (w.getAnchorY()) { + case TOP -> y = w.getAnchorOffsetY(); + case CENTER -> y = screenH * 0.5f - w.getHeight() * 0.5f + w.getAnchorOffsetY(); + case BOTTOM -> y = screenH - w.getHeight() - w.getAnchorOffsetY(); + case FREE -> y = w.getAnchorOffsetY(); + } + w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH)); + } + + public static void applyWidgetAnchor(DraggableHudElement w, DraggableHudElement parent, int screenW, int screenH) { + float x = w.getX(); + float y = w.getY(); + switch (w.getParentSide()) { + case TOP -> { + x = parent.getX() + w.getParentOffsetX(); + y = parent.getY() - w.getHeight() - RegistryState.PARENT_GAP + w.getParentOffsetY(); + } + case BOTTOM -> { + x = parent.getX() + w.getParentOffsetX(); + y = parent.getY() + parent.getHeight() + RegistryState.PARENT_GAP + w.getParentOffsetY(); + } + case LEFT -> { + x = parent.getX() - w.getWidth() - RegistryState.PARENT_GAP + w.getParentOffsetX(); + y = parent.getY() + w.getParentOffsetY(); + } + case RIGHT -> { + x = parent.getX() + parent.getWidth() + RegistryState.PARENT_GAP + w.getParentOffsetX(); + y = parent.getY() + w.getParentOffsetY(); + } + case NONE -> { + } + } + w.moveTo(clampX(x, w, screenW), clampY(y, w, screenH)); + } + + public static void applyDragAnchorLock(DraggableHudElement w, int screenW, int screenH) { + if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) { + DraggableHudElement parent = RegistryWidgetStore.findById(w.getParentId()); + if (parent != null) { + applyWidgetAnchor(w, parent, screenW, screenH); + } + return; + } + if (w.getAnchorX() != HudAnchorX.FREE || w.getAnchorY() != HudAnchorY.FREE) { + applyScreenAnchor(w, screenW, screenH); + } + } + + public static void applyDescendantAnchors(DraggableHudElement parent, int screenW, int screenH, Set visited) { + if (parent == null || !visited.add(parent.getId())) return; + String parentId = parent.getId(); + for (DraggableHudElement child : RegistryState.WIDGETS) { + if (child == parent) continue; + if (!parentId.equals(child.getParentId()) || child.getParentSide() == WidgetAnchorSide.NONE) continue; + applyWidgetAnchor(child, parent, screenW, screenH); + applyDescendantAnchors(child, screenW, screenH, visited); + } + } + + public static float clampX(float x, DraggableHudElement w, int screenW) { + float maxX = Math.max(0f, screenW - Math.max(0f, w.getWidth())); + return clamp(x, 0f, maxX); + } + + public static float clampY(float y, DraggableHudElement w, int screenH) { + float maxY = Math.max(0f, screenH - Math.max(0f, w.getHeight())); + return clamp(y, 0f, maxY); + } + + public static float clamp(float value, float min, float max) { + if (max < min) return min; + return Math.max(min, Math.min(value, max)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java new file mode 100644 index 0000000..75d286d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java @@ -0,0 +1,199 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.registry; + +import dev.loki.lovisual.features.gui.hud.*; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragSession; + +/** Математика прилипания: якоря-кандидаты, sticky snap, якоря после drag. */ +public final class RegistrySnapSupport { + + private RegistrySnapSupport() { + } + + public static RegistryState.WidgetAnchorCandidate findWidgetAnchorCandidate(DraggableHudElement dragging, int screenW, int screenH) { + float bestDist = Float.MAX_VALUE; + RegistryState.WidgetAnchorCandidate best = null; + for (DraggableHudElement target : RegistryState.WIDGETS) { + if (target == dragging) continue; + if (!RegistryWidgetStore.isWidgetVisibleForLinkOverlay(target)) continue; + if (RegistryWidgetStore.isDescendantOf(target, dragging)) continue; + if (target.getWidth() <= 0 || target.getHeight() <= 0) continue; + + float dxOverlap = overlap(dragging.getX(), dragging.getX() + dragging.getWidth(), + target.getX(), target.getX() + target.getWidth()); + float dyOverlap = overlap(dragging.getY(), dragging.getY() + dragging.getHeight(), + target.getY(), target.getY() + target.getHeight()); + + float distTop = Math.abs((dragging.getY() + dragging.getHeight()) - (target.getY() - RegistryState.PARENT_GAP)); + if (dxOverlap > 0 && distTop <= RegistryState.PARENT_SNAP_DIST && distTop < bestDist) { + float offsetX = parentAxisOffset(dragging.getX() - target.getX()); + float desiredY = target.getY() - RegistryState.PARENT_GAP - dragging.getHeight(); + float desiredX = target.getX() + offsetX; + if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; + float offsetY = 0f; + bestDist = distTop; + best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.TOP, offsetX, offsetY); + } + + float distBottom = Math.abs(dragging.getY() - (target.getY() + target.getHeight() + RegistryState.PARENT_GAP)); + if (dxOverlap > 0 && distBottom <= RegistryState.PARENT_SNAP_DIST && distBottom < bestDist) { + float offsetX = parentAxisOffset(dragging.getX() - target.getX()); + float desiredY = target.getY() + target.getHeight() + RegistryState.PARENT_GAP; + float desiredX = target.getX() + offsetX; + if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; + float offsetY = 0f; + bestDist = distBottom; + best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.BOTTOM, offsetX, offsetY); + } + + float distLeft = Math.abs((dragging.getX() + dragging.getWidth()) - (target.getX() - RegistryState.PARENT_GAP)); + if (dyOverlap > 0 && distLeft <= RegistryState.PARENT_SNAP_DIST && distLeft < bestDist) { + float offsetY = parentAxisOffset(dragging.getY() - target.getY()); + float desiredX = target.getX() - RegistryState.PARENT_GAP - dragging.getWidth(); + float desiredY = target.getY() + offsetY; + if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; + float offsetX = 0f; + bestDist = distLeft; + best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.LEFT, offsetX, offsetY); + } + + float distRight = Math.abs(dragging.getX() - (target.getX() + target.getWidth() + RegistryState.PARENT_GAP)); + if (dyOverlap > 0 && distRight <= RegistryState.PARENT_SNAP_DIST && distRight < bestDist) { + float offsetY = parentAxisOffset(dragging.getY() - target.getY()); + float desiredX = target.getX() + target.getWidth() + RegistryState.PARENT_GAP; + float desiredY = target.getY() + offsetY; + if (!RegistryDragSession.fitsLinkedBoundsAt(dragging, desiredX, desiredY, screenW, screenH)) continue; + float offsetX = 0f; + bestDist = distRight; + best = new RegistryState.WidgetAnchorCandidate(target, WidgetAnchorSide.RIGHT, offsetX, offsetY); + } + } + return best; + } + + public static float parentAxisOffset(float offset) { + return Math.abs(offset) <= RegistryState.PARENT_ALIGN_SNAP_DIST ? 0f : offset; + } + + public static float overlap(float a0, float a1, float b0, float b1) { + float left = Math.max(a0, b0); + float right = Math.min(a1, b1); + return right - left; + } + + public static RegistryState.StickySnap findStickySnap(DraggableHudElement dragging, int screenW, int screenH) { + float bestX = 0f; + float bestY = 0f; + float bestXDist = Float.MAX_VALUE; + float bestYDist = Float.MAX_VALUE; + boolean hasX = false; + boolean hasY = false; + + boolean rightSide = dragging.getX() > screenW * 0.5f; + for (DraggableHudElement target : RegistryState.WIDGETS) { + if (target == dragging) continue; + if (!target.isEnabled()) continue; + if (target.getWidth() <= 0 || target.getHeight() <= 0) continue; + + if (rightSide) { + float targetEdge = target.getX() + target.getWidth(); + float dragEdge = dragging.getX() + dragging.getWidth(); + float dist = Math.abs(targetEdge - dragEdge); + if (dist <= RegistryState.PARENT_SNAP_DIST && dist < bestXDist) { + bestXDist = dist; + bestX = targetEdge - dragging.getWidth(); + hasX = true; + } + } else { + float dist = Math.abs(target.getX() - dragging.getX()); + if (dist <= RegistryState.PARENT_SNAP_DIST && dist < bestXDist) { + bestXDist = dist; + bestX = target.getX(); + hasX = true; + } + } + + float distY = Math.abs(target.getY() - dragging.getY()); + if (distY <= RegistryState.PARENT_SNAP_DIST && distY < bestYDist) { + bestYDist = distY; + bestY = target.getY(); + hasY = true; + } + } + + float distTop = Math.abs(dragging.getY()); + if (distTop <= RegistryState.PARENT_SNAP_DIST && distTop < bestYDist) { + bestY = 0f; + hasY = true; + } + + return new RegistryState.StickySnap(hasX, hasY, bestX, bestY); + } + + public static void updateDragAnchors(DraggableHudElement w, int screenW, int screenH, boolean allowWidgetAnchoring) { + if (w.getWidth() <= 0 || w.getHeight() <= 0) { + w.clearParentAnchor(); + w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); + return; + } + + if (!allowWidgetAnchoring) { + w.clearParentAnchor(); + w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); + return; + } + + if (w.supportsWidgetAnchoring()) { + RegistryState.WidgetAnchorCandidate candidate = findWidgetAnchorCandidate(w, screenW, screenH); + if (candidate != null) { + w.setParentAnchor(candidate.parent().getId(), candidate.side(), candidate.offsetX(), candidate.offsetY()); + return; + } + } + w.clearParentAnchor(); + + float x = w.getX(); + float y = w.getY(); + + HudAnchorX ax = HudAnchorX.FREE; + float offX = x; + float leftDist = Math.abs(x); + float rightDist = Math.abs((x + w.getWidth()) - screenW); + float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f); + if (leftDist <= RegistryState.SNAP_DIST) { + ax = HudAnchorX.LEFT; + offX = x; + } else if (rightDist <= RegistryState.SNAP_DIST) { + ax = HudAnchorX.RIGHT; + offX = rightDist; + } else if (centerDistX <= RegistryState.SNAP_DIST) { + ax = HudAnchorX.CENTER; + offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f; + } + + HudAnchorY ay = HudAnchorY.FREE; + float offY = y; + float topDist = Math.abs(y); + float bottomDist = Math.abs((y + w.getHeight()) - screenH); + float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f); + if (topDist <= RegistryState.SNAP_DIST) { + ay = HudAnchorY.TOP; + offY = y; + } else if (bottomDist <= RegistryState.SNAP_DIST) { + ay = HudAnchorY.BOTTOM; + offY = bottomDist; + } else if (centerDistY <= RegistryState.SNAP_DIST) { + ay = HudAnchorY.CENTER; + offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f; + } + + w.setAnchors(ax, ay, offX, offY); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java new file mode 100644 index 0000000..696f27a --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java @@ -0,0 +1,92 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.registry; + +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide; +import dev.loki.lovisual.features.theme.Themes; + +import java.util.ArrayList; +import java.util.List; + +/** + * Общее состояние реестра HUD-виджетов: константы, поля drag-and-drop + * и структуры данных раскладки. + */ +public final class RegistryState { + + private RegistryState() { + } + + public static final List WIDGETS = new ArrayList<>(); + public static final float SNAP_DIST = 8f; + public static final float PARENT_SNAP_DIST = 10f; + public static final float PARENT_ALIGN_SNAP_DIST = 5f; + public static final float PARENT_GAP = 6f; + public static final int SNAP_PREVIEW_LINE = 0xFFFFFFFF; + public static final float DRAG_COORD_TEXT_SCALE = 0.85f; + public static final float DRAG_COORD_MARGIN = 4f; + public static final float LINK_WIRE_THICKNESS = 1.05f; + public static final float LINK_PREVIEW_THICKNESS = 1.55f; + + public static DraggableHudElement dragging; + public static float dragOffsetX; + public static float dragOffsetY; + public static boolean draggingWithLeft; + public static boolean prevLeft; + public static boolean prevMiddle; + public static long renderOrderSeq; + public static boolean forceVisible; + public static boolean snapPreviewActive; + public static boolean snapPreviewHasX; + public static boolean snapPreviewHasY; + public static float snapPreviewLineX; + public static float snapPreviewLineY; + public static String editorWidgetId; + public static boolean hudCursorActive; + public static int lastScreenW = -1; + public static int lastScreenH = -1; + public static Themes.Theme cachedLinkTheme; + public static LinkWireColors cachedLinkColors; + + public record StickySnap(boolean hasX, boolean hasY, float x, float y) { + } + + public record WidgetAnchorCandidate(DraggableHudElement parent, WidgetAnchorSide side, float offsetX, + float offsetY) { + } + + public record LinkWireColors(int wireStart, int wireEnd, int previewStart, int previewEnd) { + } + + public record Bounds(float minX, float minY, float maxX, float maxY) { + + public static Bounds of(DraggableHudElement w) { + return new Bounds(w.getX(), w.getY(), w.getX() + w.getWidth(), w.getY() + w.getHeight()); + } + + public static Bounds union(Bounds a, Bounds b) { + if (a == null) return b; + if (b == null) return a; + return new Bounds( + Math.min(a.minX, b.minX), + Math.min(a.minY, b.minY), + Math.max(a.maxX, b.maxX), + Math.max(a.maxY, b.maxY) + ); + } + + public float width() { + return maxX - minX; + } + + public float height() { + return maxY - minY; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java new file mode 100644 index 0000000..e02bf40 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java @@ -0,0 +1,196 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.registry; + +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.gui.hud.*; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.module.HudPhase; +import dev.loki.lovisual.util.input.KeyManager; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Хранение и поиск виджетов реестра + предикаты видимости для редактора. + */ +public final class RegistryWidgetStore { + + private RegistryWidgetStore() { + } + + public static void register(DraggableHudElement widget) { + if (!RegistryState.WIDGETS.contains(widget)) { + widget.postInit(); + RegistryState.WIDGETS.add(widget); + DebugLog.config("[HUD] Registered widget: %s", widget.getId()); + } + } + + public static List getWidgets() { + HudElements.ensureLoaded(); + return Collections.unmodifiableList(RegistryState.WIDGETS); + } + + public static DraggableHudElement getById(String id) { + HudElements.ensureLoaded(); + return findById(id); + } + + public static boolean isEnabled(String id) { + if (!Lifecycle.canRunHud()) return false; + DraggableHudElement w = findById(id); + return w != null && w.isEnabled(); + } + + public static T get(Class type) { + if (type == null) return null; + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (type.isInstance(w)) { + return type.cast(w); + } + } + return null; + } + + public static boolean isEnabled(Class type) { + if (!Lifecycle.canRunHud()) return false; + DraggableHudElement w = get(type); + return w != null && w.isEnabled(); + } + + public static void tickAll() { + for (DraggableHudElement w : RegistryState.WIDGETS) { + w.onTick(); + } + } + + public static DraggableHudElement findById(String id) { + if (id == null) return null; + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (id.equals(w.getId())) return w; + } + return null; + } + + public static void noteRendered(DraggableHudElement w) { + if (w == null) return; + w.markRendered(++RegistryState.renderOrderSeq); + } + + public static boolean isWidgetVisibleForEditor(DraggableHudElement w) { + if (w == null) return false; + + boolean singleEditorPreview = RegistryState.editorWidgetId != null; + if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) { + return false; + } + + boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible; + if (!bypassEnabled && !w.isEnabled()) { + return false; + } + + if (!w.usesEngineRenderer()) return false; + if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) return false; + return w.getRuntimeRenderOrder() > 0L; + } + + public static boolean isRenderableCandidate(DraggableHudElement w, HudPhase phase) { + if (w == null) return false; + + boolean singleEditorPreview = RegistryState.editorWidgetId != null; + if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) { + return false; + } + + boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible; + if (!bypassEnabled && !w.isEnabled()) { + return false; + } + + if (!w.usesEngineRenderer()) return false; + if (w.getHudPhase() != phase) return false; + return w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL; + } + + public static boolean isWidgetVisibleForLinkOverlay(DraggableHudElement w) { + if (w == null) return false; + + boolean singleEditorPreview = RegistryState.editorWidgetId != null; + if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) { + return false; + } + + boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible; + if (!bypassEnabled && !w.isEnabled()) { + return false; + } + + return w.usesEngineRenderer() && w.getRenderSpace() == HudRenderSpace.UNSCALED_LOGICAL; + } + + public static boolean isDescendantOf(DraggableHudElement possibleDescendant, DraggableHudElement ancestor) { + if (possibleDescendant == null || ancestor == null) return false; + String ancestorId = ancestor.getId(); + Set visited = new HashSet<>(); + DraggableHudElement current = possibleDescendant; + while (current != null && current.getParentId() != null && current.getParentSide() != WidgetAnchorSide.NONE) { + if (!visited.add(current.getId())) return false; + if (ancestorId.equals(current.getParentId())) return true; + current = findById(current.getParentId()); + } + return false; + } + + public static void detachChildren(DraggableHudElement parent) { + String parentId = parent.getId(); + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (w == parent) continue; + if (parentId.equals(w.getParentId()) && w.getParentSide() != WidgetAnchorSide.NONE) { + w.clearParentAnchor(); + w.setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, w.getX(), w.getY()); + w.layoutValue().set(w.snapshotLayout()); + w.saveConfig(); + } + } + } + + public static boolean isWidgetAnchorModifierDown() { + return KeyManager.isComboHeldAllowScreen("LEFT_ALT") + || KeyManager.isComboHeldAllowScreen("RIGHT_ALT"); + } + + public static DraggableHudElement pickDragCandidate(float mx, float my) { + DraggableHudElement candidate = null; + long bestOrder = Long.MIN_VALUE; + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (!isWidgetVisibleForEditor(w)) continue; + if (!w.isDraggable() || !w.contains(mx, my)) continue; + long order = w.getRuntimeRenderOrder(); + if (order > bestOrder) { + bestOrder = order; + candidate = w; + } + } + if (candidate == null) { + for (int i = RegistryState.WIDGETS.size() - 1; i >= 0; i--) { + DraggableHudElement w = RegistryState.WIDGETS.get(i); + if (!isWidgetVisibleForEditor(w)) continue; + if (w.contains(mx, my) && w.isDraggable()) { + candidate = w; + break; + } + } + } + return candidate; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java new file mode 100644 index 0000000..16b924b --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java @@ -0,0 +1,200 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.render; + +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistrySnapSupport; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.Fonts; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; + +/** Оверлей перетаскивания: координаты, snap-линии и провода родительских связей. */ +public final class RegistryOverlayRenderer { + + private RegistryOverlayRenderer() { + } + + public static void renderDragOverlay(Renderer2D renderer, + TextRenderer fallbackTextRenderer, + int screenW, + int screenH) { + renderLinkWires(renderer, screenW, screenH); + + if (RegistryState.snapPreviewActive) { + if (RegistryState.snapPreviewHasX) { + renderer.line(RegistryState.snapPreviewLineX, 0f, RegistryState.snapPreviewLineX, screenH, RegistryState.SNAP_PREVIEW_LINE); + } + if (RegistryState.snapPreviewHasY) { + renderer.line(0f, RegistryState.snapPreviewLineY, screenW, RegistryState.snapPreviewLineY, RegistryState.SNAP_PREVIEW_LINE); + } + } + + if (RegistryState.dragging == null) return; + TextRenderer coordRenderer = Fonts.renderer("Comfortaa", dev.loki.lovisual.render.engine.text.FontInfo.Type.Regular, + fallbackTextRenderer != null ? fallbackTextRenderer : TextRenderer.get()); + if (coordRenderer == null) return; + + String coords = "x: " + Math.round(RegistryState.dragging.getX()) + " y: " + Math.round(RegistryState.dragging.getY()); + coordRenderer.begin(RegistryState.DRAG_COORD_TEXT_SCALE, false, false); + try { + float textW = (float) coordRenderer.getWidth(coords, true); + float textH = (float) coordRenderer.getHeight(true); + float textX = RegistryState.dragging.getX(); + float textY = RegistryState.dragging.getY() - textH - RegistryState.DRAG_COORD_MARGIN; + if (textY < RegistryState.DRAG_COORD_MARGIN) { + textY = RegistryState.dragging.getY() + RegistryState.dragging.getHeight() + RegistryState.DRAG_COORD_MARGIN; + } + textX = RegistryAnchorLayout.clamp(textX, RegistryState.DRAG_COORD_MARGIN, + Math.max(RegistryState.DRAG_COORD_MARGIN, screenW - textW - RegistryState.DRAG_COORD_MARGIN)); + textY = RegistryAnchorLayout.clamp(textY, RegistryState.DRAG_COORD_MARGIN, + Math.max(RegistryState.DRAG_COORD_MARGIN, screenH - textH - RegistryState.DRAG_COORD_MARGIN)); + coordRenderer.render(coords, textX, textY, new dev.loki.lovisual.render.engine.color.RenderColor(0xFFFFFFFF), true); + } finally { + coordRenderer.end(); + } + } + + private static void renderLinkWires(Renderer2D renderer, + int screenW, + int screenH) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getWindow() == null || !(ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen)) + return; + + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + float uiScale = HudScale.scale(fbw, fbh); + float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale); + float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale); + + RegistryState.LinkWireColors colors = linkWireColors(); + for (DraggableHudElement child : RegistryState.WIDGETS) { + if (!RegistryWidgetStore.isWidgetVisibleForLinkOverlay(child)) continue; + if (child == RegistryState.dragging) continue; + if (child.getParentId() == null || child.getParentSide() == WidgetAnchorSide.NONE) continue; + + DraggableHudElement parent = RegistryWidgetStore.findById(child.getParentId()); + if (!RegistryWidgetStore.isWidgetVisibleForLinkOverlay(parent)) continue; + if (!parent.contains(mx, my) && !child.contains(mx, my)) continue; + renderLinkWire(renderer, parent, child, child.getParentSide(), + RegistryState.LINK_WIRE_THICKNESS, colors.wireStart(), colors.wireEnd()); + } + + if (RegistryState.dragging == null || RegistryWidgetStore.isWidgetAnchorModifierDown()) return; + RegistryState.WidgetAnchorCandidate candidate = + RegistrySnapSupport.findWidgetAnchorCandidate(RegistryState.dragging, screenW, screenH); + if (candidate == null) return; + renderLinkWire(renderer, candidate.parent(), RegistryState.dragging, candidate.side(), + RegistryState.LINK_PREVIEW_THICKNESS, colors.previewStart(), colors.previewEnd()); + } + + private static RegistryState.LinkWireColors linkWireColors() { + Themes.Theme theme = Theme.theme(); + if (RegistryState.cachedLinkTheme == theme && RegistryState.cachedLinkColors != null) { + return RegistryState.cachedLinkColors; + } + + int wireStart = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme.textMuted(), theme.strokeSoft(), 0.45f), 0x86); + int wireEnd = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme.accentSoft(), theme.windowStroke(), 0.24f), 0x78); + int previewStart = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme.textPrimary(), theme.accentSoft(), 0.38f), 0xC8); + int previewEnd = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme.accent(), theme.accentSoft(), 0.28f), 0xD8); + + RegistryState.cachedLinkTheme = theme; + RegistryState.cachedLinkColors = new RegistryState.LinkWireColors(wireStart, wireEnd, previewStart, previewEnd); + return RegistryState.cachedLinkColors; + } + + private static void renderLinkWire(Renderer2D renderer, + DraggableHudElement parent, + DraggableHudElement child, + WidgetAnchorSide side, + double thickness, + int startArgb, + int endArgb) { + if (parent == null || child == null || side == null || side == WidgetAnchorSide.NONE) return; + if (parent.getWidth() <= 0f || parent.getHeight() <= 0f || child.getWidth() <= 0f || child.getHeight() <= 0f) { + return; + } + + double parentX = 0.0; + double parentY = 0.0; + double childX = 0.0; + double childY = 0.0; + double parentCx = 0.0; + double parentCy = 0.0; + double childCx = 0.0; + double childCy = 0.0; + + switch (side) { + case TOP -> { + parentX = parent.getX() + parent.getWidth() * 0.5; + parentY = parent.getY(); + childX = child.getX() + child.getWidth() * 0.5; + childY = child.getY() + child.getHeight(); + double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45); + parentCx = parentX; + parentCy = parentY - c; + childCx = childX; + childCy = childY + c; + } + case BOTTOM -> { + parentX = parent.getX() + parent.getWidth() * 0.5; + parentY = parent.getY() + parent.getHeight(); + childX = child.getX() + child.getWidth() * 0.5; + childY = child.getY(); + double c = Math.max(14.0, Math.abs(childY - parentY) * 0.45); + parentCx = parentX; + parentCy = parentY + c; + childCx = childX; + childCy = childY - c; + } + case LEFT -> { + parentX = parent.getX(); + parentY = parent.getY() + parent.getHeight() * 0.5; + childX = child.getX() + child.getWidth(); + childY = child.getY() + child.getHeight() * 0.5; + double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45); + parentCx = parentX - c; + parentCy = parentY; + childCx = childX + c; + childCy = childY; + } + case RIGHT -> { + parentX = parent.getX() + parent.getWidth(); + parentY = parent.getY() + parent.getHeight() * 0.5; + childX = child.getX(); + childY = child.getY() + child.getHeight() * 0.5; + double c = Math.max(14.0, Math.abs(childX - parentX) * 0.45); + parentCx = parentX + c; + parentCy = parentY; + childCx = childX - c; + childCy = childY; + } + case NONE -> { + return; + } + } + + renderer.bezierConnectorGradient(childX, childY, childCx, childCy, parentCx, parentCy, parentX, parentY, + 18, thickness, startArgb, endArgb); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java new file mode 100644 index 0000000..588388f --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java @@ -0,0 +1,171 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.render; + +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.gui.chat.BetterChatRenderer; +import dev.loki.lovisual.features.gui.hud.*; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragController; +import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragSession; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore; +import dev.loki.lovisual.features.module.HudPhase; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.profiler.ProfilerPhase; +import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import net.minecraft.client.Minecraft; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * Пакетный проход движка рендера: отбор и отрисовка всех виджетов фазы + drag-оверлей. + */ +public final class RegistryRenderPipeline { + + private RegistryRenderPipeline() { + } + + public static boolean hasEngineWork(HudPhase phase, boolean foreground) { + if (!Lifecycle.canRunHud()) return false; + if (foreground && RegistryState.dragging != null) return true; + + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (RegistryWidgetStore.isRenderableCandidate(w, phase)) return true; + } + return false; + } + + public static void renderAllEngine(HudPhase phase, + Renderer2D renderer, + TextRenderer textRenderer, + net.minecraft.client.gui.GuiGraphicsExtractor ctx, + float tickDelta) { + if (!Lifecycle.canRunHud()) { + RegistryDragSession.cancelDragging(); + return; + } + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getWindow() == null) return; + boolean allowOnlyModuleList = false; + + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); + int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); + RegistryResizeHandler.handleScreenResize(screenW, screenH); + + List renderList = new ArrayList<>(); + for (DraggableHudElement w : RegistryState.WIDGETS) { + boolean singleEditorPreview = RegistryState.editorWidgetId != null; + if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) { + continue; + } + boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible; + if (!bypassEnabled && !w.isEnabled()) { + continue; + } + if (!w.usesEngineRenderer()) { + continue; + } + if (w.getHudPhase() != phase) { + continue; + } + if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) { + continue; + } + RegistryAnchorLayout.ensurePositionInitialized(w, screenW, screenH); + if (allowOnlyModuleList && !"module_list".equals(w.getId())) { + continue; + } + renderList.add(w); + } + + for (DraggableHudElement w : renderList) { + if (w == RegistryState.dragging) continue; + if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) { + RegistryAnchorLayout.applyScreenAnchor(w, screenW, screenH); + } + } + for (DraggableHudElement w : renderList) { + if (w == RegistryState.dragging) continue; + if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) { + RegistryAnchorLayout.applyAnchors(w, screenW, screenH, new HashSet<>()); + } + } + + for (DraggableHudElement w : renderList) { + RegistryWidgetStore.noteRendered(w); + try (ProfilerPhase.Scope widgetScope = + ProfilerPhase.scope("2d:widget:" + w.getId()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) { + w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + } + } + + public static void renderAllEngineForeground(HudPhase phase, + Renderer2D renderer, + TextRenderer textRenderer, + net.minecraft.client.gui.GuiGraphicsExtractor ctx, + float tickDelta) { + if (!Lifecycle.canRunHud()) { + RegistryDragSession.cancelDragging(); + return; + } + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getWindow() == null) return; + boolean allowOnlyModuleList = false; + + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); + int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); + RegistryResizeHandler.handleScreenResize(screenW, screenH); + + for (DraggableHudElement w : RegistryState.WIDGETS) { + boolean singleEditorPreview = RegistryState.editorWidgetId != null; + if (singleEditorPreview && (RegistryState.editorWidgetId.isEmpty() || !RegistryState.editorWidgetId.equals(w.getId()))) { + continue; + } + boolean bypassEnabled = singleEditorPreview && RegistryState.forceVisible; + if (!bypassEnabled && !w.isEnabled()) { + continue; + } + if (!w.usesEngineRenderer()) { + continue; + } + if (w.getHudPhase() != phase) { + continue; + } + if (w.getRenderSpace() != HudRenderSpace.UNSCALED_LOGICAL) { + continue; + } + if (allowOnlyModuleList && !"module_list".equals(w.getId())) { + continue; + } + try (ProfilerPhase.Scope widgetScope = + ProfilerPhase.scope("2d:widget_fg:" + w.getId()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) { + w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + } + + RegistryOverlayRenderer.renderDragOverlay(renderer, textRenderer, screenW, screenH); + BetterChatRenderer.renderCapturedTooltip(ctx); + if (phase == HudPhase.AFTER_SUBTITLES) { + float uiScale = HudScale.scale(fbw, fbh); + RegistryDragController.handleDragging(mc, screenW, screenH, uiScale); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java new file mode 100644 index 0000000..7383396 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java @@ -0,0 +1,130 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.render; + +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.HudAnchorX; +import dev.loki.lovisual.features.gui.hud.HudAnchorY; +import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; + +/** + * Реакция раскладки на изменение логического размера экрана. + */ +public final class RegistryResizeHandler { + + private RegistryResizeHandler() { + } + + public static void handleScreenResize(int screenW, int screenH) { + if (screenW <= 0 || screenH <= 0) return; + if (RegistryState.lastScreenW <= 0 || RegistryState.lastScreenH <= 0) { + RegistryState.lastScreenW = screenW; + RegistryState.lastScreenH = screenH; + return; + } + if (RegistryState.lastScreenW == screenW && RegistryState.lastScreenH == screenH) return; + + int oldW = RegistryState.lastScreenW; + int oldH = RegistryState.lastScreenH; + RegistryState.lastScreenW = screenW; + RegistryState.lastScreenH = screenH; + + for (DraggableHudElement w : RegistryState.WIDGETS) { + if (!w.isPositionInitialized()) continue; + if (w.getParentId() != null && w.getParentSide() != WidgetAnchorSide.NONE) { + // Parent-anchored widgets will be handled when anchors are applied. + continue; + } + if (w.getWidth() <= 0 || w.getHeight() <= 0) continue; + + boolean changed = false; + if (w.getAnchorX() == HudAnchorX.FREE && w.getAnchorY() == HudAnchorY.FREE) { + changed |= inferScreenAnchors(w, oldW, oldH); + } + + RegistryAnchorLayout.applyScreenAnchor(w, screenW, screenH); + changed |= clampToScreenIfFree(w, screenW, screenH); + + if (changed) { + w.layoutValue().set(w.snapshotLayout()); + w.saveConfig(); + } + } + } + + private static boolean inferScreenAnchors(DraggableHudElement w, int screenW, int screenH) { + float x = w.getX(); + float y = w.getY(); + boolean changed = false; + + HudAnchorX ax = w.getAnchorX(); + float offX = w.getAnchorOffsetX(); + float leftDist = Math.abs(x); + float rightDist = Math.abs((x + w.getWidth()) - screenW); + float centerDistX = Math.abs((x + w.getWidth() * 0.5f) - screenW * 0.5f); + if (leftDist <= RegistryState.SNAP_DIST) { + ax = HudAnchorX.LEFT; + offX = x; + } else if (rightDist <= RegistryState.SNAP_DIST) { + ax = HudAnchorX.RIGHT; + offX = rightDist; + } else if (centerDistX <= RegistryState.SNAP_DIST) { + ax = HudAnchorX.CENTER; + offX = (x + w.getWidth() * 0.5f) - screenW * 0.5f; + } + + HudAnchorY ay = w.getAnchorY(); + float offY = w.getAnchorOffsetY(); + float topDist = Math.abs(y); + float bottomDist = Math.abs((y + w.getHeight()) - screenH); + float centerDistY = Math.abs((y + w.getHeight() * 0.5f) - screenH * 0.5f); + if (topDist <= RegistryState.SNAP_DIST) { + ay = HudAnchorY.TOP; + offY = y; + } else if (bottomDist <= RegistryState.SNAP_DIST) { + ay = HudAnchorY.BOTTOM; + offY = bottomDist; + } else if (centerDistY <= RegistryState.SNAP_DIST) { + ay = HudAnchorY.CENTER; + offY = (y + w.getHeight() * 0.5f) - screenH * 0.5f; + } + + if (ax != w.getAnchorX() || ay != w.getAnchorY() + || offX != w.getAnchorOffsetX() || offY != w.getAnchorOffsetY()) { + w.setAnchors(ax, ay, offX, offY); + changed = true; + } + return changed; + } + + private static boolean clampToScreenIfFree(DraggableHudElement w, int screenW, int screenH) { + boolean changed = false; + float newX = w.getX(); + float newY = w.getY(); + if (w.getAnchorX() == HudAnchorX.FREE) { + float maxX = Math.max(0f, screenW - w.getWidth()); + newX = Math.max(0f, Math.min(newX, maxX)); + } + if (w.getAnchorY() == HudAnchorY.FREE) { + float maxY = Math.max(0f, screenH - w.getHeight()); + newY = Math.max(0f, Math.min(newY, maxY)); + } + if (newX != w.getX() || newY != w.getY()) { + w.moveTo(newX, newY); + if (w.getAnchorX() == HudAnchorX.FREE || w.getAnchorY() == HudAnchorY.FREE) { + w.setAnchors(w.getAnchorX(), w.getAnchorY(), + w.getAnchorX() == HudAnchorX.FREE ? newX : w.getAnchorOffsetX(), + w.getAnchorY() == HudAnchorY.FREE ? newY : w.getAnchorOffsetY()); + } + changed = true; + } + return changed; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java new file mode 100644 index 0000000..a3c4a53 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java @@ -0,0 +1,110 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.render; + +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.HudRenderSpace; +import dev.loki.lovisual.features.gui.hud.WidgetAnchorSide; +import dev.loki.lovisual.features.gui.hud.draggable.interaction.RegistryDragSession; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryAnchorLayout; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; +import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryWidgetStore; +import dev.loki.lovisual.features.module.HudPhase; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.profiler.ProfilerPhase; +import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; + +import java.util.HashSet; + +/** + * Путь отрисовки одиночного виджета по id (превью в редакторе и запросы движка). + */ +public final class RegistryWidgetRenderPath { + + private RegistryWidgetRenderPath() { + } + + public static boolean hasEngineWidgetWork(String id) { + if (!Lifecycle.canRunHud()) return false; + if (id == null || id.isBlank()) return false; + if (RegistryState.editorWidgetId != null && !RegistryState.editorWidgetId.equals(id)) return false; + + DraggableHudElement w = RegistryWidgetStore.findById(id); + if (w == null) return false; + if (!w.usesEngineRenderer()) return false; + boolean bypassEnabled = RegistryState.forceVisible && RegistryState.editorWidgetId != null && RegistryState.editorWidgetId.equals(id); + return bypassEnabled || w.isEnabled(); + } + + public static void renderEngineWidget(String id, + Renderer2D renderer, + TextRenderer textRenderer, + net.minecraft.client.gui.GuiGraphicsExtractor ctx, + float tickDelta) { + if (!Lifecycle.canRunHud()) { + RegistryDragSession.cancelDragging(); + return; + } + if (RegistryState.editorWidgetId != null && (!RegistryState.editorWidgetId.equals(id))) return; + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getWindow() == null) return; + boolean isChat = ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; + boolean isInventory = ClientScreen.current() instanceof net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; + boolean allowOnlyModuleList = isInventory && ClientScreen.current() != null; + if (RegistryState.forceVisible) { + allowOnlyModuleList = false; + } + boolean shouldHideHud = !RegistryState.forceVisible && ClientScreen.current() != null && !isChat && !allowOnlyModuleList; + if (shouldHideHud) { + return; + } + + DraggableHudElement w = RegistryWidgetStore.findById(id); + if (w == null) return; + if (!w.usesEngineRenderer()) return; + boolean bypassEnabled = RegistryState.forceVisible && RegistryState.editorWidgetId != null && RegistryState.editorWidgetId.equals(id); + if (!bypassEnabled && !w.isEnabled()) { + return; + } + if (allowOnlyModuleList && !"module_list".equals(w.getId())) { + return; + } + + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); + int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); + RegistryResizeHandler.handleScreenResize(screenW, screenH); + + RegistryAnchorLayout.ensurePositionInitialized(w, screenW, screenH); + if (w != RegistryState.dragging) { + if (w.getParentId() == null || w.getParentSide() == WidgetAnchorSide.NONE) { + RegistryAnchorLayout.applyScreenAnchor(w, screenW, screenH); + } else { + RegistryAnchorLayout.applyAnchors(w, screenW, screenH, new HashSet<>()); + } + } + + RegistryWidgetStore.noteRendered(w); + try (ProfilerPhase.Scope widgetScope = + ProfilerPhase.scope("2d:widget:" + w.getId()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget:" + w.getId())) { + w.renderEngine(renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + try (ProfilerPhase.Scope widgetScope = + ProfilerPhase.scope("2d:widget_fg:" + w.getId()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("widget_fg:" + w.getId())) { + w.renderEngineForeground(renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java index b0cbf27..dc5d7c6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/CustomHotbar.java @@ -17,6 +17,8 @@ import net.minecraft.util.Util; import net.minecraft.world.item.ItemStack; import dev.loki.lovisual.config.SettingDef; import dev.loki.lovisual.features.gui.hud.AbstractHudElement; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.visuals.Animations; import dev.loki.lovisual.features.gui.hud.HudElementRegister; import dev.loki.lovisual.features.gui.hud.HudRenderUtil; import dev.loki.lovisual.features.theme.Themes; @@ -104,7 +106,7 @@ public final class CustomHotbar extends AbstractHudElement { ItemStack offhand = player.getOffhandItem(); boolean hasOffhand = !offhand.isEmpty(); - float baseY = h - 22f; + float baseY = h - 22f + hotbarRaiseOffset(); boolean blur = shouldBlur(bm); boolean glass = shouldGlass(bm); @@ -219,7 +221,7 @@ public final class CustomHotbar extends AbstractHudElement { if (bm == null || !bm.isHudHotbarEnabled()) return; int i = mc.getWindow().getGuiScaledWidth() / 2; - int baseY = mc.getWindow().getGuiScaledHeight() - 22; + float baseY = mc.getWindow().getGuiScaledHeight() - 22 + hotbarRaiseOffset(); int selected = getSelectedSlot(player); HotbarSelection selection = updateSelectionAnimation(selected); @@ -327,6 +329,16 @@ public final class CustomHotbar extends AbstractHudElement { return ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot(); } + /** + * Vertical GUI-pixel offset applied to the whole hotbar while the Animations + * module slides it into place. The animated value itself is advanced once per + * frame inside the module; this only reads the settled value. + */ + private static float hotbarRaiseOffset() { + Animations animations = Modules.get(Animations.class); + return animations != null ? animations.getHotbarRaiseOffset() : 0.0f; + } + private static OffHandMode getOffHandMode() { CustomHotbar bm = CustomHotbar.get(); if (bm == null) return OffHandMode.MERGED; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java index 824cf5b..1125ee5 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java @@ -10,8 +10,6 @@ package dev.loki.lovisual.features.gui.hud.nondraggable.impl; import dev.loki.lovisual.util.screen.ClientScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.components.BossHealthOverlay; -import net.minecraft.client.gui.components.LerpingBossEvent; import net.minecraft.client.gui.screens.ChatScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.resources.Identifier; @@ -28,11 +26,12 @@ import dev.loki.lovisual.features.gui.hud.AbstractHudElement; import dev.loki.lovisual.features.gui.hud.HudElementRegister; import dev.loki.lovisual.features.gui.hud.HudRenderSpace; import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandBoundsPatches; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandRuntimePatches; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandUiUtil; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls; import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; import dev.loki.lovisual.features.module.HudPhase; -import dev.loki.lovisual.features.module.Modules; -import dev.loki.lovisual.features.module.modules.misc.ClickGui; -import dev.loki.lovisual.mixins.accessors.BossHealthOverlayAccessor; import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.core.RenderPhase; @@ -41,32 +40,30 @@ import dev.loki.lovisual.render.engine.core.ViewportContext; import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiRuntime; import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.UiProjectionMode; import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.UiRenderContext; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.CachedUiScriptRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiBoundsPatchSet; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptColor; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptPatchSet; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptModule; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptModuleHandle; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps; import dev.loki.lovisual.render.engine.text.FontInfo; import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.Lifecycle; import dev.loki.lovisual.util.media.MediaSessionService; -import dev.loki.lovisual.util.media.RepeatMode; import dev.loki.lovisual.util.pvp.PvpState; import java.time.LocalTime; import java.util.List; -import java.util.Map; -import java.util.UUID; import static dev.loki.lovisual.features.theme.Theme.theme; +/** + * Dynamic Island: оркестрация режимов (time/pvp/music/clickgui) и анимаций. + * Патчинг UI-скрипта, bounds-патчи, утилиты и геометрия медиа-контролов + * делегированы в {@code impl/dynamicisland/}. + */ @HudElementRegister(order = 60) public final class DynamicIsland extends AbstractHudElement { private static final String LAYOUT_ID = "lovisual:api/hud/static/dynamic_island"; @@ -83,25 +80,16 @@ public final class DynamicIsland extends AbstractHudElement { private static final float PVP_MAX_WIDTH = 96f; private static final float MUSIC_EXPANDED_WIDTH = 338f; - private static final float CONTEXT_GAP = 7f; - private static final float CONTEXT_CHIP_H = 20f; - private static final float CONTEXT_CHIP_Y = 7.5f; private static final float TIME_CONTEXT_WIDTH = 57f; - private static final float PVP_CONTEXT_WIDTH = 46f; private static final float COMPACT_PAD_X = 11f; private static final float COMPACT_GAP = 9f; private static final float COMPACT_ART = 22f; private static final float COMPACT_WAVE_W = 15f; private static final float COMPACT_FADE = 15f; - private static final float EXPANDED_PAD_X = 13f; private static final float EXPANDED_PAD_Y = 11f; - private static final float EXPANDED_ART = 36f; private static final float PROGRESS_H = 5f; - private static final float CONTROL_SIZE = 27f; - private static final float CONTROL_GAP = 7f; private static final float CONTROL_TIME_GAP = 10f; - private static final float CONTROL_ROW_GAP_Y = -5f; private static final int ICON_PAUSE = 0xEA02; private static final int ICON_PLAY = 0xEA03; @@ -109,8 +97,6 @@ public final class DynamicIsland extends AbstractHudElement { private static final int ICON_NEXT = 0xEA05; private static final int ICON_SHUFFLE = 0xEA06; - private static final int BASE_SHADOW = 0xFF000000; - private static final int BASE_STROKE = 0xFFFFFFFF; private static final int STATUS_PVP = 0xFFFF6368; private static final int PROGRESS_BG = 0x2AFFFFFF; @@ -127,6 +113,7 @@ public final class DynamicIsland extends AbstractHudElement { private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); private final CachedUiScriptRuntime scriptRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); + private final IslandControls controls = new IslandControls(); private float visibilityAnim; private float expandAnim; @@ -143,23 +130,6 @@ public final class DynamicIsland extends AbstractHudElement { private IslandMode currentMode = IslandMode.TIME; private MediaSessionService.Snapshot currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); - private float shuffleX; - private float shuffleY; - private float prevX; - private float prevY; - private float playX; - private float playY; - private float nextX; - private float nextY; - private float repeatX; - private float repeatY; - private float controlSize; - private float shuffleHover; - private float prevHover; - private float playHover; - private float nextHover; - private float repeatHover; - private int uiFillTop; private int uiFillBottom; private int uiTextPrimary; @@ -185,206 +155,6 @@ public final class DynamicIsland extends AbstractHudElement { return INSTANCE.clickGuiShellVisible; } - private static void putControlBounds(UiBoundsPatchSet patches, - String key, - float x, - float y, - float size) { - putBounds(patches, key + ":hover", x, y, size, size); - putBounds(patches, key, x, y + 4.0f, size, Math.max(0.0f, size - 4.0f)); - } - - private static void putSvgControlBounds(UiBoundsPatchSet patches, - String key, - float x, - float y, - float size) { - float iconSize = Math.max(0.0f, size * 0.74f); - putBounds(patches, key + ":hover", x, y, size, size); - putBounds(patches, key, x + (size - iconSize) * 0.5f, y + (size - iconSize) * 0.5f, iconSize, iconSize); - } - - private static void putBounds(UiBoundsPatchSet patches, - String key, - float x, - float y, - float width, - float height) { - patches.put(key, x, y, width, height); - } - - private static void patchControl(UiScriptPatchSet patches, - String key, - String icon, - String iconColor, - String accentSoft, - float expandedAlpha, - float hover) { - patchText(patches, key, icon, scaleHexAlpha(iconColor, expandedAlpha)); - patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha)); - } - - private static void patchSvgControl(UiScriptPatchSet patches, - String key, - String asset, - String iconColor, - String accentSoft, - float expandedAlpha, - float hover) { - patchImage(patches, key, asset, scaleHexAlpha(iconColor, expandedAlpha)); - patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha)); - } - - private static void patchText(UiScriptPatchSet patches, - String key, - String text, - String color) { - patches.text(key, text, color); - } - - private static void patchClippedText(UiScriptPatchSet patches, - String key, - float measuredWidth, - float boxWidth, - float scrollTime, - float delay, - float speed, - float fadeWidth) { - patches.clippedText(key, measuredWidth, boxWidth, scrollTime, delay, speed, fadeWidth); - } - - private static void patchImage(UiScriptPatchSet patches, - String key, - String asset, - String tint) { - patches.image(key, asset, tint); - } - - private static void patchShape(UiScriptPatchSet patches, - String key, - Object... pairs) { - patches.props(key, pairs); - } - - private static void patchShape(UiScriptPatchSet patches, - String key, - String prop, - Object value) { - patches.put(key, prop, value); - } - - private static void patchShape(UiScriptPatchSet patches, - String key, - String propA, - Object valueA, - String propB, - Object valueB) { - patches.put(key, propA, valueA, propB, valueB); - } - - private static String stringProp(UiScriptProps props, String key, String fallback) { - return props.string(key, fallback); - } - - private static float numberProp(UiScriptProps props, String key, float fallback) { - return props.number(key, fallback); - } - - private static boolean boolProp(UiScriptProps props, String key, boolean fallback) { - return props.bool(key, fallback); - } - - private static float clamp01(float value) { - return Math.max(0.0f, Math.min(1.0f, value)); - } - - private static String scaleHexAlpha(String hex, float amount) { - return UiScriptColor.alpha(hex, amount); - } - - private static int shuffleColor(MediaSessionService.Snapshot snapshot) { - if (snapshot == null || !snapshot.supportsShuffle()) { - return 0x99000000; - } - return snapshot.isShuffleActive() ? 0xFFFFFFFF : 0x7AB7BEC8; - } - - private static int repeatColor(MediaSessionService.Snapshot snapshot) { - if (snapshot == null || !snapshot.supportsRepeat()) { - return 0x99000000; - } - RepeatMode mode = snapshot.repeatMode(); - return mode == RepeatMode.ALL || mode == RepeatMode.ONE ? 0xFFFFFFFF : 0x7AB7BEC8; - } - - private static String repeatSvg(MediaSessionService.Snapshot snapshot) { - if (snapshot == null || !snapshot.supportsRepeat()) { - return "repeat-off"; - } - return snapshot.repeatMode() == RepeatMode.ONE ? "repeat-1" : "repeat"; - } - - private static boolean clickGuiEnabled() { - ClickGui clickGui = Modules.get(ClickGui.class); - return clickGui != null && clickGui.isEnabled(); - } - - private static boolean isSettingsLikeScreen(Screen screen) { - if (screen == null) return false; - String name = screen.getClass().getName(); - String simple = screen.getClass().getSimpleName(); - if (name.contains(".gui.screen.option.") || name.contains(".gui.screen.pack.")) return true; - return simple.contains("Options") - || simple.contains("Option") - || simple.contains("Controls") - || simple.contains("Keybind") - || simple.contains("Video") - || simple.contains("Sound") - || simple.contains("Language") - || simple.contains("Accessibility") - || simple.contains("Skin") - || simple.contains("Pack") - || simple.contains("Datapack") - || simple.contains("Customize") - || simple.contains("Telemetry") - || simple.contains("Credits"); - } - - private static String formatClock(LocalTime time) { - return String.format("%02d:%02d", time.getHour(), time.getMinute()); - } - - private static String formatSecondsOnly(float seconds) { - return Math.max(0L, (long) Math.ceil(Math.max(0f, seconds))) + "s"; - } - - private static String formatShortTime(float seconds) { - return formatShortTime((long) Math.ceil(Math.max(0f, seconds))); - } - - private static String formatShortTime(long seconds) { - long sec = Math.max(0L, seconds); - long m = sec / 60L; - long s = sec % 60L; - return String.format("%d:%02d", m, s); - } - - private static String iconString(int codepoint) { - return new String(Character.toChars(codepoint)); - } - - private static String hex(int argb) { - return String.format("#%08X", argb); - } - - private static float clamp(float value, float min, float max) { - return Math.max(min, Math.min(max, value)); - } - - private static boolean hit(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - @Override protected void defineSettings(List defs) { defs.add(SettingDef.bool(syncTheme)); @@ -429,24 +199,24 @@ public final class DynamicIsland extends AbstractHudElement { if (currentMode == IslandMode.MUSIC && expandAnim > 0.01f && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { if (currentSnapshot != null && currentSnapshot.supportsShuffle() - && hit(mx, my, shuffleX, shuffleY, controlSize, controlSize)) { + && IslandUiUtil.hit(mx, my, controls.shuffleX, controls.shuffleY, controls.controlSize, controls.controlSize)) { mediaService.toggleShuffle(); return true; } - if (hit(mx, my, prevX, prevY, controlSize, controlSize)) { + if (IslandUiUtil.hit(mx, my, controls.prevX, controls.prevY, controls.controlSize, controls.controlSize)) { mediaService.previous(); return true; } - if (hit(mx, my, playX, playY, controlSize, controlSize)) { + if (IslandUiUtil.hit(mx, my, controls.playX, controls.playY, controls.controlSize, controls.controlSize)) { mediaService.playPause(); return true; } - if (hit(mx, my, nextX, nextY, controlSize, controlSize)) { + if (IslandUiUtil.hit(mx, my, controls.nextX, controls.nextY, controls.controlSize, controls.controlSize)) { mediaService.next(); return true; } if (currentSnapshot != null && currentSnapshot.supportsRepeat() - && hit(mx, my, repeatX, repeatY, controlSize, controlSize)) { + && IslandUiUtil.hit(mx, my, controls.repeatX, controls.repeatY, controls.controlSize, controls.controlSize)) { mediaService.cycleRepeatMode(); return true; } @@ -474,7 +244,7 @@ public final class DynamicIsland extends AbstractHudElement { if (!Lifecycle.canRunHud()) return; if (mc == null || mc.getWindow() == null || ctx == null) return; Screen currentScreen = ClientScreen.current(); - boolean clickGuiOverlay = isEnabled() && clickGuiEnabled(); + boolean clickGuiOverlay = isEnabled() && IslandUiUtil.clickGuiEnabled(); if (!clickGuiOverlay && !isScreenOverlayAllowed(currentScreen)) return; FullScreenRenderer.ensureInit(); @@ -513,7 +283,7 @@ public final class DynamicIsland extends AbstractHudElement { Screen currentScreen = ClientScreen.current(); boolean chatOpen = currentScreen instanceof ChatScreen; - boolean clickGuiVisible = clickGuiEnabled(); + boolean clickGuiVisible = IslandUiUtil.clickGuiEnabled(); boolean clickGuiTabShellScreen = currentScreen instanceof ClickGuiScreen; ClickGuiRenderer.ClickGuiIslandState clickGuiState = ClickGuiRenderer.islandState(); boolean clickGuiBridge = isEnabled() @@ -525,7 +295,7 @@ public final class DynamicIsland extends AbstractHudElement { if (clickGuiVisible && clickGuiTabShellScreen && !clickGuiBridge) { clickGuiShellVisible = false; setBounds(0f, 0f, 0f, 0f); - resetControls(); + controls.reset(); return; } boolean shouldShow = clickGuiBridge @@ -547,7 +317,7 @@ public final class DynamicIsland extends AbstractHudElement { if (!shouldShow && visibilityAnim <= 0.01f) { clickGuiShellVisible = false; setBounds(0f, 0f, 0f, 0f); - resetControls(); + controls.reset(); return; } @@ -560,7 +330,7 @@ public final class DynamicIsland extends AbstractHudElement { IslandMetrics metrics = measureMetrics(titleRenderer, valueRenderer, metaRenderer); float targetMainWidth = switch (currentMode) { case TIME -> TIME_WIDTH; - case PVP -> clamp(metrics.pvpWidth, PVP_MIN_WIDTH, PVP_MAX_WIDTH); + case PVP -> IslandUiUtil.clamp(metrics.pvpWidth, PVP_MIN_WIDTH, PVP_MAX_WIDTH); case MUSIC -> AnimationUtility.lerp(metrics.musicCompactWidth, MUSIC_EXPANDED_WIDTH, expandAnim); case CLICKGUI -> clickGuiState.tabBarW(); }; @@ -572,7 +342,7 @@ public final class DynamicIsland extends AbstractHudElement { boolean clickGuiContext = currentMode == IslandMode.CLICKGUI; float contextPresence = musicContext ? 1.0f - AnimationUtility.easeOutCubic(expandAnim) : (clickGuiContext ? 1.0f : 0.0f); float targetLeftContextWidth = TIME_CONTEXT_WIDTH * contextPresence; - float targetRightContextWidth = PvpState.isActive() ? PVP_CONTEXT_WIDTH * contextPresence : 0.0f; + float targetRightContextWidth = PvpState.isActive() ? IslandBoundsPatches.PVP_CONTEXT_WIDTH * contextPresence : 0.0f; mainWidthAnim = mainWidthAnim < 0f ? targetMainWidth : AnimationUtility.approach(mainWidthAnim, targetMainWidth, dt, 12f); heightAnim = heightAnim < 0f ? targetHeight : AnimationUtility.approach(heightAnim, targetHeight, dt, 12f); @@ -585,8 +355,8 @@ public final class DynamicIsland extends AbstractHudElement { float drawMainWidth = mainWidthAnim; float drawHeight = heightAnim; - float leftOccupied = leftContextWidthAnim > 0.5f ? leftContextWidthAnim + CONTEXT_GAP : 0.0f; - float rightOccupied = rightContextWidthAnim > 0.5f ? CONTEXT_GAP + rightContextWidthAnim : 0.0f; + float leftOccupied = leftContextWidthAnim > 0.5f ? leftContextWidthAnim + IslandBoundsPatches.CONTEXT_GAP : 0.0f; + float rightOccupied = rightContextWidthAnim > 0.5f ? IslandBoundsPatches.CONTEXT_GAP + rightContextWidthAnim : 0.0f; float drawWidth = leftOccupied + drawMainWidth + rightOccupied; widthAnim = drawWidth; float drawX = currentMode == IslandMode.CLICKGUI @@ -594,7 +364,7 @@ public final class DynamicIsland extends AbstractHudElement { : (screenW - drawMainWidth) * 0.5f - leftOccupied; float drawY = currentMode == IslandMode.CLICKGUI ? clickGuiState.tabBarY() - (1.0f - AnimationUtility.easeOutCubic(clickGuiState.lifecycle())) * 18.0f - : TOP_Y + bossBarOffset(screenH); + : TOP_Y + IslandUiUtil.bossBarOffset(mc, screenH, TOP_Y); float mainX = leftOccupied; float alpha = AnimationUtility.easeOutCubic(visibilityAnim); float radius = currentMode == IslandMode.CLICKGUI ? drawHeight * 0.5f : AnimationUtility.lerp(CLOSED_RADIUS, EXPANDED_RADIUS, expandAnim); @@ -605,16 +375,16 @@ public final class DynamicIsland extends AbstractHudElement { float expandedContentAlpha = currentMode == IslandMode.MUSIC ? AnimationUtility.easeOutCubic(expandAnim) * alpha : 0f; float compactContentAlpha = currentMode == IslandMode.MUSIC ? (1f - AnimationUtility.easeOutCubic(expandAnim)) * alpha : 0f; if (expandedContentAlpha > 0.01f) { - updateMusicControls(drawX + mainX, drawY, drawMainWidth, valueRenderer); - updateControlHover(isInteractiveScreen(currentScreen)); + controls.updateLayout(drawX + mainX, drawY, drawMainWidth, valueRenderer, currentSnapshot); + controls.updateHover(isInteractiveScreen(currentScreen), mc, currentSnapshot); } else { - resetControls(); + controls.reset(); } if (!renderScripted(renderer, fallback, titleRenderer, metaRenderer, ctx, tickDelta, screenW, screenH, drawWidth, drawHeight, drawMainWidth, mainX, leftContextWidthAnim, rightContextWidthAnim, alpha, compactContentAlpha, expandedContentAlpha, radius, accent)) { - drawFallback(renderer, drawX + mainX, drawY, drawMainWidth, drawHeight, radius, alpha, accent); + IslandUiUtil.drawFallback(renderer, drawX + mainX, drawY, drawMainWidth, drawHeight, radius, alpha, accent, uiFillTop); } } @@ -647,7 +417,7 @@ public final class DynamicIsland extends AbstractHudElement { alpha, compactAlpha, expandedAlpha, radius, accent, titleRenderer, metaRenderer ); - long treeSignature = islandStructuralSignature(props); + long treeSignature = IslandRuntimePatches.islandStructuralSignature(props); long dataSignature = CachedUiScriptRuntime.signature(props.asMap()); long layoutSignature = treeSignature; UiRuntime baked = scriptRuntime.bake( @@ -665,223 +435,14 @@ public final class DynamicIsland extends AbstractHudElement { width, height, props::asMap, - () -> islandRuntimePatches(props), - () -> islandBoundsPatches(props) + () -> IslandRuntimePatches.islandRuntimePatches(props), + () -> IslandBoundsPatches.islandBoundsPatches(props, x, y, controls) ); if (baked == null) return false; baked.render(new UiRenderContext(renderer, textRenderer, ctx, tickDelta, UiProjectionMode.UNSCALED_LOGICAL)); return true; } - private long islandStructuralSignature(UiScriptProps props) { - long h = 0xcbf29ce484222325L; - h = CachedUiScriptRuntime.mix(h, stringProp(props, "mode", "")); - h = CachedUiScriptRuntime.mix(h, boolProp(props, "blur", false)); - h = CachedUiScriptRuntime.mix(h, numberProp(props, "blurAlpha", 0.0f)); - h = CachedUiScriptRuntime.mix(h, !stringProp(props, "artworkTexture", "").isEmpty()); - h = CachedUiScriptRuntime.mix(h, "music".equals(stringProp(props, "mode", ""))); - h = CachedUiScriptRuntime.mix(h, boolProp(props, "showShuffle", false)); - h = CachedUiScriptRuntime.mix(h, boolProp(props, "showRepeat", false)); - return h; - } - - private Map> islandRuntimePatches(UiScriptProps props) { - UiScriptPatchSet patches = UiScriptPatchSet.create(48); - float compactAlpha = numberProp(props, "compactAlpha", 0.0f); - float expandedAlpha = numberProp(props, "expandedAlpha", 0.0f); - float contextAlpha = numberProp(props, "contextAlpha", compactAlpha); - float width = numberProp(props, "width", 0.0f); - float mainWidth = numberProp(props, "mainWidth", width); - float progress = clamp01(numberProp(props, "progress", 0.0f)); - String mode = stringProp(props, "mode", ""); - boolean musicMode = "music".equals(mode); - boolean clickGuiMode = "clickgui".equals(mode); - boolean shellMode = musicMode || clickGuiMode; - boolean pvpMode = "pvp".equals(mode); - boolean timeMode = "time".equals(mode); - String primary = stringProp(props, "textPrimary", "#FFFFFFFF"); - String secondary = stringProp(props, "textSecondary", "#99FFFFFF"); - String muted = stringProp(props, "textMuted", "#66FFFFFF"); - String accent = stringProp(props, "accent", "#FFFFFFFF"); - String accentSoft = stringProp(props, "accentSoft", "#33FFFFFF"); - - if (shellMode) { - float shellBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * numberProp(props, "alpha", 0.0f); - patchShape(patches, "dynamic-island", "renderRadius", props.get("radius"), "renderBlurAlpha", shellBlurAlpha); - patchShape(patches, "shell:blur", "blurAlpha", shellBlurAlpha, "radius", props.get("radius")); - patchShape(patches, "shell:shadow", "fill", props.get("shadow"), "radius", props.get("radius")); - patchShape(patches, "shell:fill", "startColor", props.get("fillTop"), "endColor", props.get("fillBottom"), "radius", props.get("radius")); - patchShape(patches, "shell:tint", "startColor", props.get("tintTop"), "endColor", props.get("tintBottom"), "radius", props.get("radius")); - patchShape(patches, "shell:stroke", "stroke", props.get("stroke"), "radius", props.get("radius")); - } - - if (timeMode) { - patchShape(patches, "time:box", "fill", props.get("timeFill"), "stroke", props.get("timeStroke")); - patchText(patches, "time", stringProp(props, "time", ""), primary); - } - - if (pvpMode) { - patchShape(patches, "pvp:chip", "fill", props.get("pvpFill"), "stroke", props.get("pvpStroke")); - patchText(patches, "pvp:timer", stringProp(props, "pvpTimer", "0s"), primary); - } - - if (musicMode || clickGuiMode) { - float contextBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * contextAlpha; - patchShape(patches, "context:time:blur", "blurAlpha", contextBlurAlpha, "radius", 10.0f); - patchShape(patches, "context:time:box", "fill", scaleHexAlpha(stringProp(props, "contextFill", "#00000000"), contextAlpha), "stroke", scaleHexAlpha(stringProp(props, "contextStroke", "#00000000"), contextAlpha)); - patchText(patches, "context:time", stringProp(props, "time", ""), scaleHexAlpha(primary, contextAlpha)); - float pvpContextAlpha = boolProp(props, "pvpContextVisible", false) ? contextAlpha : 0.0f; - patchShape(patches, "context:pvp:blur", "blurAlpha", numberProp(props, "blurAlpha", 0.0f) * pvpContextAlpha, "radius", 10.0f); - patchShape(patches, "context:pvp:box", "fill", scaleHexAlpha(stringProp(props, "pvpFill", "#00000000"), pvpContextAlpha), "stroke", scaleHexAlpha(stringProp(props, "pvpStroke", "#00000000"), pvpContextAlpha)); - patchText(patches, "context:pvp", stringProp(props, "pvpTimer", ""), scaleHexAlpha(primary, pvpContextAlpha)); - } - - if (musicMode) { - patchText(patches, "music:elapsed:compact", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, compactAlpha)); - patchText(patches, "music:title:compact", stringProp(props, "title", ""), scaleHexAlpha(primary, compactAlpha)); - patchClippedText(patches, "music:title:compact", numberProp(props, "titleWidthCompact", 0.0f), Math.max(0.0f, mainWidth - 124.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 15.0f); - patchShape(patches, "music:divider", "fill", scaleHexAlpha(muted, 0.4f * compactAlpha)); - patchShape(patches, "wave:0", "fill", scaleHexAlpha(accent, 0.75f * compactAlpha)); - patchShape(patches, "wave:1", "fill", scaleHexAlpha(accent, 0.83f * compactAlpha)); - patchShape(patches, "wave:2", "fill", scaleHexAlpha(accent, 0.91f * compactAlpha)); - patchImage(patches, "artwork:compact", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", compactAlpha)); - patchShape(patches, "artwork:compact:fallback", "fill", scaleHexAlpha(accent, 0.92f * compactAlpha), "stroke", scaleHexAlpha("#33FFFFFF", compactAlpha)); - } - - if (musicMode) { - patchText(patches, "music:title:expanded", stringProp(props, "title", ""), scaleHexAlpha(primary, expandedAlpha)); - patchClippedText(patches, "music:title:expanded", numberProp(props, "titleWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 16.0f); - patchText(patches, "music:artist:expanded", stringProp(props, "artist", ""), scaleHexAlpha(secondary, expandedAlpha)); - patchClippedText(patches, "music:artist:expanded", numberProp(props, "artistWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.4f, 14.0f, 16.0f); - patchShape(patches, "music:progress:bg", "fill", scaleHexAlpha(stringProp(props, "progressBg", "#00FFFFFF"), expandedAlpha)); - patchShape(patches, "music:progress:fill", - "fill", scaleHexAlpha(accent, expandedAlpha), - "renderWidth", Math.max(0.0f, mainWidth - 26.0f) * progress); - patchText(patches, "music:elapsed:expanded", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, expandedAlpha)); - patchText(patches, "music:total:expanded", stringProp(props, "total", "0:00"), scaleHexAlpha(secondary, expandedAlpha)); - patchImage(patches, "artwork:expanded", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", expandedAlpha)); - patchShape(patches, "artwork:expanded:fallback", "fill", scaleHexAlpha(accent, 0.92f * expandedAlpha), "stroke", scaleHexAlpha("#33FFFFFF", expandedAlpha)); - if (boolProp(props, "showShuffle", false)) { - patchControl(patches, "music:shuffle", stringProp(props, "iconShuffle", ""), stringProp(props, "shuffleColor", primary), accentSoft, expandedAlpha, numberProp(props, "shuffleHover", 0.0f)); - } - patchControl(patches, "music:prev", stringProp(props, "iconPrev", ""), primary, accentSoft, expandedAlpha, numberProp(props, "prevHover", 0.0f)); - patchControl(patches, "music:play", stringProp(props, "iconPlay", ""), primary, accentSoft, expandedAlpha, numberProp(props, "playHover", 0.0f)); - patchControl(patches, "music:next", stringProp(props, "iconNext", ""), primary, accentSoft, expandedAlpha, numberProp(props, "nextHover", 0.0f)); - if (boolProp(props, "showRepeat", false)) { - patchSvgControl(patches, "music:repeat", stringProp(props, "repeatAsset", "repeat-off"), stringProp(props, "repeatColor", "#99000000"), accentSoft, expandedAlpha, numberProp(props, "repeatHover", 0.0f)); - } - } - - return patches.asMap(); - } - - private Map islandBoundsPatches(UiScriptProps props) { - UiBoundsPatchSet patches = UiBoundsPatchSet.create(48); - float rootX = x; - float rootY = y; - float width = numberProp(props, "width", 0.0f); - float height = numberProp(props, "height", 0.0f); - float mainX = numberProp(props, "mainX", 0.0f); - float mainWidth = numberProp(props, "mainWidth", width); - float leftContextWidth = numberProp(props, "leftContextWidth", 0.0f); - float rightContextWidth = numberProp(props, "rightContextWidth", 0.0f); - String mode = stringProp(props, "mode", ""); - boolean musicMode = "music".equals(mode); - boolean clickGuiMode = "clickgui".equals(mode); - - putBounds(patches, "dynamic-island", rootX, rootY, width, height); - putBounds(patches, "content", rootX, rootY, width, height); - - if (musicMode || clickGuiMode) { - putBounds(patches, "shell:blur", rootX + mainX, rootY, mainWidth, height); - putBounds(patches, "shell:shadow", rootX + mainX, rootY, mainWidth, height); - putBounds(patches, "shell:fill", rootX + mainX, rootY, mainWidth, height); - putBounds(patches, "shell:tint", rootX + mainX, rootY, mainWidth, height); - putBounds(patches, "shell:stroke", rootX + mainX, rootY, mainWidth, height); - } - - if (musicMode || clickGuiMode) { - putBounds(patches, "context:time:blur", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H); - putBounds(patches, "context:time:box", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H); - putBounds(patches, "context:time", rootX, rootY + 10.1f, leftContextWidth, 14.0f); - float rightX = mainX + mainWidth + CONTEXT_GAP; - putBounds(patches, "context:pvp:blur", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H); - putBounds(patches, "context:pvp:box", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H); - putBounds(patches, "context:pvp", rootX + rightX, rootY + 10.1f, rightContextWidth, 14.0f); - } - - if (musicMode) { - putMusicCompactBounds(patches, rootX, rootY, props, mainX, mainWidth); - putMusicExpandedBounds(patches, rootX, rootY, props, mainWidth); - } else if ("pvp".equals(mode)) { - float chipW = Math.min(mainWidth - 16.0f, Math.max(42.0f, PVP_CONTEXT_WIDTH + 8.0f)); - float chipX = mainX + (mainWidth - chipW) * 0.5f; - putBounds(patches, "pvp:chip", rootX + chipX, rootY + CONTEXT_CHIP_Y, chipW, CONTEXT_CHIP_H); - putBounds(patches, "pvp:timer", rootX + chipX, rootY + 10.1f, chipW, 14.0f); - } else { - putBounds(patches, "time:box", rootX + mainX, rootY + CONTEXT_CHIP_Y, mainWidth, CONTEXT_CHIP_H); - putBounds(patches, "time", rootX + mainX, rootY + 7.8f, mainWidth, 20.0f); - } - - return patches.asMap(); - } - - private void putMusicCompactBounds(UiBoundsPatchSet patches, - float rootX, - float rootY, - UiScriptProps props, - float mainX, - float width) { - float waveX = mainX + width - 26.0f; - putBounds(patches, "music:elapsed:compact", rootX + mainX + 11.0f, rootY + 10.5f, 38.0f, 13.0f); - putBounds(patches, "music:divider", rootX + mainX + 49.0f, rootY + 7.0f, 1.0f, 18.0f); - putBounds(patches, "artwork:compact", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f); - putBounds(patches, "artwork:compact:fallback", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f); - float titleW = Math.max(0.0f, width - 124.0f); - putBounds(patches, "music:title:compact:clip", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f); - putBounds(patches, "music:title:compact", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f); - - float centerY = 17.5f; - float t = Util.getMillis() / 1000.0f; - boolean playing = boolProp(props, "playing", false); - for (int i = 0; i < 3; i++) { - float barH = playing - ? (float) (3.0f + Math.abs(Math.sin(t * 5.2f + i * 0.85f)) * 8.0f) - : 4.0f + i; - putBounds(patches, "wave:" + i, rootX + waveX + i * 5.0f, rootY + centerY - barH * 0.5f, 2.5f, barH); - } - } - - private void putMusicExpandedBounds(UiBoundsPatchSet patches, - float rootX, - float rootY, - UiScriptProps props, - float width) { - float mainX = numberProp(props, "mainX", 0.0f); - float progressW = Math.max(0.0f, width - 26.0f); - putBounds(patches, "artwork:expanded", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f); - putBounds(patches, "artwork:expanded:fallback", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f); - float titleW = Math.max(0.0f, width - 72.0f); - putBounds(patches, "music:title:expanded:clip", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f); - putBounds(patches, "music:title:expanded", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f); - putBounds(patches, "music:artist:expanded:clip", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f); - putBounds(patches, "music:artist:expanded", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f); - putBounds(patches, "music:progress:bg", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f); - putBounds(patches, "music:progress:fill", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f); - putBounds(patches, "music:elapsed:expanded", rootX + mainX + 13.0f, rootY + 65.0f, 44.0f, 14.0f); - putBounds(patches, "music:total:expanded", rootX + mainX + width - 57.0f, rootY + 65.0f, 44.0f, 14.0f); - - if (boolProp(props, "showShuffle", false)) { - putControlBounds(patches, "music:shuffle", shuffleX, shuffleY, controlSize); - } - putControlBounds(patches, "music:prev", prevX, prevY, controlSize); - putControlBounds(patches, "music:play", playX, playY, controlSize); - putControlBounds(patches, "music:next", nextX, nextY, controlSize); - if (boolProp(props, "showRepeat", false)) { - putSvgControlBounds(patches, "music:repeat", repeatX, repeatY, controlSize); - } - } - private UiScriptProps buildProps(int screenW, int screenH, float width, @@ -909,7 +470,7 @@ public final class DynamicIsland extends AbstractHudElement { props.put("mainX", mainX); props.put("leftContextWidth", leftContextWidth); props.put("rightContextWidth", rightContextWidth); - props.put("contextGap", CONTEXT_GAP); + props.put("contextGap", IslandBoundsPatches.CONTEXT_GAP); boolean pvpContextVisible = (currentMode == IslandMode.MUSIC || currentMode == IslandMode.CLICKGUI) && PvpState.isActive() && rightContextWidth > 0.5f; props.put("pvpContextVisible", pvpContextVisible); props.put("contextAlpha", currentMode == IslandMode.MUSIC ? compactAlpha : (currentMode == IslandMode.CLICKGUI ? alpha : 0.0f)); @@ -922,70 +483,70 @@ public final class DynamicIsland extends AbstractHudElement { props.put("blur", blur.get()); props.put("blurAlpha", blurAlpha.get() / 255.0f); props.put("radius", radius); - props.put("time", formatClock(LocalTime.now())); - props.put("pvpTimer", pvpContextVisible || currentMode == IslandMode.PVP ? formatSecondsOnly(predictedPvpSeconds()) : ""); + props.put("time", IslandUiUtil.formatClock(LocalTime.now())); + props.put("pvpTimer", pvpContextVisible || currentMode == IslandMode.PVP ? IslandUiUtil.formatSecondsOnly(predictedPvpSeconds()) : ""); ClickGuiRenderer.ClickGuiIslandState clickGuiState = ClickGuiRenderer.islandState(); props.put("clickGuiLabel", clickGuiState.activeLabel()); props.put("clickGuiTabCount", clickGuiState.tabCount()); props.put("clickGuiPicker", clickGuiState.pickerActive()); props.put("title", title); props.put("artist", artist); - props.put("titleWidthCompact", measureWidth(titleRenderer, title, 0.82f)); - props.put("titleWidthExpanded", measureWidth(titleRenderer, title, 0.86f)); - props.put("artistWidthExpanded", measureWidth(metaRenderer, artist, 0.72f)); + props.put("titleWidthCompact", IslandUiUtil.measureWidth(titleRenderer, title, 0.82f)); + props.put("titleWidthExpanded", IslandUiUtil.measureWidth(titleRenderer, title, 0.86f)); + props.put("artistWidthExpanded", IslandUiUtil.measureWidth(metaRenderer, artist, 0.72f)); props.put("titleScrollTime", titleScrollSeconds(title, artist, duration)); - props.put("elapsed", formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L)); - props.put("total", formatShortTime(currentSnapshot != null ? currentSnapshot.durationSeconds() : 0L)); + props.put("elapsed", IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L)); + props.put("total", IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.durationSeconds() : 0L)); props.put("progress", currentSnapshot != null && currentSnapshot.durationSeconds() > 0 ? AnimationUtility.clamp((float) currentSnapshot.predictedPositionSeconds() / (float) currentSnapshot.durationSeconds(), 0f, 1f) : 0f); props.put("playing", currentSnapshot != null && currentSnapshot.isPlaying()); Identifier artwork = currentSnapshot != null ? currentSnapshot.artworkTexture() : null; props.put("artworkTexture", artwork != null ? artwork.toString() : ""); - props.put("fillTop", hex(HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha))); - props.put("fillBottom", hex(HudRenderUtil.scaleAlpha(uiFillBottom, 0.88f * alpha))); - props.put("tintTop", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.16f * alpha))); - props.put("tintBottom", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF000000, 0.25f), 0.112f * alpha))); - props.put("shadow", hex(HudRenderUtil.scaleAlpha(BASE_SHADOW, 0.28f * alpha))); - props.put("stroke", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(syncTheme.get() ? theme().windowStroke() : BASE_STROKE, accent, 0.18f), 0.18f * alpha))); - props.put("highlight", hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.08f * alpha))); - props.put("contextFill", hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha))); - props.put("contextStroke", hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); - props.put("timeFill", hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.48f * alpha))); - props.put("timeStroke", hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); - props.put("pvpFill", hex(HudRenderUtil.scaleAlpha(STATUS_PVP, 0.86f * alpha))); - props.put("pvpStroke", hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(STATUS_PVP, 0xFFFFFFFF, 0.32f), 0.34f * alpha))); - props.put("accent", hex(HudRenderUtil.scaleAlpha(accent, alpha))); - props.put("accentSoft", hex(HudRenderUtil.scaleAlpha(uiAccentSoft, alpha))); - props.put("textPrimary", hex(HudRenderUtil.scaleAlpha(uiTextPrimary, alpha))); - props.put("textSecondary", hex(HudRenderUtil.scaleAlpha(uiTextSecondary, alpha))); - props.put("textMuted", hex(HudRenderUtil.scaleAlpha(uiTextMuted, alpha))); - props.put("progressBg", hex(HudRenderUtil.scaleAlpha(PROGRESS_BG, expandedAlpha))); - props.put("shuffleX", shuffleX - x); - props.put("shuffleY", shuffleY - y); - props.put("prevX", prevX - x); - props.put("prevY", prevY - y); - props.put("playX", playX - x); - props.put("playY", playY - y); - props.put("nextX", nextX - x); - props.put("nextY", nextY - y); - props.put("repeatX", repeatX - x); - props.put("repeatY", repeatY - y); - props.put("controlSize", controlSize); + props.put("fillTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha))); + props.put("fillBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillBottom, 0.88f * alpha))); + props.put("tintTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.16f * alpha))); + props.put("tintBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF000000, 0.25f), 0.112f * alpha))); + props.put("shadow", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(IslandUiUtil.BASE_SHADOW, 0.28f * alpha))); + props.put("stroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(syncTheme.get() ? theme().windowStroke() : IslandUiUtil.BASE_STROKE, accent, 0.18f), 0.18f * alpha))); + props.put("highlight", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.08f * alpha))); + props.put("contextFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha))); + props.put("contextStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); + props.put("timeFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.48f * alpha))); + props.put("timeStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); + props.put("pvpFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(STATUS_PVP, 0.86f * alpha))); + props.put("pvpStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(STATUS_PVP, 0xFFFFFFFF, 0.32f), 0.34f * alpha))); + props.put("accent", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(accent, alpha))); + props.put("accentSoft", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiAccentSoft, alpha))); + props.put("textPrimary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextPrimary, alpha))); + props.put("textSecondary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextSecondary, alpha))); + props.put("textMuted", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextMuted, alpha))); + props.put("progressBg", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(PROGRESS_BG, expandedAlpha))); + props.put("shuffleX", controls.shuffleX - x); + props.put("shuffleY", controls.shuffleY - y); + props.put("prevX", controls.prevX - x); + props.put("prevY", controls.prevY - y); + props.put("playX", controls.playX - x); + props.put("playY", controls.playY - y); + props.put("nextX", controls.nextX - x); + props.put("nextY", controls.nextY - y); + props.put("repeatX", controls.repeatX - x); + props.put("repeatY", controls.repeatY - y); + props.put("controlSize", controls.controlSize); props.put("showShuffle", currentSnapshot != null && currentSnapshot.supportsShuffle()); props.put("showRepeat", currentSnapshot != null && currentSnapshot.supportsRepeat()); - props.put("shuffleHover", shuffleHover); - props.put("prevHover", prevHover); - props.put("playHover", playHover); - props.put("nextHover", nextHover); - props.put("repeatHover", repeatHover); - props.put("iconShuffle", iconString(ICON_SHUFFLE)); - props.put("iconPrev", iconString(ICON_PREV)); - props.put("iconPlay", iconString(currentSnapshot != null && currentSnapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY)); - props.put("iconNext", iconString(ICON_NEXT)); - props.put("shuffleColor", hex(shuffleColor(currentSnapshot))); - props.put("repeatColor", hex(repeatColor(currentSnapshot))); - props.put("repeatAsset", repeatSvg(currentSnapshot)); + props.put("shuffleHover", controls.shuffleHover); + props.put("prevHover", controls.prevHover); + props.put("playHover", controls.playHover); + props.put("nextHover", controls.nextHover); + props.put("repeatHover", controls.repeatHover); + props.put("iconShuffle", IslandUiUtil.iconString(ICON_SHUFFLE)); + props.put("iconPrev", IslandUiUtil.iconString(ICON_PREV)); + props.put("iconPlay", IslandUiUtil.iconString(currentSnapshot != null && currentSnapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY)); + props.put("iconNext", IslandUiUtil.iconString(ICON_NEXT)); + props.put("shuffleColor", IslandUiUtil.hex(IslandUiUtil.shuffleColor(currentSnapshot))); + props.put("repeatColor", IslandUiUtil.hex(IslandUiUtil.repeatColor(currentSnapshot))); + props.put("repeatAsset", IslandUiUtil.repeatSvg(currentSnapshot)); return props; } @@ -1023,55 +584,15 @@ public final class DynamicIsland extends AbstractHudElement { private IslandMetrics measureMetrics(TextRenderer titleRenderer, TextRenderer valueRenderer, TextRenderer metaRenderer) { - float pvpTimerW = reservedTimeWidth(valueRenderer, formatSecondsOnly(predictedPvpSeconds()), 0.84f); + float pvpTimerW = IslandUiUtil.reservedTimeWidth(valueRenderer, IslandUiUtil.formatSecondsOnly(predictedPvpSeconds()), 0.84f); float pvpWidth = 24f + pvpTimerW; String title = currentSnapshot != null ? currentSnapshot.title() : ""; - float titleW = measureWidth(titleRenderer, title, 0.82f); - String compactElapsed = formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L); - float elapsedW = reservedTimeWidth(valueRenderer, compactElapsed, 0.72f); + float titleW = IslandUiUtil.measureWidth(titleRenderer, title, 0.82f); + String compactElapsed = IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L); + float elapsedW = IslandUiUtil.reservedTimeWidth(valueRenderer, compactElapsed, 0.72f); float musicWidth = 10f + elapsedW + 18f + COMPACT_ART + COMPACT_GAP + titleW + COMPACT_GAP + COMPACT_WAVE_W + 10f; - return new IslandMetrics(pvpWidth, clamp(musicWidth, 226f, 308f)); - } - - private void updateMusicControls(float drawX, float drawY, float width, TextRenderer valueRenderer) { - float progressX = drawX + EXPANDED_PAD_X; - float progressW = width - EXPANDED_PAD_X * 2f; - float progressY = drawY + 52f; - float timeH = measureHeight(valueRenderer, 0.80f); - - controlSize = CONTROL_SIZE; - float controlGap = CONTROL_GAP; - boolean showShuffle = currentSnapshot != null && currentSnapshot.supportsShuffle(); - boolean showRepeat = currentSnapshot != null && currentSnapshot.supportsRepeat(); - int activeControls = 3 + (showShuffle ? 1 : 0) + (showRepeat ? 1 : 0); - float totalControlsW = controlSize * activeControls + controlGap * Math.max(0, activeControls - 1); - float controlsX = drawX + (width - totalControlsW) * 0.5f; - float controlsY = progressY + 5f + timeH + CONTROL_ROW_GAP_Y; - float cursorX = controlsX; - - if (showShuffle) { - shuffleX = cursorX; - shuffleY = controlsY; - cursorX += controlSize + controlGap; - } else { - shuffleX = shuffleY = 0f; - } - prevX = cursorX; - prevY = controlsY; - cursorX += controlSize + controlGap; - playX = cursorX; - playY = controlsY; - cursorX += controlSize + controlGap; - nextX = cursorX; - nextY = controlsY; - cursorX += controlSize + controlGap; - if (showRepeat) { - repeatX = cursorX; - repeatY = controlsY; - } else { - repeatX = repeatY = 0f; - } + return new IslandMetrics(pvpWidth, IslandUiUtil.clamp(musicWidth, 226f, 308f)); } private boolean isInteractiveScreen(Screen screen) { @@ -1081,91 +602,7 @@ public final class DynamicIsland extends AbstractHudElement { private boolean isScreenOverlayAllowed(Screen screen) { if (screen == null || screen instanceof ChatScreen) return false; if (screen instanceof ClickGuiScreen) return false; - return !isSettingsLikeScreen(screen); - } - - private float bossBarOffset(int logicalScreenH) { - if (mc == null || mc.gui == null || mc.getWindow() == null) return 0f; - BossHealthOverlay bossBarHud = mc.gui.hud.getBossOverlay(); - if (bossBarHud == null) return 0f; - Map bars; - try { - bars = ((BossHealthOverlayAccessor) bossBarHud).getBars(); - } catch (Throwable ignored) { - return 0f; - } - if (bars == null || bars.isEmpty()) return 0f; - - int scaledH = Math.max(1, mc.getWindow().getGuiScaledHeight()); - int y = 12; - int rows = 0; - for (LerpingBossEvent ignored : bars.values()) { - rows++; - y += 19; - if (y >= scaledH / 3) break; - } - if (rows <= 0) return 0f; - - float bossBottomScaled = 17f + 19f * (rows - 1); - float hudScale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); - float windowScale = (float) mc.getWindow().getGuiScale(); - float bossBottomLogical = (bossBottomScaled + 8f) * windowScale / Math.max(0.001f, hudScale); - return Math.max(0f, Math.min(logicalScreenH * 0.30f, bossBottomLogical) - TOP_Y); - } - - private void drawFallback(Renderer2D renderer, - float drawX, - float drawY, - float drawWidth, - float drawHeight, - float radius, - float alpha, - int accent) { - renderer.roundedRectShadow(drawX, drawY, drawWidth, drawHeight, radius, 8f, 14f, - HudRenderUtil.scaleAlpha(BASE_SHADOW, 0.28f * alpha)); - renderer.roundedRect(drawX, drawY, drawWidth, drawHeight, radius, 1.1f, - HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha)); - renderer.roundedRectStroke(drawX, drawY, drawWidth, drawHeight, radius, 1.1f, 1f, - HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(BASE_STROKE, accent, 0.18f), 0.18f * alpha)); - } - - private void updateControlHover(boolean chatOpen) { - if (!chatOpen || controlSize <= 0f || mc == null || mc.getWindow() == null) { - shuffleHover = AnimationUtility.approach(shuffleHover, 0f, 0.25f); - prevHover = AnimationUtility.approach(prevHover, 0f, 0.25f); - playHover = AnimationUtility.approach(playHover, 0f, 0.25f); - nextHover = AnimationUtility.approach(nextHover, 0f, 0.25f); - repeatHover = AnimationUtility.approach(repeatHover, 0f, 0.25f); - return; - } - - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - float uiScale = HudScale.scale(fbw, fbh); - float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale); - float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale); - boolean showShuffle = currentSnapshot != null && currentSnapshot.supportsShuffle(); - boolean showRepeat = currentSnapshot != null && currentSnapshot.supportsRepeat(); - - shuffleHover = AnimationUtility.approach(shuffleHover, showShuffle && hit(mx, my, shuffleX, shuffleY, controlSize, controlSize) ? 1f : 0f, 0.25f); - prevHover = AnimationUtility.approach(prevHover, hit(mx, my, prevX, prevY, controlSize, controlSize) ? 1f : 0f, 0.25f); - playHover = AnimationUtility.approach(playHover, hit(mx, my, playX, playY, controlSize, controlSize) ? 1f : 0f, 0.25f); - nextHover = AnimationUtility.approach(nextHover, hit(mx, my, nextX, nextY, controlSize, controlSize) ? 1f : 0f, 0.25f); - repeatHover = AnimationUtility.approach(repeatHover, showRepeat && hit(mx, my, repeatX, repeatY, controlSize, controlSize) ? 1f : 0f, 0.25f); - } - - private void resetControls() { - shuffleX = shuffleY = 0f; - prevX = prevY = 0f; - playX = playY = 0f; - nextX = nextY = 0f; - repeatX = repeatY = 0f; - controlSize = 0f; - shuffleHover = 0f; - prevHover = 0f; - playHover = 0f; - nextHover = 0f; - repeatHover = 0f; + return !IslandUiUtil.isSettingsLikeScreen(screen); } private int accentFor(IslandMode mode) { @@ -1208,40 +645,6 @@ public final class DynamicIsland extends AbstractHudElement { return Math.max(0f, secondsLeft - sinceUpdate / 1000f); } - private float measureWidth(TextRenderer renderer, String text, float scale) { - if (renderer == null || text == null || text.isEmpty()) return 0f; - renderer.begin(scale, true, false); - try { - return (float) renderer.getWidth(text, false); - } finally { - renderer.end(); - } - } - - private float measureHeight(TextRenderer renderer, float scale) { - if (renderer == null) return 0f; - renderer.begin(scale, true, false); - try { - return (float) renderer.getHeight(false); - } finally { - renderer.end(); - } - } - - private float reservedTimeWidth(TextRenderer renderer, String text, float scale) { - return measureWidth(renderer, reserveTimeTemplate(text), scale); - } - - private String reserveTimeTemplate(String text) { - if (text == null || text.isEmpty()) return "0:00"; - StringBuilder out = new StringBuilder(text.length()); - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - out.append(Character.isDigit(c) ? '8' : c); - } - return out.toString(); - } - private enum IslandMode { TIME, MUSIC, diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java new file mode 100644 index 0000000..1cace62 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java @@ -0,0 +1,141 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiBoundsPatchSet; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps; +import net.minecraft.util.Util; + +import java.util.Map; + +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandPatchOps.*; + +/** + * Bounds-патчи узлов UI-скрипта Dynamic Island по режимам (time/pvp/music/clickgui). + */ +public final class IslandBoundsPatches { + + public static final float CONTEXT_GAP = 7f; + public static final float CONTEXT_CHIP_H = 20f; + public static final float CONTEXT_CHIP_Y = 7.5f; + public static final float PVP_CONTEXT_WIDTH = 46f; + + private IslandBoundsPatches() { + } + + public static Map islandBoundsPatches(UiScriptProps props, + float rootX, + float rootY, + IslandControls controls) { + UiBoundsPatchSet patches = UiBoundsPatchSet.create(48); + float width = numberProp(props, "width", 0.0f); + float height = numberProp(props, "height", 0.0f); + float mainX = numberProp(props, "mainX", 0.0f); + float mainWidth = numberProp(props, "mainWidth", width); + float leftContextWidth = numberProp(props, "leftContextWidth", 0.0f); + float rightContextWidth = numberProp(props, "rightContextWidth", 0.0f); + String mode = stringProp(props, "mode", ""); + boolean musicMode = "music".equals(mode); + boolean clickGuiMode = "clickgui".equals(mode); + + putBounds(patches, "dynamic-island", rootX, rootY, width, height); + putBounds(patches, "content", rootX, rootY, width, height); + + if (musicMode || clickGuiMode) { + putBounds(patches, "shell:blur", rootX + mainX, rootY, mainWidth, height); + putBounds(patches, "shell:shadow", rootX + mainX, rootY, mainWidth, height); + putBounds(patches, "shell:fill", rootX + mainX, rootY, mainWidth, height); + putBounds(patches, "shell:tint", rootX + mainX, rootY, mainWidth, height); + putBounds(patches, "shell:stroke", rootX + mainX, rootY, mainWidth, height); + } + + if (musicMode || clickGuiMode) { + putBounds(patches, "context:time:blur", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H); + putBounds(patches, "context:time:box", rootX, rootY + CONTEXT_CHIP_Y, leftContextWidth, CONTEXT_CHIP_H); + putBounds(patches, "context:time", rootX, rootY + 10.1f, leftContextWidth, 14.0f); + float rightX = mainX + mainWidth + CONTEXT_GAP; + putBounds(patches, "context:pvp:blur", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H); + putBounds(patches, "context:pvp:box", rootX + rightX, rootY + CONTEXT_CHIP_Y, rightContextWidth, CONTEXT_CHIP_H); + putBounds(patches, "context:pvp", rootX + rightX, rootY + 10.1f, rightContextWidth, 14.0f); + } + + if (musicMode) { + putMusicCompactBounds(patches, rootX, rootY, props, mainX, mainWidth); + putMusicExpandedBounds(patches, rootX, rootY, props, mainWidth, controls); + } else if ("pvp".equals(mode)) { + float chipW = Math.min(mainWidth - 16.0f, Math.max(42.0f, PVP_CONTEXT_WIDTH + 8.0f)); + float chipX = mainX + (mainWidth - chipW) * 0.5f; + putBounds(patches, "pvp:chip", rootX + chipX, rootY + CONTEXT_CHIP_Y, chipW, CONTEXT_CHIP_H); + putBounds(patches, "pvp:timer", rootX + chipX, rootY + 10.1f, chipW, 14.0f); + } else { + putBounds(patches, "time:box", rootX + mainX, rootY + CONTEXT_CHIP_Y, mainWidth, CONTEXT_CHIP_H); + putBounds(patches, "time", rootX + mainX, rootY + 7.8f, mainWidth, 20.0f); + } + + return patches.asMap(); + } + + private static void putMusicCompactBounds(UiBoundsPatchSet patches, + float rootX, + float rootY, + UiScriptProps props, + float mainX, + float width) { + float waveX = mainX + width - 26.0f; + putBounds(patches, "music:elapsed:compact", rootX + mainX + 11.0f, rootY + 10.5f, 38.0f, 13.0f); + putBounds(patches, "music:divider", rootX + mainX + 49.0f, rootY + 7.0f, 1.0f, 18.0f); + putBounds(patches, "artwork:compact", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f); + putBounds(patches, "artwork:compact:fallback", rootX + mainX + 58.0f, rootY + 6.5f, 22.0f, 22.0f); + float titleW = Math.max(0.0f, width - 124.0f); + putBounds(patches, "music:title:compact:clip", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f); + putBounds(patches, "music:title:compact", rootX + mainX + 89.0f, rootY + 8.5f, titleW, 18.0f); + + float centerY = 17.5f; + float t = Util.getMillis() / 1000.0f; + boolean playing = boolProp(props, "playing", false); + for (int i = 0; i < 3; i++) { + float barH = playing + ? (float) (3.0f + Math.abs(Math.sin(t * 5.2f + i * 0.85f)) * 8.0f) + : 4.0f + i; + putBounds(patches, "wave:" + i, rootX + waveX + i * 5.0f, rootY + centerY - barH * 0.5f, 2.5f, barH); + } + } + + private static void putMusicExpandedBounds(UiBoundsPatchSet patches, + float rootX, + float rootY, + UiScriptProps props, + float width, + IslandControls controls) { + float mainX = numberProp(props, "mainX", 0.0f); + float progressW = Math.max(0.0f, width - 26.0f); + putBounds(patches, "artwork:expanded", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f); + putBounds(patches, "artwork:expanded:fallback", rootX + mainX + 13.0f, rootY + 11.0f, 36.0f, 36.0f); + float titleW = Math.max(0.0f, width - 72.0f); + putBounds(patches, "music:title:expanded:clip", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f); + putBounds(patches, "music:title:expanded", rootX + mainX + 59.0f, rootY + 11.0f, titleW, 16.0f); + putBounds(patches, "music:artist:expanded:clip", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f); + putBounds(patches, "music:artist:expanded", rootX + mainX + 59.0f, rootY + 28.0f, titleW, 14.0f); + putBounds(patches, "music:progress:bg", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f); + putBounds(patches, "music:progress:fill", rootX + mainX + 13.0f, rootY + 52.0f, progressW, 5.0f); + putBounds(patches, "music:elapsed:expanded", rootX + mainX + 13.0f, rootY + 65.0f, 44.0f, 14.0f); + putBounds(patches, "music:total:expanded", rootX + mainX + width - 57.0f, rootY + 65.0f, 44.0f, 14.0f); + + if (boolProp(props, "showShuffle", false)) { + IslandPatchOps.putControlBounds(patches, "music:shuffle", controls.shuffleX, controls.shuffleY, controls.controlSize); + } + IslandPatchOps.putControlBounds(patches, "music:prev", controls.prevX, controls.prevY, controls.controlSize); + IslandPatchOps.putControlBounds(patches, "music:play", controls.playX, controls.playY, controls.controlSize); + IslandPatchOps.putControlBounds(patches, "music:next", controls.nextX, controls.nextY, controls.controlSize); + if (boolProp(props, "showRepeat", false)) { + IslandPatchOps.putSvgControlBounds(patches, "music:repeat", controls.repeatX, controls.repeatY, controls.controlSize); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java new file mode 100644 index 0000000..9a87d2b --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java @@ -0,0 +1,139 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiBoundsPatchSet; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptColor; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptPatchSet; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps; + +/** + * Примитивы патчинга UI-скрипта: текст, изображения, формы и bounds-патчи. + */ +public final class IslandPatchOps { + + private IslandPatchOps() { + } + + public static void putControlBounds(UiBoundsPatchSet patches, + String key, + float x, + float y, + float size) { + putBounds(patches, key + ":hover", x, y, size, size); + putBounds(patches, key, x, y + 4.0f, size, Math.max(0.0f, size - 4.0f)); + } + + public static void putSvgControlBounds(UiBoundsPatchSet patches, + String key, + float x, + float y, + float size) { + float iconSize = Math.max(0.0f, size * 0.74f); + putBounds(patches, key + ":hover", x, y, size, size); + putBounds(patches, key, x + (size - iconSize) * 0.5f, y + (size - iconSize) * 0.5f, iconSize, iconSize); + } + + public static void putBounds(UiBoundsPatchSet patches, + String key, + float x, + float y, + float width, + float height) { + patches.put(key, x, y, width, height); + } + + public static void patchControl(UiScriptPatchSet patches, + String key, + String icon, + String iconColor, + String accentSoft, + float expandedAlpha, + float hover) { + patchText(patches, key, icon, scaleHexAlpha(iconColor, expandedAlpha)); + patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha)); + } + + public static void patchSvgControl(UiScriptPatchSet patches, + String key, + String asset, + String iconColor, + String accentSoft, + float expandedAlpha, + float hover) { + patchImage(patches, key, asset, scaleHexAlpha(iconColor, expandedAlpha)); + patchShape(patches, key + ":hover", "fill", scaleHexAlpha(accentSoft, Math.max(0.0f, hover) * 0.3f * expandedAlpha)); + } + + public static void patchText(UiScriptPatchSet patches, + String key, + String text, + String color) { + patches.text(key, text, color); + } + + public static void patchClippedText(UiScriptPatchSet patches, + String key, + float measuredWidth, + float boxWidth, + float scrollTime, + float delay, + float speed, + float fadeWidth) { + patches.clippedText(key, measuredWidth, boxWidth, scrollTime, delay, speed, fadeWidth); + } + + public static void patchImage(UiScriptPatchSet patches, + String key, + String asset, + String tint) { + patches.image(key, asset, tint); + } + + public static void patchShape(UiScriptPatchSet patches, + String key, + Object... pairs) { + patches.props(key, pairs); + } + + public static void patchShape(UiScriptPatchSet patches, + String key, + String prop, + Object value) { + patches.put(key, prop, value); + } + + public static void patchShape(UiScriptPatchSet patches, + String key, + String propA, + Object valueA, + String propB, + Object valueB) { + patches.put(key, propA, valueA, propB, valueB); + } + + public static String stringProp(UiScriptProps props, String key, String fallback) { + return props.string(key, fallback); + } + + public static float numberProp(UiScriptProps props, String key, float fallback) { + return props.number(key, fallback); + } + + public static boolean boolProp(UiScriptProps props, String key, boolean fallback) { + return props.bool(key, fallback); + } + + public static float clamp01(float value) { + return Math.max(0.0f, Math.min(1.0f, value)); + } + + public static String scaleHexAlpha(String hex, float amount) { + return UiScriptColor.alpha(hex, amount); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java new file mode 100644 index 0000000..35d35bb --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java @@ -0,0 +1,127 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptPatchSet; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.UiScriptProps; + +import java.util.Map; + +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandPatchOps.*; + +/** + * Рантайм-патчи UI-скрипта Dynamic Island (свойства узлов по режимам) и структурная сигнатура. + */ +public final class IslandRuntimePatches { + + private IslandRuntimePatches() { + } + + public static long islandStructuralSignature(UiScriptProps props) { + long h = 0xcbf29ce484222325L; + h = CachedUiScriptRuntime.mix(h, stringProp(props, "mode", "")); + h = CachedUiScriptRuntime.mix(h, boolProp(props, "blur", false)); + h = CachedUiScriptRuntime.mix(h, numberProp(props, "blurAlpha", 0.0f)); + h = CachedUiScriptRuntime.mix(h, !stringProp(props, "artworkTexture", "").isEmpty()); + h = CachedUiScriptRuntime.mix(h, "music".equals(stringProp(props, "mode", ""))); + h = CachedUiScriptRuntime.mix(h, boolProp(props, "showShuffle", false)); + h = CachedUiScriptRuntime.mix(h, boolProp(props, "showRepeat", false)); + return h; + } + + public static Map> islandRuntimePatches(UiScriptProps props) { + UiScriptPatchSet patches = UiScriptPatchSet.create(48); + float compactAlpha = numberProp(props, "compactAlpha", 0.0f); + float expandedAlpha = numberProp(props, "expandedAlpha", 0.0f); + float contextAlpha = numberProp(props, "contextAlpha", compactAlpha); + float width = numberProp(props, "width", 0.0f); + float mainWidth = numberProp(props, "mainWidth", width); + float progress = clamp01(numberProp(props, "progress", 0.0f)); + String mode = stringProp(props, "mode", ""); + boolean musicMode = "music".equals(mode); + boolean clickGuiMode = "clickgui".equals(mode); + boolean shellMode = musicMode || clickGuiMode; + boolean pvpMode = "pvp".equals(mode); + boolean timeMode = "time".equals(mode); + String primary = stringProp(props, "textPrimary", "#FFFFFFFF"); + String secondary = stringProp(props, "textSecondary", "#99FFFFFF"); + String muted = stringProp(props, "textMuted", "#66FFFFFF"); + String accent = stringProp(props, "accent", "#FFFFFFFF"); + String accentSoft = stringProp(props, "accentSoft", "#33FFFFFF"); + + if (shellMode) { + float shellBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * numberProp(props, "alpha", 0.0f); + patchShape(patches, "dynamic-island", "renderRadius", props.get("radius"), "renderBlurAlpha", shellBlurAlpha); + patchShape(patches, "shell:blur", "blurAlpha", shellBlurAlpha, "radius", props.get("radius")); + patchShape(patches, "shell:shadow", "fill", props.get("shadow"), "radius", props.get("radius")); + patchShape(patches, "shell:fill", "startColor", props.get("fillTop"), "endColor", props.get("fillBottom"), "radius", props.get("radius")); + patchShape(patches, "shell:tint", "startColor", props.get("tintTop"), "endColor", props.get("tintBottom"), "radius", props.get("radius")); + patchShape(patches, "shell:stroke", "stroke", props.get("stroke"), "radius", props.get("radius")); + } + + if (timeMode) { + patchShape(patches, "time:box", "fill", props.get("timeFill"), "stroke", props.get("timeStroke")); + patchText(patches, "time", stringProp(props, "time", ""), primary); + } + + if (pvpMode) { + patchShape(patches, "pvp:chip", "fill", props.get("pvpFill"), "stroke", props.get("pvpStroke")); + patchText(patches, "pvp:timer", stringProp(props, "pvpTimer", "0s"), primary); + } + + if (musicMode || clickGuiMode) { + float contextBlurAlpha = numberProp(props, "blurAlpha", 0.0f) * contextAlpha; + patchShape(patches, "context:time:blur", "blurAlpha", contextBlurAlpha, "radius", 10.0f); + patchShape(patches, "context:time:box", "fill", scaleHexAlpha(stringProp(props, "contextFill", "#00000000"), contextAlpha), "stroke", scaleHexAlpha(stringProp(props, "contextStroke", "#00000000"), contextAlpha)); + patchText(patches, "context:time", stringProp(props, "time", ""), scaleHexAlpha(primary, contextAlpha)); + float pvpContextAlpha = boolProp(props, "pvpContextVisible", false) ? contextAlpha : 0.0f; + patchShape(patches, "context:pvp:blur", "blurAlpha", numberProp(props, "blurAlpha", 0.0f) * pvpContextAlpha, "radius", 10.0f); + patchShape(patches, "context:pvp:box", "fill", scaleHexAlpha(stringProp(props, "pvpFill", "#00000000"), pvpContextAlpha), "stroke", scaleHexAlpha(stringProp(props, "pvpStroke", "#00000000"), pvpContextAlpha)); + patchText(patches, "context:pvp", stringProp(props, "pvpTimer", ""), scaleHexAlpha(primary, pvpContextAlpha)); + } + + if (musicMode) { + patchText(patches, "music:elapsed:compact", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, compactAlpha)); + patchText(patches, "music:title:compact", stringProp(props, "title", ""), scaleHexAlpha(primary, compactAlpha)); + patchClippedText(patches, "music:title:compact", numberProp(props, "titleWidthCompact", 0.0f), Math.max(0.0f, mainWidth - 124.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 15.0f); + patchShape(patches, "music:divider", "fill", scaleHexAlpha(muted, 0.4f * compactAlpha)); + patchShape(patches, "wave:0", "fill", scaleHexAlpha(accent, 0.75f * compactAlpha)); + patchShape(patches, "wave:1", "fill", scaleHexAlpha(accent, 0.83f * compactAlpha)); + patchShape(patches, "wave:2", "fill", scaleHexAlpha(accent, 0.91f * compactAlpha)); + patchImage(patches, "artwork:compact", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", compactAlpha)); + patchShape(patches, "artwork:compact:fallback", "fill", scaleHexAlpha(accent, 0.92f * compactAlpha), "stroke", scaleHexAlpha("#33FFFFFF", compactAlpha)); + } + + if (musicMode) { + patchText(patches, "music:title:expanded", stringProp(props, "title", ""), scaleHexAlpha(primary, expandedAlpha)); + patchClippedText(patches, "music:title:expanded", numberProp(props, "titleWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.0f, 18.0f, 16.0f); + patchText(patches, "music:artist:expanded", stringProp(props, "artist", ""), scaleHexAlpha(secondary, expandedAlpha)); + patchClippedText(patches, "music:artist:expanded", numberProp(props, "artistWidthExpanded", 0.0f), Math.max(0.0f, mainWidth - 72.0f), numberProp(props, "titleScrollTime", 0.0f), 1.4f, 14.0f, 16.0f); + patchShape(patches, "music:progress:bg", "fill", scaleHexAlpha(stringProp(props, "progressBg", "#00FFFFFF"), expandedAlpha)); + patchShape(patches, "music:progress:fill", + "fill", scaleHexAlpha(accent, expandedAlpha), + "renderWidth", Math.max(0.0f, mainWidth - 26.0f) * progress); + patchText(patches, "music:elapsed:expanded", stringProp(props, "elapsed", "0:00"), scaleHexAlpha(secondary, expandedAlpha)); + patchText(patches, "music:total:expanded", stringProp(props, "total", "0:00"), scaleHexAlpha(secondary, expandedAlpha)); + patchImage(patches, "artwork:expanded", stringProp(props, "artworkTexture", ""), scaleHexAlpha("#FFFFFFFF", expandedAlpha)); + patchShape(patches, "artwork:expanded:fallback", "fill", scaleHexAlpha(accent, 0.92f * expandedAlpha), "stroke", scaleHexAlpha("#33FFFFFF", expandedAlpha)); + if (boolProp(props, "showShuffle", false)) { + patchControl(patches, "music:shuffle", stringProp(props, "iconShuffle", ""), stringProp(props, "shuffleColor", primary), accentSoft, expandedAlpha, numberProp(props, "shuffleHover", 0.0f)); + } + patchControl(patches, "music:prev", stringProp(props, "iconPrev", ""), primary, accentSoft, expandedAlpha, numberProp(props, "prevHover", 0.0f)); + patchControl(patches, "music:play", stringProp(props, "iconPlay", ""), primary, accentSoft, expandedAlpha, numberProp(props, "playHover", 0.0f)); + patchControl(patches, "music:next", stringProp(props, "iconNext", ""), primary, accentSoft, expandedAlpha, numberProp(props, "nextHover", 0.0f)); + if (boolProp(props, "showRepeat", false)) { + patchSvgControl(patches, "music:repeat", stringProp(props, "repeatAsset", "repeat-off"), stringProp(props, "repeatColor", "#99000000"), accentSoft, expandedAlpha, numberProp(props, "repeatHover", 0.0f)); + } + } + + return patches.asMap(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java new file mode 100644 index 0000000..efca081 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandUiUtil.java @@ -0,0 +1,199 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.misc.ClickGui; +import dev.loki.lovisual.mixins.accessors.BossHealthOverlayAccessor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.media.MediaSessionService; +import dev.loki.lovisual.util.media.RepeatMode; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.BossHealthOverlay; +import net.minecraft.client.gui.components.LerpingBossEvent; +import net.minecraft.client.gui.screens.Screen; + +import java.time.LocalTime; +import java.util.Map; +import java.util.UUID; + +/** Чистые утилиты Dynamic Island: форматирование, иконки, цвета медиа, замеры, экраны, fallback-рендер. */ +public final class IslandUiUtil { + + public static final int BASE_SHADOW = 0xFF000000; + public static final int BASE_STROKE = 0xFFFFFFFF; + + private IslandUiUtil() { + } + + public static int shuffleColor(MediaSessionService.Snapshot snapshot) { + if (snapshot == null || !snapshot.supportsShuffle()) { + return 0x99000000; + } + return snapshot.isShuffleActive() ? 0xFFFFFFFF : 0x7AB7BEC8; + } + + public static int repeatColor(MediaSessionService.Snapshot snapshot) { + if (snapshot == null || !snapshot.supportsRepeat()) { + return 0x99000000; + } + RepeatMode mode = snapshot.repeatMode(); + return mode == RepeatMode.ALL || mode == RepeatMode.ONE ? 0xFFFFFFFF : 0x7AB7BEC8; + } + + public static String repeatSvg(MediaSessionService.Snapshot snapshot) { + if (snapshot == null || !snapshot.supportsRepeat()) { + return "repeat-off"; + } + return snapshot.repeatMode() == RepeatMode.ONE ? "repeat-1" : "repeat"; + } + + public static boolean clickGuiEnabled() { + ClickGui clickGui = Modules.get(ClickGui.class); + return clickGui != null && clickGui.isEnabled(); + } + + public static boolean isSettingsLikeScreen(Screen screen) { + if (screen == null) return false; + String name = screen.getClass().getName(); + String simple = screen.getClass().getSimpleName(); + if (name.contains(".gui.screen.option.") || name.contains(".gui.screen.pack.")) return true; + return simple.contains("Options") + || simple.contains("Option") + || simple.contains("Controls") + || simple.contains("Keybind") + || simple.contains("Video") + || simple.contains("Sound") + || simple.contains("Language") + || simple.contains("Accessibility") + || simple.contains("Skin") + || simple.contains("Pack") + || simple.contains("Datapack") + || simple.contains("Customize") + || simple.contains("Telemetry") + || simple.contains("Credits"); + } + + public static String formatClock(LocalTime time) { + return String.format("%02d:%02d", time.getHour(), time.getMinute()); + } + + public static String formatSecondsOnly(float seconds) { + return Math.max(0L, (long) Math.ceil(Math.max(0f, seconds))) + "s"; + } + + public static String formatShortTime(float seconds) { + return formatShortTime((long) Math.ceil(Math.max(0f, seconds))); + } + + public static String formatShortTime(long seconds) { + long sec = Math.max(0L, seconds); + long m = sec / 60L; + long s = sec % 60L; + return String.format("%d:%02d", m, s); + } + + public static String iconString(int codepoint) { + return new String(Character.toChars(codepoint)); + } + + public static String hex(int argb) { + return String.format("#%08X", argb); + } + + public static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + public static boolean hit(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + public static float measureWidth(TextRenderer renderer, String text, float scale) { + if (renderer == null || text == null || text.isEmpty()) return 0f; + renderer.begin(scale, true, false); + try { + return (float) renderer.getWidth(text, false); + } finally { + renderer.end(); + } + } + + public static float measureHeight(TextRenderer renderer, float scale) { + if (renderer == null) return 0f; + renderer.begin(scale, true, false); + try { + return (float) renderer.getHeight(false); + } finally { + renderer.end(); + } + } + + public static float reservedTimeWidth(TextRenderer renderer, String text, float scale) { + return measureWidth(renderer, reserveTimeTemplate(text), scale); + } + + public static String reserveTimeTemplate(String text) { + if (text == null || text.isEmpty()) return "0:00"; + StringBuilder out = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + out.append(Character.isDigit(c) ? '8' : c); + } + return out.toString(); + } + + public static void drawFallback(Renderer2D renderer, + float drawX, + float drawY, + float drawWidth, + float drawHeight, + float radius, + float alpha, + int accent, + int fillTop) { + renderer.roundedRectShadow(drawX, drawY, drawWidth, drawHeight, radius, 8f, 14f, + HudRenderUtil.scaleAlpha(BASE_SHADOW, 0.28f * alpha)); + renderer.roundedRect(drawX, drawY, drawWidth, drawHeight, radius, 1.1f, + HudRenderUtil.scaleAlpha(fillTop, 0.88f * alpha)); + renderer.roundedRectStroke(drawX, drawY, drawWidth, drawHeight, radius, 1.1f, 1f, + HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(BASE_STROKE, accent, 0.18f), 0.18f * alpha)); + } + + public static float bossBarOffset(Minecraft mc, int logicalScreenH, float topY) { + if (mc == null || mc.gui == null || mc.getWindow() == null) return 0f; + BossHealthOverlay bossBarHud = mc.gui.hud.getBossOverlay(); + if (bossBarHud == null) return 0f; + Map bars; + try { + bars = ((BossHealthOverlayAccessor) bossBarHud).getBars(); + } catch (Throwable ignored) { + return 0f; + } + if (bars == null || bars.isEmpty()) return 0f; + + int scaledH = Math.max(1, mc.getWindow().getGuiScaledHeight()); + int y = 12; + int rows = 0; + for (LerpingBossEvent ignored : bars.values()) { + rows++; + y += 19; + if (y >= scaledH / 3) break; + } + if (rows <= 0) return 0f; + + float bossBottomScaled = 17f + 19f * (rows - 1); + float hudScale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + float windowScale = (float) mc.getWindow().getGuiScale(); + float bossBottomLogical = (bossBottomScaled + 8f) * windowScale / Math.max(0.001f, hudScale); + return Math.max(0f, Math.min(logicalScreenH * 0.30f, bossBottomLogical) - topY); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java new file mode 100644 index 0000000..637fec8 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java @@ -0,0 +1,123 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls; + +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandUiUtil; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.media.MediaSessionService; +import net.minecraft.client.Minecraft; + +/** + * Геометрия и hover-состояния ряда медиа-контролов Dynamic Island. + */ +public final class IslandControls { + + public static final float EXPANDED_PAD_X = 13f; + public static final float CONTROL_SIZE = 27f; + public static final float CONTROL_GAP = 7f; + public static final float CONTROL_ROW_GAP_Y = -5f; + + public float shuffleX; + public float shuffleY; + public float prevX; + public float prevY; + public float playX; + public float playY; + public float nextX; + public float nextY; + public float repeatX; + public float repeatY; + public float controlSize; + public float shuffleHover; + public float prevHover; + public float playHover; + public float nextHover; + public float repeatHover; + + public void updateLayout(float drawX, float drawY, float width, TextRenderer valueRenderer, + MediaSessionService.Snapshot snapshot) { + float progressX = drawX + EXPANDED_PAD_X; + float progressW = width - EXPANDED_PAD_X * 2f; + float progressY = drawY + 52f; + float timeH = IslandUiUtil.measureHeight(valueRenderer, 0.80f); + + controlSize = CONTROL_SIZE; + float controlGap = CONTROL_GAP; + boolean showShuffle = snapshot != null && snapshot.supportsShuffle(); + boolean showRepeat = snapshot != null && snapshot.supportsRepeat(); + int activeControls = 3 + (showShuffle ? 1 : 0) + (showRepeat ? 1 : 0); + float totalControlsW = controlSize * activeControls + controlGap * Math.max(0, activeControls - 1); + float controlsX = drawX + (width - totalControlsW) * 0.5f; + float controlsY = progressY + 5f + timeH + CONTROL_ROW_GAP_Y; + float cursorX = controlsX; + + if (showShuffle) { + shuffleX = cursorX; + shuffleY = controlsY; + cursorX += controlSize + controlGap; + } else { + shuffleX = shuffleY = 0f; + } + prevX = cursorX; + prevY = controlsY; + cursorX += controlSize + controlGap; + playX = cursorX; + playY = controlsY; + cursorX += controlSize + controlGap; + nextX = cursorX; + nextY = controlsY; + cursorX += controlSize + controlGap; + if (showRepeat) { + repeatX = cursorX; + repeatY = controlsY; + } else { + repeatX = repeatY = 0f; + } + } + + public void updateHover(boolean chatOpen, Minecraft mc, MediaSessionService.Snapshot snapshot) { + if (!chatOpen || controlSize <= 0f || mc == null || mc.getWindow() == null) { + shuffleHover = AnimationUtility.approach(shuffleHover, 0f, 0.25f); + prevHover = AnimationUtility.approach(prevHover, 0f, 0.25f); + playHover = AnimationUtility.approach(playHover, 0f, 0.25f); + nextHover = AnimationUtility.approach(nextHover, 0f, 0.25f); + repeatHover = AnimationUtility.approach(repeatHover, 0f, 0.25f); + return; + } + + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + float uiScale = HudScale.scale(fbw, fbh); + float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale); + float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale); + boolean showShuffle = snapshot != null && snapshot.supportsShuffle(); + boolean showRepeat = snapshot != null && snapshot.supportsRepeat(); + + shuffleHover = AnimationUtility.approach(shuffleHover, showShuffle && IslandUiUtil.hit(mx, my, shuffleX, shuffleY, controlSize, controlSize) ? 1f : 0f, 0.25f); + prevHover = AnimationUtility.approach(prevHover, IslandUiUtil.hit(mx, my, prevX, prevY, controlSize, controlSize) ? 1f : 0f, 0.25f); + playHover = AnimationUtility.approach(playHover, IslandUiUtil.hit(mx, my, playX, playY, controlSize, controlSize) ? 1f : 0f, 0.25f); + nextHover = AnimationUtility.approach(nextHover, IslandUiUtil.hit(mx, my, nextX, nextY, controlSize, controlSize) ? 1f : 0f, 0.25f); + repeatHover = AnimationUtility.approach(repeatHover, showRepeat && IslandUiUtil.hit(mx, my, repeatX, repeatY, controlSize, controlSize) ? 1f : 0f, 0.25f); + } + + public void reset() { + shuffleX = shuffleY = 0f; + prevX = prevY = 0f; + playX = playY = 0f; + nextX = nextY = 0f; + repeatX = repeatY = 0f; + controlSize = 0f; + shuffleHover = 0f; + prevHover = 0f; + playHover = 0f; + nextHover = 0f; + repeatHover = 0f; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java index 4cac46a..8c8d0fb 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/Module.java @@ -20,22 +20,21 @@ import dev.loki.lovisual.config.common.CommonBooleanGroupSchema; import dev.loki.lovisual.config.common.CommonSettingSchema; import dev.loki.lovisual.config.values.*; import dev.loki.lovisual.features.gui.clickgui.settings.*; +import dev.loki.lovisual.features.module.module.ModuleBinds; +import dev.loki.lovisual.features.module.module.ModuleConfigValues; +import dev.loki.lovisual.features.module.module.ModuleNames; +import dev.loki.lovisual.features.module.module.ModuleSettingValues; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.render.engine.text.FontInfo; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.Lifecycle; -import dev.loki.lovisual.util.input.KeyManager; import dev.loki.lovisual.util.logging.DebugLog; -import net.minecraft.client.resources.language.I18n; - import java.util.*; import java.util.function.Supplier; public abstract class Module implements ConfigObject, ConfigNameProvider, SettingOwner, ConfigValueAliasProvider { - private static final String SETTING_SHOW_IN_MODULE_LIST = "show_in_module_list"; - private static final String LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES = "show_in_active_modules"; protected final List settings = new ArrayList<>(); private final String name; private final String displayName; @@ -45,7 +44,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin private final BooleanValue enabledValue; private final EnumValue activationSourceValue = new EnumValue<>("activation_source", ModuleActivationSource.NONE, ModuleActivationSource.class); - private final BooleanValue moduleListVisibleValue = new BooleanValue(SETTING_SHOW_IN_MODULE_LIST, true); + private final BooleanValue moduleListVisibleValue = new BooleanValue(ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST, true); private final List declaredSettingDefs = new ArrayList<>(); private final Map, SettingDef> declaredSettingDefsByValue = new IdentityHashMap<>(); private final KeyBindSetting keyBindSetting; @@ -61,7 +60,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin this.name = info.id().toLowerCase(Locale.ROOT); this.displayName = info.displayName(); - this.aliases = normalizeAliases(info.aliases()); + this.aliases = ModuleNames.normalizeAliases(info.aliases()); this.description = info.description(); this.category = info.category(); this.enabledValue = new BooleanValue("enabled", info.enabledByDefault()); @@ -89,75 +88,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } private static KeyBindSetting createKeyBindSetting() { - return new KeyBindSetting(new KeyBindValue("bind", "NONE")); - } - - private static List normalizeAliases(String[] aliases) { - if (aliases == null || aliases.length == 0) return List.of(); - - List out = new ArrayList<>(aliases.length); - for (String alias : aliases) { - if (alias == null || alias.isBlank()) continue; - out.add(alias.trim()); - } - return List.copyOf(out); - } - - private static String stripOwnerPrefix(String value, String owner) { - if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value; - - String normalizedOwner = normalizePrefix(owner); - String lowerValue = value.toLowerCase(java.util.Locale.ROOT); - if (!lowerValue.startsWith(normalizedOwner)) return value; - - String tail = value.substring(normalizedOwner.length()); - if (tail.isEmpty()) return value; - - char first = tail.charAt(0); - if (first == '_' || first == '-' || first == '.') { - tail = tail.substring(1); - } else if (!Character.isUpperCase(first)) { - return value; - } - - return camelToSnake(tail); - } - - private static String normalizePrefix(String value) { - StringBuilder out = new StringBuilder(value.length()); - for (int i = 0; i < value.length(); i++) { - char c = Character.toLowerCase(value.charAt(i)); - if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { - out.append(c); - } - } - return out.toString(); - } - - private static String camelToSnake(String value) { - if (value == null || value.isEmpty()) return value; - - StringBuilder out = new StringBuilder(value.length() + 4); - char previous = 0; - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (c == '-' || c == '.' || c == ' ') { - c = '_'; - } - if (c == '_') { - if (!out.isEmpty() && out.charAt(out.length() - 1) != '_') { - out.append('_'); - } - previous = '_'; - continue; - } - if (Character.isUpperCase(c) && i > 0 && previous != '_' && !Character.isUpperCase(previous)) { - out.append('_'); - } - out.append(Character.toLowerCase(c)); - previous = c; - } - return out.toString(); + return ModuleBinds.createKeyBindSetting(); } private static ModuleActivationSource normalizeActivationSource(boolean state, ModuleActivationSource source) { @@ -192,9 +123,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final BooleanValue bool(String configName, String settingId, boolean def) { - BooleanValue value = new BooleanValue(configName, def); - declare(value, SettingDef.bool(settingId, value)); - return value; + return ModuleSettingValues.bool(configName, settingId, def, this::declare); } protected final NumberValue num(String configName, int def, int min, int max) { @@ -202,9 +131,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final NumberValue num(String configName, String settingId, int def, int min, int max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; + return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); } protected final NumberValue num(String configName, long def, long min, long max) { @@ -212,9 +139,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final NumberValue num(String configName, String settingId, long def, long min, long max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; + return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); } protected final NumberValue num(String configName, float def, float min, float max) { @@ -222,9 +147,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final NumberValue num(String configName, String settingId, float def, float min, float max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; + return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); } protected final NumberValue num(String configName, double def, double min, double max) { @@ -232,9 +155,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final NumberValue num(String configName, String settingId, double def, double min, double max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; + return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); } protected final ModeValue mode(String configName, String def, String... options) { @@ -242,9 +163,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final ModeValue modeSetting(String configName, String settingId, String def, String... options) { - ModeValue value = new ModeValue(configName, def, options); - declare(value, SettingDef.mode(settingId, value)); - return value; + return ModuleSettingValues.modeSetting(configName, settingId, def, this::declare, options); } protected final > EnumValue enumMode(String configName, E def) { @@ -252,10 +171,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final > EnumValue enumSetting(String configName, String settingId, E def) { - Class enumClass = def.getDeclaringClass(); - EnumValue value = new EnumValue<>(configName, def, enumClass); - declare(value, SettingDef.mode(settingId, value)); - return value; + return ModuleSettingValues.enumSetting(configName, settingId, def, this::declare); } @Deprecated(forRemoval = false) @@ -265,9 +181,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin @Deprecated(forRemoval = false) protected final > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass) { - EnumValue value = new EnumValue<>(configName, def, enumClass); - declare(value, SettingDef.mode(settingId, value)); - return value; + return ModuleSettingValues.enumSetting(configName, settingId, def, enumClass, this::declare); } @SafeVarargs @@ -277,9 +191,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin @SafeVarargs protected final > EnumValue enumSetting(String configName, String settingId, E def, E... options) { - EnumValue value = new EnumValue<>(configName, def, options); - declare(value, SettingDef.mode(settingId, value)); - return value; + return ModuleSettingValues.enumSetting(configName, settingId, def, this::declare, options); } protected final RGBAColorValue color(String configName, String defHex) { @@ -287,9 +199,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final RGBAColorValue color(String configName, String settingId, String defHex) { - RGBAColorValue value = new RGBAColorValue(configName, defHex); - declare(value, SettingDef.color(settingId, value)); - return value; + return ModuleSettingValues.color(configName, settingId, defHex, this::declare); } protected final RGBColorValue colorNoAlpha(String configName, String defHex) { @@ -297,9 +207,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final RGBColorValue colorNoAlpha(String configName, String settingId, String defHex) { - RGBColorValue value = new RGBColorValue(configName, defHex); - declare(value, SettingDef.colorNoAlpha(settingId, value)); - return value; + return ModuleSettingValues.colorNoAlpha(configName, settingId, defHex, this::declare); } protected final StringValue text(String configName, String def) { @@ -307,9 +215,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final StringValue text(String configName, String settingId, String def) { - StringValue value = new StringValue(configName, def); - declare(value, SettingDef.text(settingId, value)); - return value; + return ModuleSettingValues.text(configName, settingId, def, this::declare); } protected final SetValue textList(String configName) { @@ -321,15 +227,11 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode) { - SetValue value = new SetValue(configName); - declare(value, SettingDef.textList(settingId, value, pickerMode)); - return value; + return ModuleSettingValues.textList(configName, settingId, pickerMode, this::declare); } protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, Set defaults) { - SetValue value = new SetValue(configName, defaults); - declare(value, SettingDef.textList(settingId, value, pickerMode)); - return value; + return ModuleSettingValues.textList(configName, settingId, pickerMode, defaults, this::declare); } protected final ItemIdSetValue itemList(String configName) { @@ -341,9 +243,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final ItemIdSetValue itemList(String configName, String settingId, TextListSetting.PickerMode pickerMode) { - ItemIdSetValue value = new ItemIdSetValue(configName); - declare(value, SettingDef.textList(settingId, value, pickerMode)); - return value; + return ModuleSettingValues.itemList(configName, settingId, pickerMode, this::declare); } protected final BooleanMapValue group(String configName, Map defaults) { @@ -351,9 +251,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final BooleanMapValue group(String configName, String settingId, Map defaults) { - BooleanMapValue value = new BooleanMapValue(configName, defaults); - declare(value, SettingDef.group(settingId, value)); - return value; + return ModuleSettingValues.group(configName, settingId, defaults, this::declare); } protected final KeyBindValue bind(String configName, String def, BindMode mode) { @@ -361,9 +259,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final KeyBindValue bind(String configName, String settingId, String def, BindMode mode) { - KeyBindValue value = new KeyBindValue(configName, def); - declare(value, SettingDef.bind(settingId, value, mode)); - return value; + return ModuleSettingValues.bind(configName, settingId, def, mode, this::declare); } protected final BooleanMapValue groupCommon(String configName, @@ -520,12 +416,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin protected final BooleanMapValue protocolHeuristics(String configName, String settingId, CommonBooleanGroupSchema schema) { - Map defaults = schema == null ? Map.of() : schema.defaults(); - BooleanMapValue value = new BooleanMapValue(configName, defaults); - SettingDef def = SettingDef.protocolHeuristics(settingId, value); - if (schema != null) def.common(schema); - declare(value, def); - return value; + return ModuleSettingValues.protocolHeuristics(configName, settingId, schema, this::declare); } protected final > EnumValue enumCommon(String configName, @@ -744,17 +635,13 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } private String settingIdFromConfigName(String configName) { - if (configName == null || configName.isBlank()) return configName; - - String tail = stripOwnerPrefix(configName, name()); - tail = stripOwnerPrefix(tail, getClass().getSimpleName()); - return tail; + return ModuleNames.settingId(configName, name(), getClass().getSimpleName()); } @Override public List getLegacyValueNames(String currentValueName) { - if (SETTING_SHOW_IN_MODULE_LIST.equals(currentValueName)) { - return List.of(LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES); + if (ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST.equals(currentValueName)) { + return List.of(ModuleConfigValues.LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES); } return List.of(); } @@ -786,10 +673,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final FunctionBindSetting action(String name, String defaultKey, BindMode mode) { - FunctionBindSetting setting = new FunctionBindSetting(name, defaultKey, mode); - actions.put(name, setting); - settings.add(setting); - return setting; + return ModuleBinds.registerAction(actions, settings, name, defaultKey, mode); } protected final FunctionBindSetting action(String name, String defaultKey) { @@ -805,22 +689,15 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } protected final boolean isActionPressedOnce(String name) { - if (!isEnabled()) return false; - FunctionBindSetting setting = actions.get(name); - if (setting == null) return false; - return KeyManager.wasPressed(setting.getBindingName()); + return ModuleBinds.isPressedOnce(isEnabled(), actions.get(name)); } protected final boolean isActionHeld(String name) { - if (!isEnabled()) return false; - FunctionBindSetting setting = actions.get(name); - if (setting == null) return false; - return KeyManager.isHeld(setting.getBindingName()); + return ModuleBinds.isHeld(isEnabled(), actions.get(name)); } protected final String getActionKey(String name) { - FunctionBindSetting setting = actions.get(name); - return setting == null ? "NONE" : setting.get(); + return ModuleBinds.actionKey(actions.get(name)); } public boolean isShownInModuleList() { @@ -830,10 +707,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin public void setShownInModuleList(boolean value) { if (moduleListVisibleValue.get() == value) return; moduleListVisibleValue.set(value); - try { - ConfigSerializer.requestSave(this); - } catch (Throwable ignored) { - } + ModuleConfigValues.requestSaveQuietly(this); } public void toggleShownInModuleList() { @@ -872,10 +746,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin public final void setActivationSource(ModuleActivationSource source) { activationSource = normalizeActivationSource(enabled, source); activationSourceValue.set(activationSource); - try { - ConfigSerializer.requestSave(this); - } catch (Throwable ignored) { - } + ModuleConfigValues.requestSaveQuietly(this); } public boolean isEnabledFromKeybind() { @@ -896,7 +767,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } public String getDisplayName() { - return translateFirst(displayName, "module." + name(), "module." + name() + ".name"); + return ModuleNames.translateFirst(displayName, "module." + name(), "module." + name() + ".name"); } public List getAliases() { @@ -904,24 +775,13 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } public String getDescription() { - return translateFirst(description, "module." + name() + ".description"); + return ModuleNames.translateFirst(description, "module." + name() + ".description"); } public ModuleCategory getCategory() { return category; } - private static String translateFirst(String fallback, String... keys) { - for (String key : keys) { - if (key == null || key.isBlank()) continue; - String translated = I18n.get(key); - if (!translated.equals(key)) { - return translated; - } - } - return fallback == null ? "" : fallback; - } - public final void toggle() { setEnabled(!enabled, ModuleActivationSource.MANUAL); } @@ -944,10 +804,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin activationSource = normalizedSource; enabledValue.set(enabled); activationSourceValue.set(activationSource); - try { - ConfigSerializer.requestSave(this); - } catch (Throwable ignored) { - } + ModuleConfigValues.requestSaveQuietly(this); return; } @@ -975,10 +832,7 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin // Persist change enabledValue.set(enabled); activationSourceValue.set(activationSource); - try { - ConfigSerializer.requestSave(this); - } catch (Throwable ignored) { - } + ModuleConfigValues.requestSaveQuietly(this); } public final void setEnabledFromConfig(boolean state) { @@ -1084,64 +938,21 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin @Override public List> getConfigValues() { - Map> map = new LinkedHashMap<>(); - - map.put(enabledValue.getName(), enabledValue); - map.put(activationSourceValue.getName(), activationSourceValue); - map.put(moduleListVisibleValue.getName(), moduleListVisibleValue); - - for (Setting s : settings) { - ConfigValue v = s.getConfigValue(); - if (v != null) { - map.put(v.getName(), v); - } - } - - return new ArrayList<>(map.values()); + return ModuleConfigValues.orderedValues(enabledValue, activationSourceValue, moduleListVisibleValue, settings); } /** * Legacy support: apply values from the old "settings" map saved by previous versions. */ public void applyLegacySettings(Object json) { - if (json == null) return; - - if (json instanceof de.marhali.json5.Json5Object obj) { - for (var e : obj.asMap().entrySet()) { - applySetting(e.getKey(), e.getValue()); - } - } else if (json instanceof Map map) { - for (var e : map.entrySet()) { - if (e.getKey() instanceof String k) { - applySetting(k, e.getValue()); - } - } - } - } - - private void applySetting(String id, Object data) { - if (SETTING_SHOW_IN_MODULE_LIST.equals(id) || LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES.equals(id)) { - moduleListVisibleValue.fromJson(data); - return; - } - for (Setting s : settings) { - if (s.getId().equals(id)) { - s.load(data); - break; - } - } + ModuleConfigValues.applyLegacy(json, moduleListVisibleValue, settings); } /** * Re-applies values to settings to trigger any side effects in Setting.load(). */ public void syncSettingsAfterLoad() { - for (Setting s : settings) { - ConfigValue v = s.getConfigValue(); - if (v != null) { - s.load(v.toJson()); - } - } + ModuleConfigValues.syncAfterLoad(settings); } public void applyConfiguredEnabledState() { @@ -1173,9 +984,6 @@ public abstract class Module implements ConfigObject, ConfigNameProvider, Settin } public void saveConfig() { - try { - ConfigSerializer.requestSave(this); - } catch (Throwable ignored) { - } + ModuleConfigValues.requestSaveQuietly(this); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java new file mode 100644 index 0000000..dcf9790 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java @@ -0,0 +1,57 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.module; + +import dev.loki.lovisual.config.values.BindMode; +import dev.loki.lovisual.config.values.KeyBindValue; +import dev.loki.lovisual.features.gui.clickgui.settings.FunctionBindSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.KeyBindSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.Setting; +import dev.loki.lovisual.util.input.KeyManager; + +import java.util.List; +import java.util.Map; + +/** + * Keybind and action helpers for modules: the shared key bind setting creation, action + * registration into the module's settings and action query helpers. + */ +public final class ModuleBinds { + + private ModuleBinds() { + } + + public static KeyBindSetting createKeyBindSetting() { + return new KeyBindSetting(new KeyBindValue("bind", "NONE")); + } + + public static FunctionBindSetting registerAction(Map actions, + List settings, + String name, + String defaultKey, + BindMode mode) { + FunctionBindSetting setting = new FunctionBindSetting(name, defaultKey, mode); + actions.put(name, setting); + settings.add(setting); + return setting; + } + + public static boolean isPressedOnce(boolean enabled, FunctionBindSetting setting) { + if (!enabled || setting == null) return false; + return KeyManager.wasPressed(setting.getBindingName()); + } + + public static boolean isHeld(boolean enabled, FunctionBindSetting setting) { + if (!enabled || setting == null) return false; + return KeyManager.isHeld(setting.getBindingName()); + } + + public static String actionKey(FunctionBindSetting setting) { + return setting == null ? "NONE" : setting.get(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java new file mode 100644 index 0000000..9736c3f --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java @@ -0,0 +1,106 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.module; + +import de.marhali.json5.Json5Object; +import dev.loki.lovisual.config.ConfigObject; +import dev.loki.lovisual.config.ConfigSerializer; +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.ConfigValue; +import dev.loki.lovisual.config.values.EnumValue; +import dev.loki.lovisual.features.gui.clickgui.settings.Setting; +import dev.loki.lovisual.features.module.ModuleActivationSource; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Config plumbing for modules: the ordered config value collection, legacy settings + * application, post-load value sync and the quiet save request used across Module. + */ +public final class ModuleConfigValues { + + public static final String SETTING_SHOW_IN_MODULE_LIST = "show_in_module_list"; + public static final String LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES = "show_in_active_modules"; + + private ModuleConfigValues() { + } + + public static List> orderedValues(BooleanValue enabledValue, + EnumValue activationSourceValue, + BooleanValue moduleListVisibleValue, + List settings) { + Map> map = new LinkedHashMap<>(); + + map.put(enabledValue.getName(), enabledValue); + map.put(activationSourceValue.getName(), activationSourceValue); + map.put(moduleListVisibleValue.getName(), moduleListVisibleValue); + + for (Setting s : settings) { + ConfigValue v = s.getConfigValue(); + if (v != null) { + map.put(v.getName(), v); + } + } + + return new ArrayList<>(map.values()); + } + + /** + * Legacy support: apply values from the old "settings" map saved by previous versions. + */ + public static void applyLegacy(Object json, BooleanValue moduleListVisibleValue, List settings) { + if (json == null) return; + + if (json instanceof Json5Object obj) { + for (var e : obj.asMap().entrySet()) { + applySetting(e.getKey(), e.getValue(), moduleListVisibleValue, settings); + } + } else if (json instanceof Map map) { + for (var e : map.entrySet()) { + if (e.getKey() instanceof String k) { + applySetting(k, e.getValue(), moduleListVisibleValue, settings); + } + } + } + } + + private static void applySetting(String id, Object data, BooleanValue moduleListVisibleValue, List settings) { + if (SETTING_SHOW_IN_MODULE_LIST.equals(id) || LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES.equals(id)) { + moduleListVisibleValue.fromJson(data); + return; + } + for (Setting s : settings) { + if (s.getId().equals(id)) { + s.load(data); + break; + } + } + } + + /** + * Re-applies values to settings to trigger any side effects in Setting.load(). + */ + public static void syncAfterLoad(List settings) { + for (Setting s : settings) { + ConfigValue v = s.getConfigValue(); + if (v != null) { + s.load(v.toJson()); + } + } + } + + public static void requestSaveQuietly(ConfigObject module) { + try { + ConfigSerializer.requestSave(module); + } catch (Throwable ignored) { + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java new file mode 100644 index 0000000..acf0de9 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java @@ -0,0 +1,111 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.module; + +import net.minecraft.client.resources.language.I18n; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Name and identity helpers for modules: alias normalization, setting id derivation + * from config names and the i18n-first display/description translation lookup. + */ +public final class ModuleNames { + + private ModuleNames() { + } + + public static List normalizeAliases(String[] aliases) { + if (aliases == null || aliases.length == 0) return List.of(); + + List out = new ArrayList<>(aliases.length); + for (String alias : aliases) { + if (alias == null || alias.isBlank()) continue; + out.add(alias.trim()); + } + return List.copyOf(out); + } + + public static String stripOwnerPrefix(String value, String owner) { + if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value; + + String normalizedOwner = normalizePrefix(owner); + String lowerValue = value.toLowerCase(Locale.ROOT); + if (!lowerValue.startsWith(normalizedOwner)) return value; + + String tail = value.substring(normalizedOwner.length()); + if (tail.isEmpty()) return value; + + char first = tail.charAt(0); + if (first == '_' || first == '-' || first == '.') { + tail = tail.substring(1); + } else if (!Character.isUpperCase(first)) { + return value; + } + + return camelToSnake(tail); + } + + private static String normalizePrefix(String value) { + StringBuilder out = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = Character.toLowerCase(value.charAt(i)); + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + out.append(c); + } + } + return out.toString(); + } + + private static String camelToSnake(String value) { + if (value == null || value.isEmpty()) return value; + + StringBuilder out = new StringBuilder(value.length() + 4); + char previous = 0; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '-' || c == '.' || c == ' ') { + c = '_'; + } + if (c == '_') { + if (!out.isEmpty() && out.charAt(out.length() - 1) != '_') { + out.append('_'); + } + previous = '_'; + continue; + } + if (Character.isUpperCase(c) && i > 0 && previous != '_' && !Character.isUpperCase(previous)) { + out.append('_'); + } + out.append(Character.toLowerCase(c)); + previous = c; + } + return out.toString(); + } + + public static String settingId(String configName, String ownerId, String ownerSimpleName) { + if (configName == null || configName.isBlank()) return configName; + + String tail = stripOwnerPrefix(configName, ownerId); + tail = stripOwnerPrefix(tail, ownerSimpleName); + return tail; + } + + public static String translateFirst(String fallback, String... keys) { + for (String key : keys) { + if (key == null || key.isBlank()) continue; + String translated = I18n.get(key); + if (!translated.equals(key)) { + return translated; + } + } + return fallback == null ? "" : fallback; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java new file mode 100644 index 0000000..51091bf --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java @@ -0,0 +1,156 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.module; + +import dev.loki.lovisual.config.SettingDef; +import dev.loki.lovisual.config.common.CommonBooleanGroupSchema; +import dev.loki.lovisual.config.values.BindMode; +import dev.loki.lovisual.config.values.BooleanMapValue; +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.ConfigValue; +import dev.loki.lovisual.config.values.EnumValue; +import dev.loki.lovisual.config.values.ItemIdSetValue; +import dev.loki.lovisual.config.values.KeyBindValue; +import dev.loki.lovisual.config.values.ModeValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBAColorValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.config.values.SetValue; +import dev.loki.lovisual.config.values.StringValue; +import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting; + +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * Factory helpers for module setting values. Every factory creates the config value, + * builds its {@link SettingDef} and hands both to the caller's declare callback so the + * owning module can register them exactly as before. + */ +public final class ModuleSettingValues { + + private ModuleSettingValues() { + } + + public static BooleanValue bool(String configName, String settingId, boolean def, BiConsumer, SettingDef> declare) { + BooleanValue value = new BooleanValue(configName, def); + declare.accept(value, SettingDef.bool(settingId, value)); + return value; + } + + public static NumberValue num(String configName, String settingId, int def, int min, int max, BiConsumer, SettingDef> declare) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare.accept(value, SettingDef.number(settingId, value)); + return value; + } + + public static NumberValue num(String configName, String settingId, long def, long min, long max, BiConsumer, SettingDef> declare) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare.accept(value, SettingDef.number(settingId, value)); + return value; + } + + public static NumberValue num(String configName, String settingId, float def, float min, float max, BiConsumer, SettingDef> declare) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare.accept(value, SettingDef.number(settingId, value)); + return value; + } + + public static NumberValue num(String configName, String settingId, double def, double min, double max, BiConsumer, SettingDef> declare) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare.accept(value, SettingDef.number(settingId, value)); + return value; + } + + public static ModeValue modeSetting(String configName, String settingId, String def, BiConsumer, SettingDef> declare, String... options) { + ModeValue value = new ModeValue(configName, def, options); + declare.accept(value, SettingDef.mode(settingId, value)); + return value; + } + + public static > EnumValue enumSetting(String configName, String settingId, E def, BiConsumer, SettingDef> declare) { + Class enumClass = def.getDeclaringClass(); + EnumValue value = new EnumValue<>(configName, def, enumClass); + declare.accept(value, SettingDef.mode(settingId, value)); + return value; + } + + public static > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass, BiConsumer, SettingDef> declare) { + EnumValue value = new EnumValue<>(configName, def, enumClass); + declare.accept(value, SettingDef.mode(settingId, value)); + return value; + } + + @SafeVarargs + public static > EnumValue enumSetting(String configName, String settingId, E def, BiConsumer, SettingDef> declare, E... options) { + EnumValue value = new EnumValue<>(configName, def, options); + declare.accept(value, SettingDef.mode(settingId, value)); + return value; + } + + public static RGBAColorValue color(String configName, String settingId, String defHex, BiConsumer, SettingDef> declare) { + RGBAColorValue value = new RGBAColorValue(configName, defHex); + declare.accept(value, SettingDef.color(settingId, value)); + return value; + } + + public static RGBColorValue colorNoAlpha(String configName, String settingId, String defHex, BiConsumer, SettingDef> declare) { + RGBColorValue value = new RGBColorValue(configName, defHex); + declare.accept(value, SettingDef.colorNoAlpha(settingId, value)); + return value; + } + + public static StringValue text(String configName, String settingId, String def, BiConsumer, SettingDef> declare) { + StringValue value = new StringValue(configName, def); + declare.accept(value, SettingDef.text(settingId, value)); + return value; + } + + public static SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, BiConsumer, SettingDef> declare) { + SetValue value = new SetValue(configName); + declare.accept(value, SettingDef.textList(settingId, value, pickerMode)); + return value; + } + + public static SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, Set defaults, BiConsumer, SettingDef> declare) { + SetValue value = new SetValue(configName, defaults); + declare.accept(value, SettingDef.textList(settingId, value, pickerMode)); + return value; + } + + public static ItemIdSetValue itemList(String configName, String settingId, TextListSetting.PickerMode pickerMode, BiConsumer, SettingDef> declare) { + ItemIdSetValue value = new ItemIdSetValue(configName); + declare.accept(value, SettingDef.textList(settingId, value, pickerMode)); + return value; + } + + public static BooleanMapValue group(String configName, String settingId, Map defaults, BiConsumer, SettingDef> declare) { + BooleanMapValue value = new BooleanMapValue(configName, defaults); + declare.accept(value, SettingDef.group(settingId, value)); + return value; + } + + public static KeyBindValue bind(String configName, String settingId, String def, BindMode mode, BiConsumer, SettingDef> declare) { + KeyBindValue value = new KeyBindValue(configName, def); + declare.accept(value, SettingDef.bind(settingId, value, mode)); + return value; + } + + public static BooleanMapValue protocolHeuristics(String configName, + String settingId, + CommonBooleanGroupSchema schema, + BiConsumer, SettingDef> declare) { + Map defaults = schema == null ? Map.of() : schema.defaults(); + BooleanMapValue value = new BooleanMapValue(configName, defaults); + SettingDef def = SettingDef.protocolHeuristics(settingId, value); + if (schema != null) def.common(schema); + declare.accept(value, def); + return value; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java new file mode 100644 index 0000000..1caea00 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/NoFriendDamage.java @@ -0,0 +1,53 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat; + +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import dev.loki.lovisual.features.relations.CategoryService; +import dev.loki.lovisual.util.protection.NotifyThrottle; +import net.minecraft.util.Util; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; + +/** + * Cancels attacks against players marked as friends (including the + * Bedwars self team) and shows a throttled warning instead. + */ +@ModuleInfo( + id = "nofrienddamage", + displayName = "No Friend Damage", + aliases = {"nofrienddamage", "nfd"}, + category = ModuleCategory.COMBAT, + description = "module.nofrienddamage.description" +) +public final class NoFriendDamage extends Module { + + private static final long WARN_INTERVAL_MS = 2000L; + + private final NotifyThrottle warnThrottle = new NotifyThrottle(WARN_INTERVAL_MS); + + /** + * Called from the client attack hook (MultiPlayerGameMode.attack). + * + * @return true when the attack must be cancelled + */ + public boolean shouldBlockAttack(Entity target) { + if (!isEnabled() || !(target instanceof Player targetPlayer)) return false; + if (!CategoryService.isFriend(targetPlayer)) return false; + notifyBlocked(targetPlayer); + return true; + } + + private void notifyBlocked(Player targetPlayer) { + if (!warnThrottle.tryAcquire(Util.getMillis())) return; + Notifier.warning("Blocked attack on friend: " + targetPlayer.getGameProfile().name()); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java index 02d964d..1afa626 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java @@ -16,38 +16,34 @@ import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.resources.Identifier; import net.minecraft.util.Mth; -import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.item.PrimedTnt; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.FireworkRocketEntity; -import net.minecraft.world.entity.projectile.Projectile; -import net.minecraft.world.entity.projectile.ProjectileUtil; +import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball; +import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge; import net.minecraft.world.entity.projectile.arrow.AbstractArrow; import net.minecraft.world.entity.projectile.arrow.ThrownTrident; -import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball; -import net.minecraft.world.entity.projectile.hurtingprojectile.SmallFireball; -import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge; -import net.minecraft.world.entity.projectile.throwableitemprojectile.*; -import net.minecraft.world.item.BowItem; -import net.minecraft.world.item.CrossbowItem; +import net.minecraft.world.entity.projectile.throwableitemprojectile.AbstractThrownPotion; +import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; -import net.minecraft.world.level.ClipContext; import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.EntityHitResult; -import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import org.joml.Quaternionf; -import org.joml.Vector3f; import dev.loki.lovisual.config.values.BooleanMapValue; import dev.loki.lovisual.config.values.BooleanValue; import dev.loki.lovisual.config.values.NumberValue; import dev.loki.lovisual.config.values.RGBColorValue; -import dev.loki.lovisual.features.module.*; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsAim; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TimerPlate; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; import dev.loki.lovisual.mixins.accessors.PersistentProjectileEntityAccessor; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.TextureStorage; @@ -68,6 +64,15 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addBillboardQuad; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addGradientLine; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addImpactDecal; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addImpactRing; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.multiplyAlpha; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.withAlpha; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_ENTITY_COLOR; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_SELF_COLOR; + //todo Description @ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT) public class Predictions extends Module { @@ -87,8 +92,6 @@ public class Predictions extends Module { private static final String SETTING_TNT_COLOR = "tnt_color"; private static final String SETTING_PROJECTILES = "projectiles"; - private static final int HIT_SELF_COLOR = 0xFFFF4444; - private static final int HIT_ENTITY_COLOR = 0xFFFFCC44; private static final int GLOW_SUBSTEPS = 10; private static final float GLOW_CORE_SIZE = 0.085f; private static final float GLOW_SOFT_SIZE = 0.16f; @@ -125,20 +128,10 @@ public class Predictions extends Module { private static final float TRAJECTORY_LINE_WIDTH = 1.35f; private static final float TRAJECTORY_LINE_ALPHA_START = 0.16f; private static final float TRAJECTORY_LINE_ALPHA_END = 0.78f; - private static final int IMPACT_RING_SEGMENTS = 64; + private static final float IMPACT_DECAL_RADIUS = 0.34f; private static final float IMPACT_RING_RADIUS = 0.30f; - private static final float IMPACT_RING_CROSS_RADIUS = 0.18f; private static final float IMPACT_RING_LINE_WIDTH = 1.2f; private static final float IMPACT_RING_ALPHA = 0.88f; - private static final float IMPACT_DECAL_RADIUS = 0.34f; - private static final float IMPACT_DECAL_EDGE_SOFTNESS = 0.085f; - private static final float IMPACT_DECAL_STROKE_WIDTH = 0.10f; - private static final float IMPACT_DECAL_PATTERN_SCALE = 4.25f; - private static final float IMPACT_DECAL_FILL_ALPHA = 0.48f; - private static final float IMPACT_DECAL_STROKE_ALPHA = 0.92f; - private static final float IMPACT_DECAL_PATTERN_STRENGTH = 0.10f; - private static final float IMPACT_DECAL_QUAD_PADDING = 1.18f; - private static final double IMPACT_DECAL_OFFSET = 0.004; private final Minecraft mc = Minecraft.getInstance(); @@ -177,301 +170,6 @@ public class Predictions extends Module { private ItemStack tntTimerIcon = ItemStack.EMPTY; private final List timerPlates = new ArrayList<>(); - private static boolean canHitPredictedEntity(Entity projectile, Entity entity, AABB collisionBox) { - if (projectile == null || entity == null || !entity.isAlive() || !entity.canBeHitByProjectile()) { - return false; - } - - if (projectile instanceof Projectile projectileEntity) { - Entity owner = projectileEntity.getOwner(); - if (owner != null && !hasPredictedLeftOwner(owner, entity, collisionBox)) { - return !owner.isPassengerOfSameVehicle(entity); - } - } - - return true; - } - - private static boolean hasPredictedLeftOwner(Entity owner, Entity entity, AABB collisionBox) { - if (owner == null || collisionBox == null) { - return true; - } - return owner.getRootVehicle() - .getSelfAndPassengers() - .filter(Entity::isPickable) - .noneMatch(passenger -> collisionBox.intersects(passenger.getBoundingBox())); - } - - private static void addGradientLine(MeshBuilder mesh, Vec3 from, Vec3 to, int startColor, int endColor) { - if (mesh == null || from == null || to == null) return; - mesh.ensureLineCapacity(); - int i1 = mesh.vec3(from.x, from.y, from.z).color(new RenderColor(startColor)).next(); - int i2 = mesh.vec3(to.x, to.y, to.z).color(new RenderColor(endColor)).next(); - mesh.line(i1, i2); - } - - private static void addImpactRing(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) { - if (mesh == null || center == null) return; - - Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6 - ? new Vec3(0.0, 1.0, 0.0) - : normal.normalize(); - Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0); - Vec3 tangent = useNormal.cross(axis); - if (tangent.lengthSqr() < 1.0E-6) { - tangent = new Vec3(1.0, 0.0, 0.0); - } else { - tangent = tangent.normalize(); - } - Vec3 bitangent = useNormal.cross(tangent).normalize(); - - for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) { - double angle0 = (Math.PI * 2.0 * i) / IMPACT_RING_SEGMENTS; - double angle1 = (Math.PI * 2.0 * (i + 1)) / IMPACT_RING_SEGMENTS; - Vec3 p0 = center - .add(tangent.scale(Math.cos(angle0) * radius)) - .add(bitangent.scale(Math.sin(angle0) * radius)); - Vec3 p1 = center - .add(tangent.scale(Math.cos(angle1) * radius)) - .add(bitangent.scale(Math.sin(angle1) * radius)); - addGradientLine(mesh, p0, p1, argb, argb); - } - - Vec3 crossTangent = tangent.scale(IMPACT_RING_CROSS_RADIUS); - Vec3 crossBitangent = bitangent.scale(IMPACT_RING_CROSS_RADIUS); - addGradientLine(mesh, center.subtract(crossTangent), center.add(crossTangent), argb, argb); - addGradientLine(mesh, center.subtract(crossBitangent), center.add(crossBitangent), argb, argb); - } - - private static void addImpactDecal(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) { - if (mesh == null || center == null || radius <= 0.0f) { - return; - } - - Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6 - ? new Vec3(0.0, 1.0, 0.0) - : normal.normalize(); - Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0); - Vec3 tangent = useNormal.cross(axis); - if (tangent.lengthSqr() < 1.0E-6) { - tangent = new Vec3(1.0, 0.0, 0.0); - } else { - tangent = tangent.normalize(); - } - Vec3 bitangent = useNormal.cross(tangent).normalize(); - Vec3 offsetCenter = center.add(useNormal.scale(IMPACT_DECAL_OFFSET)); - float quadRadius = radius * IMPACT_DECAL_QUAD_PADDING; - float sdfRadius = 1.0f / IMPACT_DECAL_QUAD_PADDING; - Vec3 tangentScaled = tangent.scale(quadRadius); - Vec3 bitangentScaled = bitangent.scale(quadRadius); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).x, - offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).y, - offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).z) - .vec2(0.0, 0.0) - .color(new RenderColor(argb)) - .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) - .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) - .next(); - int i2 = mesh.vec3(offsetCenter.add(tangentScaled).subtract(bitangentScaled).x, - offsetCenter.add(tangentScaled).subtract(bitangentScaled).y, - offsetCenter.add(tangentScaled).subtract(bitangentScaled).z) - .vec2(1.0, 0.0) - .color(new RenderColor(argb)) - .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) - .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) - .next(); - int i3 = mesh.vec3(offsetCenter.add(tangentScaled).add(bitangentScaled).x, - offsetCenter.add(tangentScaled).add(bitangentScaled).y, - offsetCenter.add(tangentScaled).add(bitangentScaled).z) - .vec2(1.0, 1.0) - .color(new RenderColor(argb)) - .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) - .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) - .next(); - int i4 = mesh.vec3(offsetCenter.subtract(tangentScaled).add(bitangentScaled).x, - offsetCenter.subtract(tangentScaled).add(bitangentScaled).y, - offsetCenter.subtract(tangentScaled).add(bitangentScaled).z) - .vec2(0.0, 1.0) - .color(new RenderColor(argb)) - .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) - .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) - .next(); - mesh.quad(i1, i2, i3, i4); - } - - private static Vec3 resolveImpactNormal(HitResult hit, Vec3 velocity) { - if (hit instanceof net.minecraft.world.phys.BlockHitResult blockHit) { - return Vec3.atLowerCornerOf(blockHit.getDirection().getUnitVec3i()); - } - if (velocity != null && velocity.lengthSqr() > 1.0E-6) { - return velocity.normalize(); - } - return new Vec3(0.0, 1.0, 0.0); - } - - private static double partialStepProgress(Vec3 from, Vec3 hitPos, double segmentLengthSq) { - if (from == null || hitPos == null || segmentLengthSq <= 1.0E-6) { - return 1.0; - } - return Mth.clamp(from.distanceTo(hitPos) / Math.sqrt(segmentLengthSq), 0.0, 1.0); - } - - private static Vec3 resolveEntityImpactNormal(AABB box, Vec3 hitPos, Vec3 fallbackVelocity) { - if (box == null || hitPos == null) { - return resolveImpactNormal(null, fallbackVelocity); - } - - double dxMin = Math.abs(hitPos.x - box.minX); - double dxMax = Math.abs(box.maxX - hitPos.x); - double dyMin = Math.abs(hitPos.y - box.minY); - double dyMax = Math.abs(box.maxY - hitPos.y); - double dzMin = Math.abs(hitPos.z - box.minZ); - double dzMax = Math.abs(box.maxZ - hitPos.z); - - double best = dxMin; - Vec3 normal = new Vec3(-1.0, 0.0, 0.0); - if (dxMax < best) { - best = dxMax; - normal = new Vec3(1.0, 0.0, 0.0); - } - if (dyMin < best) { - best = dyMin; - normal = new Vec3(0.0, -1.0, 0.0); - } - if (dyMax < best) { - best = dyMax; - normal = new Vec3(0.0, 1.0, 0.0); - } - if (dzMin < best) { - best = dzMin; - normal = new Vec3(0.0, 0.0, -1.0); - } - if (dzMax < best) { - normal = new Vec3(0.0, 0.0, 1.0); - } - return normal; - } - - private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, - float size, Quaternionf camRot, int argb) { - Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot).mul(size); - Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot).mul(size); - - double p1x = cx - right.x() - up.x(); - double p1y = cy - right.y() - up.y(); - double p1z = cz - right.z() - up.z(); - double p2x = cx + right.x() - up.x(); - double p2y = cy + right.y() - up.y(); - double p2z = cz + right.z() - up.z(); - double p3x = cx + right.x() + up.x(); - double p3y = cy + right.y() + up.y(); - double p3z = cz + right.z() + up.z(); - double p4x = cx - right.x() + up.x(); - double p4y = cy - right.y() + up.y(); - double p4z = cz - right.z() + up.z(); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0.0, 1.0).color(new RenderColor(argb)).next(); - int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(argb)).next(); - int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(argb)).next(); - int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(argb)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void addFilledBox(Renderer3D renderer, AABB box, int argb) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - double minX = box.minX; - double minY = box.minY; - double minZ = box.minZ; - double maxX = box.maxX; - double maxY = box.maxY; - double maxZ = box.maxZ; - - renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); - renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a); - renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); - renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a); - renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a); - renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); - } - - private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - double minX = box.minX; - double minY = box.minY; - double minZ = box.minZ; - double maxX = box.maxX; - double maxY = box.maxY; - double maxZ = box.maxZ; - - renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a); - renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a); - renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); - renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a); - - renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a); - renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a); - renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); - renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); - - renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a); - renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a); - renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a); - renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a); - } - - private static int multiplyAlpha(int argb, float alphaMultiplier) { - int baseAlpha = (argb >>> 24) & 0xFF; - int alpha = Math.max(0, Math.min(255, Math.round(baseAlpha * Math.max(0.0f, alphaMultiplier)))); - return (argb & 0x00FFFFFF) | (alpha << 24); - } - - private static int withAlpha(int argb, int alpha) { - return (argb & 0x00FFFFFF) | (Math.max(0, Math.min(255, alpha)) << 24); - } - - private static Vec3 advanceTickVelocity(Entity entity, Vec3 velocity) { - return velocity.scale(0.99).add(0.0, -entity.getGravity(), 0.0); - } - - private static Kind kindFromParams(ItemStack stack) { - if (stack == null) return Kind.ARROW; - if (stack.is(Items.TRIDENT)) return Kind.TRIDENT; - if (stack.is(Items.SPLASH_POTION)) return Kind.POTION_SPLASH; - if (stack.is(Items.LINGERING_POTION)) return Kind.POTION_LINGERING; - if (stack.is(Items.SNOWBALL)) return Kind.SNOWBALL; - if (stack.is(Items.EGG)) return Kind.EGG; - if (stack.is(Items.FIREWORK_ROCKET)) return Kind.FIREWORK; - if (stack.is(Items.WIND_CHARGE)) return Kind.WIND_CHARGE; - if (stack.is(Items.FIRE_CHARGE)) return Kind.FIREBALL; - if (stack.is(Items.ENDER_PEARL)) return Kind.PEARL_ENTITY; - return Kind.ARROW; - } - - private static Kind kindFromProjectile(Entity entity) { - if (entity instanceof ThrownTrident) return Kind.TRIDENT; - if (entity instanceof AbstractThrownPotion potion) { - return potion.getItem().is(Items.LINGERING_POTION) ? Kind.POTION_LINGERING : Kind.POTION_SPLASH; - } - if (entity instanceof Snowball) return Kind.SNOWBALL; - if (entity instanceof ThrownEgg) return Kind.EGG; - if (entity instanceof FireworkRocketEntity) return Kind.FIREWORK; - if (entity instanceof WindCharge) return Kind.WIND_CHARGE; - if (entity instanceof Fireball) return Kind.FIREBALL; - if (entity instanceof ThrownEnderpearl) return Kind.PEARL_ENTITY; - return Kind.ARROW; - } - @Override public WorldPhase getWorldPhase() { return WorldPhase.END_MAIN; @@ -524,8 +222,8 @@ public class Predictions extends Module { List results = showPlate ? simulateProjectiles(tickDelta) : Collections.emptyList(); if (showPlate && !results.isEmpty()) { for (TrajectoryResult res : results) { - if (res.points.isEmpty()) continue; - Vec3 end = res.points.getLast(); + if (res.points().isEmpty()) continue; + Vec3 end = res.points().getLast(); Vec3 screen = ScreenProjection.worldToScreen(end, tickDelta); if (screen == null) continue; drawTimerPlate(renderer, textRenderer, res, (float) screen.x, (float) screen.y); @@ -567,6 +265,16 @@ public class Predictions extends Module { } } + private TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta) { + return PredictionsSimulation.simulateEntity(e, kind, tickDelta, + maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue); + } + + private AimPrediction computeAimPrediction(Player player, float tickDelta) { + return PredictionsAim.computeAimPrediction(player, tickDelta, + maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue); + } + private List simulateProjectiles(float tickDelta) { ClientLevel world = mc.level; if (world == null) return Collections.emptyList(); @@ -664,268 +372,6 @@ public class Predictions extends Module { return out; } - private TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta) { - ClientLevel world = mc.level; - if (world == null) return null; - - Vec3 pos = e.position(); - Vec3 vel = e.getDeltaMovement(); - - List points = new ArrayList<>(); - points.add(pos); - - double t = 0.0; - int baseMaxTicks = Math.max(1, maxSimTicks.get()); - int maxTicks = kind == Kind.PEARL_ENTITY - ? Math.max(1, pearlMaxSimTicks.get()) - : baseMaxTicks; - int hitColor = 0; - long impactTick = world.getGameTime(); - Vec3 impactPos = null; - Vec3 impactNormal = null; - Entity hitEntity = null; - boolean hitBlock = false; - - for (int i = 0; i < maxTicks; i++) { - Vec3 segmentStart = pos; - Vec3 simulatedVelocity = advanceTickVelocity(e, vel); - Vec3 simulatedPos = pos.add(simulatedVelocity); - double segmentLengthSq = segmentStart.distanceToSqr(simulatedPos); - - HitResult hit = world.clip(new ClipContext( - segmentStart, - simulatedPos, - ClipContext.Block.COLLIDER, - ClipContext.Fluid.NONE, - e - )); - - EntityCollision entityCollision = getEntityCollision(e, segmentStart, simulatedPos); - boolean hasBlockHit = hit.getType() != HitResult.Type.MISS; - boolean useEntityHit = entityCollision != null - && (!hasBlockHit || segmentStart.distanceToSqr(entityCollision.hitPos()) <= segmentStart.distanceToSqr(hit.getLocation())); - - if (useEntityHit) { - hitColor = (entityCollision.entity() == mc.player) ? HIT_SELF_COLOR : HIT_ENTITY_COLOR; - Vec3 entityHitPos = entityCollision.hitPos(); - t += partialStepProgress(segmentStart, entityHitPos, segmentLengthSq); - points.add(entityHitPos); - impactTick = world.getGameTime() + (long) Math.ceil(t); - impactPos = entityHitPos; - impactNormal = entityCollision.hitNormal(); - hitEntity = entityCollision.entity(); - break; - } else if (hasBlockHit) { - Vec3 blockHitPos = hit.getLocation(); - t += partialStepProgress(segmentStart, blockHitPos, segmentLengthSq); - points.add(blockHitPos); - impactTick = world.getGameTime() + (long) Math.ceil(t); - impactPos = blockHitPos; - impactNormal = resolveImpactNormal(hit, simulatedVelocity); - hitBlock = true; - break; - } - - pos = simulatedPos; - vel = simulatedVelocity; - t += 1.0; - points.add(pos); - impactTick = world.getGameTime() + (long) Math.ceil(t); - } - - ItemStack icon = switch (kind) { - case PEARL_ENTITY -> new ItemStack(Items.ENDER_PEARL); - case TRIDENT -> new ItemStack(Items.TRIDENT); - case FIREBALL -> new ItemStack(Items.FIRE_CHARGE); - case WIND_CHARGE -> new ItemStack(Items.WIND_CHARGE); - case FIREWORK -> new ItemStack(Items.FIREWORK_ROCKET); - case SNOWBALL -> new ItemStack(Items.SNOWBALL); - case EGG -> new ItemStack(Items.EGG); - case POTION_SPLASH -> new ItemStack(Items.SPLASH_POTION); - case POTION_LINGERING -> new ItemStack(Items.LINGERING_POTION); - default -> new ItemStack(Items.ARROW); - }; - - int color = hitColor; - if (color == 0 && kind == Kind.PEARL_ENTITY && pearlColorToggle.get()) { - color = pearlColorValue.getArgb(); - } - - return new TrajectoryResult(points, t, impactTick, icon, color, kind, impactPos, impactNormal, hitEntity, hitBlock); - } - - private EntityCollision getEntityCollision(Entity movingEntity, Vec3 startPos, Vec3 endPos) { - Vec3 direction = endPos.subtract(startPos); - if (direction.lengthSqr() == 0.0) { - return null; - } - - AABB projectileBox = movingEntity.getBoundingBox().move(startPos.subtract(movingEntity.position())); - AABB collisionBox = projectileBox.expandTowards(direction).inflate(0.5); - EntityHitResult hitResult = ProjectileUtil.getEntityHitResult( - movingEntity, - startPos, - endPos, - collisionBox, - entity -> canHitPredictedEntity(movingEntity, entity, collisionBox) - && entity instanceof LivingEntity - && !(entity instanceof ItemEntity) - && !(entity instanceof ExperienceOrb) - && entity != movingEntity, - direction.lengthSqr() - ); - if (hitResult == null) { - return null; - } - - return new EntityCollision( - hitResult.getEntity(), - hitResult.getLocation(), - resolveEntityImpactNormal(hitResult.getEntity().getBoundingBox(), hitResult.getLocation(), direction) - ); - } - - private AimPrediction computeAimPrediction(Player player, float tickDelta) { - Projectile projectile = createHeldProjectile(player, tickDelta); - if (projectile == null) { - return null; - } - - Kind kind = kindFromProjectile(projectile); - TrajectoryResult result = simulateEntity(projectile, kind, tickDelta); - if (result == null || result.points().isEmpty()) { - return null; - } - - Vec3 hitPos = result.impactPos() != null ? result.impactPos() : result.points().getLast(); - Entity hitEntity = result.hitEntity(); - Vec3 anchorPos; - if (hitEntity instanceof LivingEntity living) { - anchorPos = living.getPosition(tickDelta).add(0.0, living.getBbHeight() * 0.5, 0.0); - } else if (hitEntity != null) { - anchorPos = hitEntity.getBoundingBox().getCenter(); - } else { - anchorPos = hitPos; - } - - return new AimPrediction(hitEntity, hitPos, anchorPos, result.impactNormal(), result.iconStack(), result); - } - - private Projectile createHeldProjectile(Player player, float tickDelta) { - if (mc.level == null) { - return null; - } - - InteractionHand hand = null; - ItemStack handStack = ItemStack.EMPTY; - - ItemStack mainHand = player.getItemInHand(InteractionHand.MAIN_HAND); - if (isAimUsable(player, mainHand, InteractionHand.MAIN_HAND)) { - hand = InteractionHand.MAIN_HAND; - handStack = mainHand; - } else { - ItemStack offHand = player.getItemInHand(InteractionHand.OFF_HAND); - if (isAimUsable(player, offHand, InteractionHand.OFF_HAND)) { - hand = InteractionHand.OFF_HAND; - handStack = offHand; - } - } - - if (hand == null || handStack.isEmpty()) { - return null; - } - - Projectile projectile = null; - float speed = 0.0f; - float roll = 0.0f; - - if (handStack.is(Items.ENDER_PEARL)) { - projectile = new ThrownEnderpearl(mc.level, player, handStack); - speed = 1.5f; - } else if (handStack.is(Items.SNOWBALL)) { - projectile = new Snowball(mc.level, player, handStack); - speed = 1.5f; - } else if (handStack.is(Items.EGG)) { - projectile = new ThrownEgg(mc.level, player, handStack); - speed = 1.5f; - } else if (handStack.is(Items.SPLASH_POTION) || handStack.is(Items.LINGERING_POTION)) { - projectile = handStack.is(Items.LINGERING_POTION) - ? new ThrownLingeringPotion(mc.level, player, handStack) - : new ThrownSplashPotion(mc.level, player, handStack); - speed = 0.5f; - roll = -20.0f; - } else if (handStack.is(Items.EXPERIENCE_BOTTLE)) { - projectile = new ThrownExperienceBottle(mc.level, player, handStack); - speed = 0.7f; - roll = -20.0f; - } else if (handStack.is(Items.WIND_CHARGE)) { - projectile = new WindCharge(player, mc.level, player.position().x, player.getEyePosition().y, player.position().z); - speed = 1.5f; - } else if (handStack.is(Items.FIRE_CHARGE)) { - projectile = new SmallFireball(mc.level, player, Vec3.ZERO); - speed = 1.0f; - } else if (handStack.is(Items.TRIDENT) && player.isUsingItem() && player.getUsedItemHand() == hand) { - projectile = new ThrownTrident(mc.level, player, handStack); - speed = 2.5f; - } else if (handStack.is(Items.BOW) && player.isUsingItem() && player.getUsedItemHand() == hand) { - float pull = BowItem.getPowerForTime(player.getTicksUsingItem()); - if (pull <= 0.0f) { - return null; - } - ItemStack arrowStack = new ItemStack(Items.ARROW); - projectile = new net.minecraft.world.entity.projectile.arrow.Arrow(mc.level, player, arrowStack, handStack); - speed = 3.0f * pull; - } else if (handStack.getItem() instanceof CrossbowItem && CrossbowItem.isCharged(handStack)) { - ItemStack arrowStack = new ItemStack(Items.ARROW); - projectile = new net.minecraft.world.entity.projectile.arrow.Arrow(mc.level, player, arrowStack, handStack); - speed = 3.15f; - } - - if (projectile == null) { - return null; - } - - Vec3 eye = player.getEyePosition(tickDelta); - projectile.absSnapTo(eye.x, eye.y, eye.z); - setHeldProjectileVelocity(projectile, player, player.getXRot(), player.getYRot(), roll, speed); - return projectile; - } - - private void setHeldProjectileVelocity(Projectile projectile, Entity shooter, float pitch, float yaw, float roll, float speed) { - float x = -Mth.sin(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f)); - float y = -Mth.sin((pitch + roll) * ((float) Math.PI / 180.0f)); - float z = Mth.cos(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f)); - - Vec3 velocity = new Vec3(x, y, z).normalize().scale(speed); - projectile.setDeltaMovement(velocity); - - Vec3 movement = shooter.getKnownMovement(); - projectile.setDeltaMovement(projectile.getDeltaMovement().add(movement.x, shooter.onGround() ? 0.0 : movement.y, movement.z)); - } - - private boolean isAimUsable(Player player, ItemStack stack, InteractionHand hand) { - if (stack == null || stack.isEmpty()) { - return false; - } - - if (stack.is(Items.BOW) || stack.is(Items.TRIDENT)) { - return player.isUsingItem() && player.getUsedItemHand() == hand && player.getUseItem() == stack; - } - - if (stack.is(Items.CROSSBOW)) { - return net.minecraft.world.item.CrossbowItem.isCharged(stack); - } - - return stack.is(Items.ENDER_PEARL) - || stack.is(Items.SNOWBALL) - || stack.is(Items.EGG) - || stack.is(Items.SPLASH_POTION) - || stack.is(Items.LINGERING_POTION) - || stack.is(Items.EXPERIENCE_BOTTLE) - || stack.is(Items.FIRE_CHARGE) - || stack.is(Items.WIND_CHARGE); - } - private boolean isKindDisabled(String key) { // Ender pearls are always shown; caller must skip check for them. return !kindToggles.get(key); @@ -941,12 +387,12 @@ public class Predictions extends Module { } private void drawTimerPlate(Renderer2D renderer, TextRenderer textRenderer, TrajectoryResult res, float sx, float sy) { - double seconds = res.flightTicks / 20.0; + double seconds = res.flightTicks() / 20.0; if (seconds <= 0.0) return; - ItemStack iconStack = useItemIcon(res.kind) ? resolveTimerItem(res.kind) : ItemStack.EMPTY; - Identifier iconId = iconStack.isEmpty() ? resolveTimerIcon(res.kind) : null; - drawTimerPlate(renderer, textRenderer, formatTime(seconds), sx, sy, res.argb, iconStack, iconId); + ItemStack iconStack = useItemIcon(res.kind()) ? resolveTimerItem(res.kind()) : ItemStack.EMPTY; + Identifier iconId = iconStack.isEmpty() ? resolveTimerIcon(res.kind()) : null; + drawTimerPlate(renderer, textRenderer, formatTime(seconds), sx, sy, res.argb(), iconStack, iconId); } private void drawTntTimers(Renderer2D renderer, TextRenderer textRenderer, float tickDelta) { @@ -1300,64 +746,53 @@ public class Predictions extends Module { } } - private enum Kind { - PEARL_ENTITY, - ARROW, - TRIDENT, - FIREBALL, - WIND_CHARGE, - FIREWORK, - SNOWBALL, - EGG, - POTION_SPLASH, - POTION_LINGERING + private static void addFilledBox(Renderer3D renderer, AABB box, int argb) { + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + + double minX = box.minX; + double minY = box.minY; + double minZ = box.minZ; + double maxX = box.maxX; + double maxY = box.maxY; + double maxZ = box.maxZ; + + renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); + renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a); + renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); + renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a); + renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a); + renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); } - private record AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack, - TrajectoryResult trajectory) { - private AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack, - TrajectoryResult trajectory) { - this.entity = entity; - this.hitPos = hitPos; - this.anchorPos = anchorPos; - this.hitNormal = hitNormal; - this.displayStack = displayStack == null ? ItemStack.EMPTY : displayStack; - this.trajectory = trajectory; - } - } + private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) { + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; - private record TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack, - int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity, - boolean hitBlock) { - private TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack, - int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity, boolean hitBlock) { - this.points = points; - this.flightTicks = flightTicks; - this.impactTick = impactTick; - this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack; - this.argb = argb; - this.kind = kind; - this.impactPos = impactPos; - this.impactNormal = impactNormal; - this.hitEntity = hitEntity; - this.hitBlock = hitBlock; - } - } + double minX = box.minX; + double minY = box.minY; + double minZ = box.minZ; + double maxX = box.maxX; + double maxY = box.maxY; + double maxZ = box.maxZ; - private record EntityCollision(Entity entity, Vec3 hitPos, Vec3 hitNormal) { - } + renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a); + renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a); + renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); + renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a); - private record TimerPlate(float textX, float textY, String text, - float iconX, float iconY, float iconScale, ItemStack iconStack) { - private TimerPlate(float textX, float textY, String text, - float iconX, float iconY, float iconScale, ItemStack iconStack) { - this.textX = textX; - this.textY = textY; - this.text = text; - this.iconX = iconX; - this.iconY = iconY; - this.iconScale = iconScale; - this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack; - } + renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a); + renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a); + renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); + renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); + + renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a); + renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a); + renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a); + renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java index 0ea3fb4..7202813 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java @@ -7,9 +7,6 @@ package dev.loki.lovisual.features.module.modules.combat; -import dev.loki.lovisual.util.time.GameClock; - -import net.minecraft.client.Camera; import net.minecraft.client.Minecraft; import net.minecraft.resources.Identifier; import net.minecraft.util.Mth; @@ -17,8 +14,6 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; -import org.joml.Quaternionf; -import org.joml.Vector3f; import dev.loki.lovisual.config.common.CommonSettingSchemas; import dev.loki.lovisual.config.values.BooleanValue; import dev.loki.lovisual.config.values.ModeValue; @@ -28,13 +23,16 @@ import dev.loki.lovisual.features.module.Module; import dev.loki.lovisual.features.module.ModuleCategory; import dev.loki.lovisual.features.module.ModuleInfo; import dev.loki.lovisual.features.module.WorldPhase; +import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals; +import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects; +import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes; +import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals.CrystalInstance; import dev.loki.lovisual.features.relations.CategoryService; import dev.loki.lovisual.mixininterface.IEntity; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.TextureStorage; import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.math.RenderMath; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; @@ -44,6 +42,14 @@ import java.util.ArrayList; import java.util.List; import java.util.function.IntFunction; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderBlackhole; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderGhosts; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.drawOldTargetEsp; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.renderCaptureMark; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.renderVortex; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.setCaptureSpeedScale; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes.tickTargetEsp; + //todo Description @ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.COMBAT) public class TargetESP extends Module { @@ -69,20 +75,9 @@ public class TargetESP extends Module { private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed"; private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style"; private static final String SETTING_VORTEX_SPEED = "vortex_speed"; - private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY; - private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE; private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM; - private static final float CRYSTAL_BASE_SIZE = 0.05f; - private static final int CRYSTAL_SIDES = 8; private static final float ALPHA_TICK_DT = 1.0f / 20.0f; private static final float ALPHA_SNAP_EPSILON = 0.02f; - private static float prevCircleStep; - private static float circleStep; - private static double captureValue = 1.0; - private static double prevCaptureValue; - private static double captureSpeed = 1.0; - private static double captureSpeedScale = 1.0; - private static boolean captureFlipSpeed; private final Minecraft mc = Minecraft.getInstance(); private final ModeValue espMode = modeSetting( "targetEspMode", @@ -140,602 +135,6 @@ public class TargetESP extends Module { private Entity lastCrystalTarget; private float crystalRotationAngle; - private static void tickTargetEsp() { - prevCircleStep = circleStep; - circleStep += 0.15f; - - prevCaptureValue = captureValue; - captureValue += captureSpeed * captureSpeedScale; - if (captureSpeed > 25.0) captureFlipSpeed = true; - if (captureSpeed < -25.0) captureFlipSpeed = false; - captureSpeed = captureFlipSpeed - ? captureSpeed - 0.5 * captureSpeedScale - : captureSpeed + 0.5 * captureSpeedScale; - } - - private static void setCaptureSpeedScale(float scale) { - if (scale <= 0f) { - captureSpeedScale = 0.05; - } else { - captureSpeedScale = scale; - } - } - - private static void drawOldTargetEsp(Renderer3D renderer, Entity target, float tickDelta, IntFunction palette) { - if (renderer == null || target == null) return; - double cs = prevCircleStep + (circleStep - prevCircleStep) * tickDelta; - double prevSinAnim = absSinAnimation(cs - 0.45f); - double sinAnim = absSinAnimation(cs); - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - double x = pos.x; - double y = pos.y + prevSinAnim * target.getBbHeight(); - double z = pos.z; - double nextY = pos.y + sinAnim * target.getBbHeight(); - - MeshBuilder mesh = renderer.tris(); - - final int segments = 96; - final double step = Math.PI * 2.0 / segments; - final double radius = target.getBbWidth() * 0.8; - - for (int i = 0; i < segments; i++) { - double a0 = i * step; - double a1 = (i + 1) * step; - float cos = (float) (x + Math.cos(a0) * radius); - float sin = (float) (z + Math.sin(a0) * radius); - float cosNext = (float) (x + Math.cos(a1) * radius); - float sinNext = (float) (z + Math.sin(a1) * radius); - - int idx0 = (int) Math.round(i * (360.0 / segments)); - int idx1 = (int) Math.round((i + 1) * (360.0 / segments)); - int cTop0 = applyOpacity(palette.apply(idx0), 170.0f / 255.0f); - int cTop1 = applyOpacity(palette.apply(idx1), 170.0f / 255.0f); - int cBottom0 = applyOpacity(palette.apply(idx0), 0.0f); - int cBottom1 = applyOpacity(palette.apply(idx1), 0.0f); - - mesh.ensureQuadCapacity(); - int i1 = addVertex(mesh, cos, nextY, sin, cTop0); - int i2 = addVertex(mesh, cosNext, nextY, sinNext, cTop1); - int i3 = addVertex(mesh, cosNext, y, sinNext, cBottom1); - int i4 = addVertex(mesh, cos, y, sin, cBottom0); - mesh.quad(i1, i2, i3, i4); - } - - } - - private static void renderGhosts(Renderer3D renderer, - int espLength, int factor, float shaking, float amplitude, - Entity target, float tickDelta, IntFunction palette) { - Minecraft mc = Minecraft.getInstance(); - if (renderer == null || mc == null || mc.player == null) return; - - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - double tPosX = pos.x; - double tPosY = pos.y; - double tPosZ = pos.z; - float iAge = (float) interpolate(target.tickCount - 1, target.tickCount, tickDelta); - - MeshBuilder mesh = renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, - GHOST_PARTICLE_TEXTURE, - Renderer3D.DepthMode.MAIN - ); - if (mesh == null) return; - - for (int j = 0; j < 3; j++) { - for (int i = 0; i <= espLength; i++) { - double radians = Math.toRadians((((float) i / 1.5f + iAge) * factor + (j * 120)) % (factor * 360)); - double sinQuad = Math.sin(Math.toRadians(iAge * 2.5f + i * (j + 1)) * amplitude) / shaking; - - float offset = ((float) i / espLength); - int color = applyOpacity(palette.apply((int) (180 * offset)), offset); - float scale = Math.max(0.24f * (offset), 0.2f); - - double cx = tPosX + Math.cos(radians) * target.getBbWidth(); - double cy = tPosY + 1 + sinQuad; - double cz = tPosZ + Math.sin(radians) * target.getBbWidth(); - - addBillboardQuad(mesh, cx, cy, cz, scale, color); - } - } - } - - private static final int VORTEX_HELIX_STEPS = 28; - private static final int VORTEX_RING_SEGMENTS = 24; - - private static void renderVortex(Renderer3D renderer, - LivingEntity target, - float tickDelta, - IntFunction palette, - float spinSpeed) { - if (renderer == null || target == null) return; - - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - float radius = target.getBbWidth() * 0.7f; - float height = target.getBbHeight(); - double time = (GameClock.millis() / 1000.0) * spinSpeed; - - float prevWidth = RenderState.lineWidth; - RenderState.lineWidth = 1.4f; - MeshBuilder mesh; - try { - mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE); - } finally { - RenderState.lineWidth = prevWidth; - } - if (mesh == null) return; - - for (int strand = 0; strand < 2; strand++) { - double strandOffset = strand * Math.PI; - int prevIndex = -1; - for (int i = 0; i <= VORTEX_HELIX_STEPS; i++) { - double t = (double) i / VORTEX_HELIX_STEPS; - double angle = time * 2.4 + t * Math.PI * 3.0 + strandOffset; - double r = radius * (0.85 + 0.15 * Math.sin(t * Math.PI)); - double x = pos.x + Math.cos(angle) * r; - double y = pos.y + t * height; - double z = pos.z + Math.sin(angle) * r; - int color = applyOpacity(palette.apply((int) (t * 180)), (float) (1.0 - t * 0.35)); - - int index = addVertex(mesh, x, y, z, color); - if (prevIndex >= 0) { - mesh.line(prevIndex, index); - } - prevIndex = index; - } - } - - float[] ringHeights = {0.05f, height * 0.5f, height * 0.95f}; - float[] ringSpeeds = {0.4f, -0.55f, 0.35f}; - for (int ring = 0; ring < ringHeights.length; ring++) { - double ringY = pos.y + ringHeights[ring]; - double ringRotation = time * ringSpeeds[ring]; - int ringColor = applyOpacity(palette.apply(60), 0.4f); - int prevIndex = -1; - int firstIndex = -1; - for (int i = 0; i <= VORTEX_RING_SEGMENTS; i++) { - double angle = ringRotation + (i % VORTEX_RING_SEGMENTS) * 2.0 * Math.PI / VORTEX_RING_SEGMENTS; - double x = pos.x + Math.cos(angle) * radius; - double z = pos.z + Math.sin(angle) * radius; - int index = addVertex(mesh, x, ringY, z, ringColor); - if (firstIndex < 0) { - firstIndex = index; - } - if (prevIndex >= 0) { - mesh.line(prevIndex, index); - } - prevIndex = index; - } - } - } - - private static void renderCaptureMark(Renderer3D renderer, - Entity target, - float tickDelta, - IntFunction palette) { - if (renderer == null) return; - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - double tPosX = pos.x; - double tPosY = pos.y; - double tPosZ = pos.z; - - MeshBuilder mesh = renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, - CAPTURE_MARK_TEXTURE, - Renderer3D.DepthMode.MAIN - ); - if (mesh == null) return; - - float angle = interpolateFloat((float) prevCaptureValue, (float) captureValue, tickDelta); - float size = 0.75f; - - double cy = tPosY + target.getEyeHeight(target.getPose()) / 2f; - - addRotatedBillboard(mesh, tPosX, cy, tPosZ, size, angle, palette); - } - - private static final float[][] BODY_HOLES = { - {0.0f, 0.60f}, // голова - {-0.48f, 0.36f}, // левая рука - {0.48f, 0.36f}, // правая рука - {0.0f, 0.05f}, // грудь - {-0.30f, -0.42f},// левая нога - {0.30f, -0.42f} // правая нога - }; - - private static void renderBlackhole(Renderer3D renderer, - LivingEntity target, - float tickDelta, - IntFunction palette, - float size, - float spinSpeed, - float alpha, - String style) { - if (renderer == null || target == null) return; - Minecraft mc = Minecraft.getInstance(); - if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return; - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - Vec3 baseCenter = new Vec3(pos.x, pos.y + target.getBbHeight() * 0.5, pos.z); - - // Камера-зависимый billboard-базис: правый/верх в мировых координатах - Camera cam = mc.gameRenderer.mainCamera(); - float yaw = (float) Math.toRadians(cam.yRot()); - float pitch = (float) Math.toRadians(cam.xRot()); - Vec3 fwd = new Vec3(-Math.sin(yaw) * Math.cos(pitch), -Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch)).normalize(); - Vec3 rgt = new Vec3(0.0, 1.0, 0.0).cross(fwd); - if (rgt.lengthSqr() < 1.0E-6) { - rgt = new Vec3(1.0, 0.0, 0.0); - } else { - rgt = rgt.normalize(); - } - Vec3 upp = fwd.cross(rgt).normalize(); - Vector3f right = new Vector3f((float) rgt.x, (float) rgt.y, (float) rgt.z); - Vector3f up = new Vector3f((float) upp.x, (float) upp.y, (float) upp.z); - - MeshBuilder diskMesh = renderer.batch( - LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, - Renderer3D.DepthMode.PRE_DEPTH - ); - MeshBuilder coreMesh = renderer.batch( - LoVisualRenderPipelines.WORLD_COLORED_DEPTH, - Renderer3D.DepthMode.PRE_DEPTH - ); - if (diskMesh == null || coreMesh == null) return; - - float time = (GameClock.millis() % 100000L) / 1000.0f; - - if ("Body".equals(style)) { - float holeSize = size * 0.34f; - for (float[] off : BODY_HOLES) { - Vec3 center = new Vec3( - baseCenter.x + rgt.x * off[0] + upp.x * off[1], - baseCenter.y + rgt.y * off[0] + upp.y * off[1], - baseCenter.z + rgt.z * off[0] + upp.z * off[1] - ); - appendBlackhole(diskMesh, coreMesh, center, right, up, holeSize, time, spinSpeed, palette, alpha); - } - } else { - appendBlackhole(diskMesh, coreMesh, baseCenter, right, up, size, time, spinSpeed, palette, alpha); - } - } - - private static void appendBlackhole(MeshBuilder diskMesh, - MeshBuilder coreMesh, - Vec3 center, - Vector3f right, - Vector3f up, - float size, - float time, - float spinSpeed, - IntFunction palette, - float alpha) { - float horizon = 0.35f * size; - int rings = 4; - int segs = 64; - - // аккреционный диск: вращающаяся спираль на аддитивном бленде - for (int ring = 0; ring < rings; ring++) { - float r0 = horizon + (size - horizon) * (ring / (float) rings); - float r1 = horizon + (size - horizon) * ((ring + 1) / (float) rings); - for (int s = 0; s < segs; s++) { - float a0 = (float) (Math.PI * 2.0 * s / segs); - float a1 = (float) (Math.PI * 2.0 * (s + 1) / segs); - int v0 = addBlackholeVertex(diskMesh, center, right, up, r0, a0, horizon, size, time, spinSpeed, palette); - int v1 = addBlackholeVertex(diskMesh, center, right, up, r1, a0, horizon, size, time, spinSpeed, palette); - int v2 = addBlackholeVertex(diskMesh, center, right, up, r1, a1, horizon, size, time, spinSpeed, palette); - int v3 = addBlackholeVertex(diskMesh, center, right, up, r0, a1, horizon, size, time, spinSpeed, palette); - diskMesh.triangle(v0, v1, v2); - diskMesh.triangle(v0, v2, v3); - } - } - - // чёрное ядро закрывает центр диска - int coreArgb = applyOpacity(0xFF080808, alpha); - int coreCenter = addBlackholeCoreVertex(coreMesh, center, right, up, 0f, 0f, coreArgb); - int coreSegs = 32; - for (int s = 0; s < coreSegs; s++) { - float a0 = (float) (Math.PI * 2.0 * s / coreSegs); - float a1 = (float) (Math.PI * 2.0 * (s + 1) / coreSegs); - int c0 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a0, coreArgb); - int c1 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a1, coreArgb); - coreMesh.triangle(coreCenter, c1, c0); - } - } - - private static int addBlackholeVertex(MeshBuilder mesh, - Vec3 center, - Vector3f right, - Vector3f up, - float r, - float angle, - float horizon, - float size, - float time, - float spinSpeed, - IntFunction palette) { - float dist = r / size; - float rot = time * 2.5f * spinSpeed + 1.3f / (dist + 0.05f); - float ca = (float) Math.cos(rot); - float sa = (float) Math.sin(rot); - float ux = dist * (float) Math.cos(angle); - float uy = dist * (float) Math.sin(angle); - float rx = ux * ca - uy * sa; - float ry = ux * sa + uy * ca; - float spiral = Math.abs((float) Math.sin(rx * 14.0f + ry * 4.0f) * 0.5f + 0.5f); - float falloff = (float) Math.pow(horizon / Math.max(dist, 0.001f), 2.2); - float outerFade = smoothstep(1.0f, horizon, dist); - float brightness = falloff * (0.55f + 0.45f * spiral) * outerFade; - - int color = palette.apply((int) ((Math.toDegrees(angle) + time * 90f * spinSpeed) % 360f)); - color = applyOpacity(color, brightness); - - float wx = (float) center.x + right.x() * ux * size + up.x() * uy * size; - float wy = (float) center.y + right.y() * ux * size + up.y() * uy * size; - float wz = (float) center.z + right.z() * ux * size + up.z() * uy * size; - return addVertex(mesh, wx, wy, wz, color); - } - - private static int addBlackholeCoreVertex(MeshBuilder mesh, - Vec3 center, - Vector3f right, - Vector3f up, - float r, - float angle, - int argb) { - float ux = r * (float) Math.cos(angle); - float uy = r * (float) Math.sin(angle); - float wx = (float) center.x + right.x() * ux + up.x() * uy; - float wy = (float) center.y + right.y() * ux + up.y() * uy; - float wz = (float) center.z + right.z() * ux + up.z() * uy; - return addVertex(mesh, wx, wy, wz, argb); - } - - private static float smoothstep(float edge0, float edge1, float x) { - float t = (x - edge0) / (edge1 - edge0); - t = Math.max(0f, Math.min(1f, t)); - return t * t * (3f - 2f * t); - } - - private static void appendCrystalGeometry(MeshBuilder mesh, - Vec3 targetPos, - CrystalInstance crystal, - float orbitYawRad, - float scale, - int argb, - boolean filled) { - if (mesh == null || crystal == null || targetPos == null) { - return; - } - - float s = CRYSTAL_BASE_SIZE * scale; - float hPrism = s; - float hPyramid = s * 1.5f; - - Vector3f[] topVertices = new Vector3f[CRYSTAL_SIDES]; - Vector3f[] bottomVertices = new Vector3f[CRYSTAL_SIDES]; - for (int i = 0; i < CRYSTAL_SIDES; i++) { - float angle = (float) (2.0 * Math.PI * i / CRYSTAL_SIDES); - float x = (float) (s * Math.cos(angle)); - float z = (float) (s * Math.sin(angle)); - topVertices[i] = new Vector3f(x, hPrism * 0.5f, z); - bottomVertices[i] = new Vector3f(x, -hPrism * 0.5f, z); - } - - Vector3f topPeak = new Vector3f(0.0f, hPrism * 0.5f + hPyramid, 0.0f); - Vector3f bottomPeak = new Vector3f(0.0f, -hPrism * 0.5f - hPyramid, 0.0f); - - if (filled) { - for (int i = 0; i < CRYSTAL_SIDES; i++) { - Vector3f v1 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos); - Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); - Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); - Vector3f v4 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos); - addTriangle(mesh, v1, v2, v3, argb); - addTriangle(mesh, v1, v3, v4, argb); - } - for (int i = 0; i < CRYSTAL_SIDES; i++) { - Vector3f v1 = transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos); - Vector3f v2 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos); - Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); - addTriangle(mesh, v1, v2, v3, argb); - } - for (int i = 0; i < CRYSTAL_SIDES; i++) { - Vector3f v1 = transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos); - Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); - Vector3f v3 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos); - addTriangle(mesh, v1, v2, v3, argb); - } - return; - } - - for (int i = 0; i < CRYSTAL_SIDES; i++) { - Vector3f bottomA = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos); - Vector3f bottomB = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); - Vector3f topA = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos); - Vector3f topB = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); - addLine(mesh, bottomA, bottomB, argb); - addLine(mesh, topA, topB, argb); - addLine(mesh, bottomA, topA, argb); - addLine(mesh, transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos), topA, argb); - addLine(mesh, transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos), bottomA, argb); - } - } - - private static Vector3f transformCrystalVertex(Vector3f vertex, - CrystalInstance crystal, - float orbitYawRad, - Vec3 targetPos) { - Vector3f transformed = new Vector3f(vertex).mul(crystal.pulsation()); - transformed.rotateX((float) Math.toRadians(crystal.rotation.x)); - transformed.rotateY((float) Math.toRadians(crystal.rotation.y + crystal.selfRotationDeg())); - transformed.rotateZ((float) Math.toRadians(crystal.rotation.z)); - transformed.add((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z); - transformed.rotateY(orbitYawRad); - transformed.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z); - return transformed; - } - - private static void appendBloomSphere(MeshBuilder mesh, - Vec3 targetPos, - CrystalInstance crystal, - float orbitYawRad, - int argb) { - if (mesh == null || crystal == null || targetPos == null) { - return; - } - - Vector3f center = new Vector3f((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z); - center.rotateY(orbitYawRad); - center.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z); - - float size = CRYSTAL_BASE_SIZE * 13.0f; - Quaternionf camRot = RenderState.cameraRotation; - float yaw = orbitYawRad; - int bloomColor = applyOpacity(argb, (0.4f * 25.0f) / 255.0f); - - for (int i = 0; i < 6; i++) { - float angle = (360.0f / 6.0f) * i; - Quaternionf rotation = new Quaternionf() - .rotateY((float) Math.toRadians(angle)) - .rotateY(yaw) - .mul(camRot); - addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor); - } - - for (int i = 0; i < 6; i++) { - float angle = (360.0f / 6.0f) * i; - Quaternionf rotation = new Quaternionf() - .rotateX((float) Math.toRadians(90.0f)) - .rotateY((float) Math.toRadians(angle)) - .rotateY(yaw) - .mul(camRot); - addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor); - } - } - - private static void addTexturedBillboard(MeshBuilder mesh, - double cx, double cy, double cz, - float size, Quaternionf rotation, int argb) { - Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(rotation).mul(size * 0.5f); - Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(rotation).mul(size * 0.5f); - addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb); - } - - private static void addTriangle(MeshBuilder mesh, Vector3f v1, Vector3f v2, Vector3f v3, int argb) { - mesh.ensureTriCapacity(); - int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb); - int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb); - int i3 = addVertex(mesh, v3.x, v3.y, v3.z, argb); - mesh.triangle(i1, i2, i3); - } - - private static void addLine(MeshBuilder mesh, Vector3f v1, Vector3f v2, int argb) { - mesh.ensureLineCapacity(); - int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb); - int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb); - mesh.line(i1, i2); - } - - private static void addRotatedBillboard(MeshBuilder mesh, double cx, double cy, double cz, - float size, float angle, IntFunction palette) { - Quaternionf camRot = RenderState.cameraRotation; - Vector3f right = new Vector3f(1, 0, 0).rotate(camRot); - Vector3f up = new Vector3f(0, 1, 0).rotate(camRot); - - Vector3f r = new Vector3f(right).mul(size); - Vector3f u = new Vector3f(up).mul(size); - - float sin = Mth.sin((float) Math.toRadians(angle)); - float cos = Mth.cos((float) Math.toRadians(angle)); - - Vector3f rRot = new Vector3f( - r.x() * cos - u.x() * sin, - r.y() * cos - u.y() * sin, - r.z() * cos - u.z() * sin - ); - Vector3f uRot = new Vector3f( - r.x() * sin + u.x() * cos, - r.y() * sin + u.y() * cos, - r.z() * sin + u.z() * cos - ); - - addBillboardVertices(mesh, cx, cy, cz, rRot, uRot, - palette.apply(90), - palette.apply(0), - palette.apply(180), - palette.apply(270)); - } - - private static void addBillboardQuad(MeshBuilder mesh, - double cx, double cy, double cz, - float size, int argb) { - Quaternionf camRot = RenderState.cameraRotation; - Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); - Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); - addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb); - } - - private static void addBillboardVertices(MeshBuilder mesh, double cx, double cy, double cz, - Vector3f right, Vector3f up, - int c1, int c2, int c3, int c4) { - double p1x = cx - right.x() - up.x(); - double p1y = cy - right.y() - up.y(); - double p1z = cz - right.z() - up.z(); - double p2x = cx + right.x() - up.x(); - double p2y = cy + right.y() - up.y(); - double p2z = cz + right.z() - up.z(); - double p3x = cx + right.x() + up.x(); - double p3y = cy + right.y() + up.y(); - double p3z = cz + right.z() + up.z(); - double p4x = cx - right.x() + up.x(); - double p4y = cy - right.y() + up.y(); - double p4z = cz - right.z() + up.z(); - - mesh.ensureQuadCapacity(); - int i1 = addVertex(mesh, p1x, p1y, p1z, 0, 1, c1); - int i2 = addVertex(mesh, p2x, p2y, p2z, 1, 1, c2); - int i3 = addVertex(mesh, p3x, p3y, p3z, 1, 0, c3); - int i4 = addVertex(mesh, p4x, p4y, p4z, 0, 0, c4); - mesh.quad(i1, i2, i3, i4); - } - - private static int applyOpacity(int argb, float opacity) { - opacity = Math.min(1f, Math.max(0f, opacity)); - int baseA = (argb >>> 24) & 0xFF; - int a = Mth.clamp((int) (baseA * opacity), 0, 255); - return (a << 24) | (argb & 0x00FFFFFF); - } - - private static int addVertex(MeshBuilder mesh, double x, double y, double z, int argb) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - mesh.vec3(x, y, z).color(r, g, b, a); - return mesh.next(); - } - - private static int addVertex(MeshBuilder mesh, double x, double y, double z, double u, double v, int argb) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - mesh.vec3(x, y, z).vec2(u, v).color(r, g, b, a); - return mesh.next(); - } - - private static double absSinAnimation(double input) { - return Math.abs(1 + Math.sin(input)) / 2; - } - - private static double interpolate(double oldValue, double newValue, double interpolationValue) { - return (oldValue + (newValue - oldValue) * interpolationValue); - } - - private static float interpolateFloat(float oldValue, float newValue, double interpolationValue) { - return (float) interpolate(oldValue, newValue, interpolationValue); - } - @Override public void onDisable() { target = null; @@ -1024,38 +423,4 @@ public class TargetESP extends Module { default -> AnimatedRenderColors.Mode.STATIC; }; } - - private static final class CrystalInstance { - private final Vec3 position; - private final Vec3 rotation; - private final float rotationSpeed; - - private CrystalInstance(Vec3 position, Vec3 rotation) { - this.position = position; - this.rotation = rotation; - this.rotationSpeed = 0.5f + (float) (Math.random() * 1.5f); - } - - private float selfRotationDeg() { - return (GameClock.millis() % 36000L) / 100.0f * rotationSpeed; - } - - private float pulsation() { - return 1.0f + (float) (Math.sin(GameClock.millis() / 500.0) * 0.1f); - } - - private void append(MeshBuilder additiveMesh, - MeshBuilder fillMesh, - MeshBuilder lineMesh, - MeshBuilder bloomMesh, - Vec3 targetPos, - float orbitYawRad, - int baseColor) { - appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.2f), true); - appendCrystalGeometry(fillMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.3f), true); - appendCrystalGeometry(lineMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.8f), false); - appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.2f, applyOpacity(baseColor, 0.3f), true); - appendBloomSphere(bloomMesh, targetPos, this, orbitYawRad, baseColor); - } - } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java new file mode 100644 index 0000000..b2be6d9 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java @@ -0,0 +1,190 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.predictions; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.projectile.arrow.Arrow; +import net.minecraft.world.entity.projectile.arrow.ThrownTrident; +import net.minecraft.world.entity.projectile.hurtingprojectile.SmallFireball; +import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge; +import net.minecraft.world.entity.projectile.throwableitemprojectile.*; +import net.minecraft.world.item.BowItem; +import net.minecraft.world.item.CrossbowItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.Vec3; + +/** + * Held-item aim prediction for the Predictions module: builds the hypothetical + * projectile for the item in hand and simulates its trajectory. + */ +public final class PredictionsAim { + private PredictionsAim() { + } + + public static AimPrediction computeAimPrediction(Player player, float tickDelta, + NumberValue maxSimTicks, + NumberValue pearlMaxSimTicks, + BooleanValue pearlColorToggle, + RGBColorValue pearlColorValue) { + Projectile projectile = createHeldProjectile(player, tickDelta); + if (projectile == null) { + return null; + } + + Kind kind = PredictionsModel.kindFromProjectile(projectile); + TrajectoryResult result = PredictionsSimulation.simulateEntity(projectile, kind, tickDelta, + maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue); + if (result == null || result.points().isEmpty()) { + return null; + } + + Vec3 hitPos = result.impactPos() != null ? result.impactPos() : result.points().getLast(); + Entity hitEntity = result.hitEntity(); + Vec3 anchorPos; + if (hitEntity instanceof LivingEntity living) { + anchorPos = living.getPosition(tickDelta).add(0.0, living.getBbHeight() * 0.5, 0.0); + } else if (hitEntity != null) { + anchorPos = hitEntity.getBoundingBox().getCenter(); + } else { + anchorPos = hitPos; + } + + return new AimPrediction(hitEntity, hitPos, anchorPos, result.impactNormal(), result.iconStack(), result); + } + + private static Projectile createHeldProjectile(Player player, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + ClientLevel world = mc.level; + if (world == null) { + return null; + } + + InteractionHand hand = null; + ItemStack handStack = ItemStack.EMPTY; + + ItemStack mainHand = player.getItemInHand(InteractionHand.MAIN_HAND); + if (isAimUsable(player, mainHand, InteractionHand.MAIN_HAND)) { + hand = InteractionHand.MAIN_HAND; + handStack = mainHand; + } else { + ItemStack offHand = player.getItemInHand(InteractionHand.OFF_HAND); + if (isAimUsable(player, offHand, InteractionHand.OFF_HAND)) { + hand = InteractionHand.OFF_HAND; + handStack = offHand; + } + } + + if (hand == null || handStack.isEmpty()) { + return null; + } + + Projectile projectile = null; + float speed = 0.0f; + float roll = 0.0f; + + if (handStack.is(Items.ENDER_PEARL)) { + projectile = new ThrownEnderpearl(world, player, handStack); + speed = 1.5f; + } else if (handStack.is(Items.SNOWBALL)) { + projectile = new Snowball(world, player, handStack); + speed = 1.5f; + } else if (handStack.is(Items.EGG)) { + projectile = new ThrownEgg(world, player, handStack); + speed = 1.5f; + } else if (handStack.is(Items.SPLASH_POTION) || handStack.is(Items.LINGERING_POTION)) { + projectile = handStack.is(Items.LINGERING_POTION) + ? new ThrownLingeringPotion(world, player, handStack) + : new ThrownSplashPotion(world, player, handStack); + speed = 0.5f; + roll = -20.0f; + } else if (handStack.is(Items.EXPERIENCE_BOTTLE)) { + projectile = new ThrownExperienceBottle(world, player, handStack); + speed = 0.7f; + roll = -20.0f; + } else if (handStack.is(Items.WIND_CHARGE)) { + projectile = new WindCharge(player, world, player.position().x, player.getEyePosition().y, player.position().z); + speed = 1.5f; + } else if (handStack.is(Items.FIRE_CHARGE)) { + projectile = new SmallFireball(world, player, Vec3.ZERO); + speed = 1.0f; + } else if (handStack.is(Items.TRIDENT) && player.isUsingItem() && player.getUsedItemHand() == hand) { + projectile = new ThrownTrident(world, player, handStack); + speed = 2.5f; + } else if (handStack.is(Items.BOW) && player.isUsingItem() && player.getUsedItemHand() == hand) { + float pull = BowItem.getPowerForTime(player.getTicksUsingItem()); + if (pull <= 0.0f) { + return null; + } + ItemStack arrowStack = new ItemStack(Items.ARROW); + projectile = new Arrow(world, player, arrowStack, handStack); + speed = 3.0f * pull; + } else if (handStack.getItem() instanceof CrossbowItem && CrossbowItem.isCharged(handStack)) { + ItemStack arrowStack = new ItemStack(Items.ARROW); + projectile = new Arrow(world, player, arrowStack, handStack); + speed = 3.15f; + } + + if (projectile == null) { + return null; + } + + Vec3 eye = player.getEyePosition(tickDelta); + projectile.absSnapTo(eye.x, eye.y, eye.z); + setHeldProjectileVelocity(projectile, player, player.getXRot(), player.getYRot(), roll, speed); + return projectile; + } + + private static void setHeldProjectileVelocity(Projectile projectile, Entity shooter, float pitch, float yaw, float roll, float speed) { + float x = -Mth.sin(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f)); + float y = -Mth.sin((pitch + roll) * ((float) Math.PI / 180.0f)); + float z = Mth.cos(yaw * ((float) Math.PI / 180.0f)) * Mth.cos(pitch * ((float) Math.PI / 180.0f)); + + Vec3 velocity = new Vec3(x, y, z).normalize().scale(speed); + projectile.setDeltaMovement(velocity); + + Vec3 movement = shooter.getKnownMovement(); + projectile.setDeltaMovement(projectile.getDeltaMovement().add(movement.x, shooter.onGround() ? 0.0 : movement.y, movement.z)); + } + + private static boolean isAimUsable(Player player, ItemStack stack, InteractionHand hand) { + if (stack == null || stack.isEmpty()) { + return false; + } + + if (stack.is(Items.BOW) || stack.is(Items.TRIDENT)) { + return player.isUsingItem() && player.getUsedItemHand() == hand && player.getUseItem() == stack; + } + + if (stack.is(Items.CROSSBOW)) { + return net.minecraft.world.item.CrossbowItem.isCharged(stack); + } + + return stack.is(Items.ENDER_PEARL) + || stack.is(Items.SNOWBALL) + || stack.is(Items.EGG) + || stack.is(Items.SPLASH_POTION) + || stack.is(Items.LINGERING_POTION) + || stack.is(Items.EXPERIENCE_BOTTLE) + || stack.is(Items.FIRE_CHARGE) + || stack.is(Items.WIND_CHARGE); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java new file mode 100644 index 0000000..6aade55 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java @@ -0,0 +1,194 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.predictions; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Low level mesh primitives, impact shapes and small vector/color math shared by + * the Predictions renderers. + */ +public final class PredictionsGeometry { + private PredictionsGeometry() { + } + + private static final int IMPACT_RING_SEGMENTS = 64; + private static final float IMPACT_RING_CROSS_RADIUS = 0.18f; + private static final float IMPACT_DECAL_EDGE_SOFTNESS = 0.085f; + private static final float IMPACT_DECAL_STROKE_WIDTH = 0.10f; + private static final float IMPACT_DECAL_PATTERN_SCALE = 4.25f; + private static final float IMPACT_DECAL_FILL_ALPHA = 0.48f; + private static final float IMPACT_DECAL_STROKE_ALPHA = 0.92f; + private static final float IMPACT_DECAL_PATTERN_STRENGTH = 0.10f; + private static final float IMPACT_DECAL_QUAD_PADDING = 1.18f; + private static final double IMPACT_DECAL_OFFSET = 0.004; + + public static void addGradientLine(MeshBuilder mesh, Vec3 from, Vec3 to, int startColor, int endColor) { + if (mesh == null || from == null || to == null) return; + mesh.ensureLineCapacity(); + int i1 = mesh.vec3(from.x, from.y, from.z).color(new RenderColor(startColor)).next(); + int i2 = mesh.vec3(to.x, to.y, to.z).color(new RenderColor(endColor)).next(); + mesh.line(i1, i2); + } + + public static void addImpactRing(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) { + if (mesh == null || center == null) return; + + Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6 + ? new Vec3(0.0, 1.0, 0.0) + : normal.normalize(); + Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0); + Vec3 tangent = useNormal.cross(axis); + if (tangent.lengthSqr() < 1.0E-6) { + tangent = new Vec3(1.0, 0.0, 0.0); + } else { + tangent = tangent.normalize(); + } + Vec3 bitangent = useNormal.cross(tangent).normalize(); + + for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) { + double angle0 = (Math.PI * 2.0 * i) / IMPACT_RING_SEGMENTS; + double angle1 = (Math.PI * 2.0 * (i + 1)) / IMPACT_RING_SEGMENTS; + Vec3 p0 = center + .add(tangent.scale(Math.cos(angle0) * radius)) + .add(bitangent.scale(Math.sin(angle0) * radius)); + Vec3 p1 = center + .add(tangent.scale(Math.cos(angle1) * radius)) + .add(bitangent.scale(Math.sin(angle1) * radius)); + addGradientLine(mesh, p0, p1, argb, argb); + } + + Vec3 crossTangent = tangent.scale(IMPACT_RING_CROSS_RADIUS); + Vec3 crossBitangent = bitangent.scale(IMPACT_RING_CROSS_RADIUS); + addGradientLine(mesh, center.subtract(crossTangent), center.add(crossTangent), argb, argb); + addGradientLine(mesh, center.subtract(crossBitangent), center.add(crossBitangent), argb, argb); + } + + public static void addImpactDecal(MeshBuilder mesh, Vec3 center, Vec3 normal, float radius, int argb) { + if (mesh == null || center == null || radius <= 0.0f) { + return; + } + + Vec3 useNormal = normal == null || normal.lengthSqr() < 1.0E-6 + ? new Vec3(0.0, 1.0, 0.0) + : normal.normalize(); + Vec3 axis = Math.abs(useNormal.y) < 0.95 ? new Vec3(0.0, 1.0, 0.0) : new Vec3(1.0, 0.0, 0.0); + Vec3 tangent = useNormal.cross(axis); + if (tangent.lengthSqr() < 1.0E-6) { + tangent = new Vec3(1.0, 0.0, 0.0); + } else { + tangent = tangent.normalize(); + } + Vec3 bitangent = useNormal.cross(tangent).normalize(); + Vec3 offsetCenter = center.add(useNormal.scale(IMPACT_DECAL_OFFSET)); + float quadRadius = radius * IMPACT_DECAL_QUAD_PADDING; + float sdfRadius = 1.0f / IMPACT_DECAL_QUAD_PADDING; + Vec3 tangentScaled = tangent.scale(quadRadius); + Vec3 bitangentScaled = bitangent.scale(quadRadius); + + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).x, + offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).y, + offsetCenter.subtract(tangentScaled).subtract(bitangentScaled).z) + .vec2(0.0, 0.0) + .color(new RenderColor(argb)) + .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) + .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) + .next(); + int i2 = mesh.vec3(offsetCenter.add(tangentScaled).subtract(bitangentScaled).x, + offsetCenter.add(tangentScaled).subtract(bitangentScaled).y, + offsetCenter.add(tangentScaled).subtract(bitangentScaled).z) + .vec2(1.0, 0.0) + .color(new RenderColor(argb)) + .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) + .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) + .next(); + int i3 = mesh.vec3(offsetCenter.add(tangentScaled).add(bitangentScaled).x, + offsetCenter.add(tangentScaled).add(bitangentScaled).y, + offsetCenter.add(tangentScaled).add(bitangentScaled).z) + .vec2(1.0, 1.0) + .color(new RenderColor(argb)) + .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) + .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) + .next(); + int i4 = mesh.vec3(offsetCenter.subtract(tangentScaled).add(bitangentScaled).x, + offsetCenter.subtract(tangentScaled).add(bitangentScaled).y, + offsetCenter.subtract(tangentScaled).add(bitangentScaled).z) + .vec2(0.0, 1.0) + .color(new RenderColor(argb)) + .vec4(sdfRadius, IMPACT_DECAL_EDGE_SOFTNESS, IMPACT_DECAL_STROKE_WIDTH, IMPACT_DECAL_PATTERN_SCALE) + .vec4(IMPACT_DECAL_FILL_ALPHA, IMPACT_DECAL_STROKE_ALPHA, IMPACT_DECAL_PATTERN_STRENGTH, 0.0f) + .next(); + mesh.quad(i1, i2, i3, i4); + } + + public static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, + float size, Quaternionf camRot, int argb) { + Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot).mul(size); + Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot).mul(size); + + double p1x = cx - right.x() - up.x(); + double p1y = cy - right.y() - up.y(); + double p1z = cz - right.z() - up.z(); + double p2x = cx + right.x() - up.x(); + double p2y = cy + right.y() - up.y(); + double p2z = cz + right.z() - up.z(); + double p3x = cx + right.x() + up.x(); + double p3y = cy + right.y() + up.y(); + double p3z = cz + right.z() + up.z(); + double p4x = cx - right.x() + up.x(); + double p4y = cy - right.y() + up.y(); + double p4z = cz - right.z() + up.z(); + + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0.0, 1.0).color(new RenderColor(argb)).next(); + int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(argb)).next(); + int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(argb)).next(); + int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(argb)).next(); + mesh.quad(i1, i2, i3, i4); + } + + public static int multiplyAlpha(int argb, float alphaMultiplier) { + int baseAlpha = (argb >>> 24) & 0xFF; + int alpha = Math.max(0, Math.min(255, Math.round(baseAlpha * Math.max(0.0f, alphaMultiplier)))); + return (argb & 0x00FFFFFF) | (alpha << 24); + } + + public static int withAlpha(int argb, int alpha) { + return (argb & 0x00FFFFFF) | (Math.max(0, Math.min(255, alpha)) << 24); + } + + public static double partialStepProgress(Vec3 from, Vec3 hitPos, double segmentLengthSq) { + if (from == null || hitPos == null || segmentLengthSq <= 1.0E-6) { + return 1.0; + } + return Mth.clamp(from.distanceTo(hitPos) / Math.sqrt(segmentLengthSq), 0.0, 1.0); + } + + public static Vec3 resolveImpactNormal(HitResult hit, Vec3 velocity) { + if (hit instanceof net.minecraft.world.phys.BlockHitResult blockHit) { + return Vec3.atLowerCornerOf(blockHit.getDirection().getUnitVec3i()); + } + if (velocity != null && velocity.lengthSqr() > 1.0E-6) { + return velocity.normalize(); + } + return new Vec3(0.0, 1.0, 0.0); + } + + public static Vec3 advanceTickVelocity(Entity entity, Vec3 velocity) { + return velocity.scale(0.99).add(0.0, -entity.getGravity(), 0.0); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java new file mode 100644 index 0000000..ee33f05 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java @@ -0,0 +1,157 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.predictions; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.projectile.FireworkRocketEntity; +import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball; +import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge; +import net.minecraft.world.entity.projectile.arrow.ThrownTrident; +import net.minecraft.world.entity.projectile.throwableitemprojectile.AbstractThrownPotion; +import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Shared value types and item/entity classification helpers for the Predictions module. + */ +public final class PredictionsModel { + private PredictionsModel() { + } + + public enum Kind { + PEARL_ENTITY, + ARROW, + TRIDENT, + FIREBALL, + WIND_CHARGE, + FIREWORK, + SNOWBALL, + EGG, + POTION_SPLASH, + POTION_LINGERING + } + + public record TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack, + int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity, + boolean hitBlock) { + public TrajectoryResult(List points, double flightTicks, long impactTick, ItemStack iconStack, + int argb, Kind kind, Vec3 impactPos, Vec3 impactNormal, Entity hitEntity, boolean hitBlock) { + this.points = points; + this.flightTicks = flightTicks; + this.impactTick = impactTick; + this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack; + this.argb = argb; + this.kind = kind; + this.impactPos = impactPos; + this.impactNormal = impactNormal; + this.hitEntity = hitEntity; + this.hitBlock = hitBlock; + } + } + + public record AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack, + TrajectoryResult trajectory) { + public AimPrediction(Entity entity, Vec3 hitPos, Vec3 anchorPos, Vec3 hitNormal, ItemStack displayStack, + TrajectoryResult trajectory) { + this.entity = entity; + this.hitPos = hitPos; + this.anchorPos = anchorPos; + this.hitNormal = hitNormal; + this.displayStack = displayStack == null ? ItemStack.EMPTY : displayStack; + this.trajectory = trajectory; + } + } + + public record EntityCollision(Entity entity, Vec3 hitPos, Vec3 hitNormal) { + } + + public record TimerPlate(float textX, float textY, String text, + float iconX, float iconY, float iconScale, ItemStack iconStack) { + public TimerPlate(float textX, float textY, String text, + float iconX, float iconY, float iconScale, ItemStack iconStack) { + this.textX = textX; + this.textY = textY; + this.text = text; + this.iconX = iconX; + this.iconY = iconY; + this.iconScale = iconScale; + this.iconStack = iconStack == null ? ItemStack.EMPTY : iconStack; + } + } + + public static Kind kindFromParams(ItemStack stack) { + if (stack == null) return Kind.ARROW; + if (stack.is(Items.TRIDENT)) return Kind.TRIDENT; + if (stack.is(Items.SPLASH_POTION)) return Kind.POTION_SPLASH; + if (stack.is(Items.LINGERING_POTION)) return Kind.POTION_LINGERING; + if (stack.is(Items.SNOWBALL)) return Kind.SNOWBALL; + if (stack.is(Items.EGG)) return Kind.EGG; + if (stack.is(Items.FIREWORK_ROCKET)) return Kind.FIREWORK; + if (stack.is(Items.WIND_CHARGE)) return Kind.WIND_CHARGE; + if (stack.is(Items.FIRE_CHARGE)) return Kind.FIREBALL; + if (stack.is(Items.ENDER_PEARL)) return Kind.PEARL_ENTITY; + return Kind.ARROW; + } + + public static Kind kindFromProjectile(Entity entity) { + if (entity instanceof ThrownTrident) return Kind.TRIDENT; + if (entity instanceof AbstractThrownPotion potion) { + return potion.getItem().is(Items.LINGERING_POTION) ? Kind.POTION_LINGERING : Kind.POTION_SPLASH; + } + if (entity instanceof Snowball) return Kind.SNOWBALL; + if (entity instanceof ThrownEgg) return Kind.EGG; + if (entity instanceof FireworkRocketEntity) return Kind.FIREWORK; + if (entity instanceof WindCharge) return Kind.WIND_CHARGE; + if (entity instanceof Fireball) return Kind.FIREBALL; + if (entity instanceof ThrownEnderpearl) return Kind.PEARL_ENTITY; + return Kind.ARROW; + } + + public static Vec3 resolveEntityImpactNormal(AABB box, Vec3 hitPos, Vec3 fallbackVelocity) { + if (box == null || hitPos == null) { + return PredictionsGeometry.resolveImpactNormal(null, fallbackVelocity); + } + + double dxMin = Math.abs(hitPos.x - box.minX); + double dxMax = Math.abs(box.maxX - hitPos.x); + double dyMin = Math.abs(hitPos.y - box.minY); + double dyMax = Math.abs(box.maxY - hitPos.y); + double dzMin = Math.abs(hitPos.z - box.minZ); + double dzMax = Math.abs(box.maxZ - hitPos.z); + + double best = dxMin; + Vec3 normal = new Vec3(-1.0, 0.0, 0.0); + if (dxMax < best) { + best = dxMax; + normal = new Vec3(1.0, 0.0, 0.0); + } + if (dyMin < best) { + best = dyMin; + normal = new Vec3(0.0, -1.0, 0.0); + } + if (dyMax < best) { + best = dyMax; + normal = new Vec3(0.0, 1.0, 0.0); + } + if (dzMin < best) { + best = dzMin; + normal = new Vec3(0.0, 0.0, -1.0); + } + if (dzMax < best) { + normal = new Vec3(0.0, 0.0, 1.0); + } + return normal; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java new file mode 100644 index 0000000..e626dbd --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java @@ -0,0 +1,196 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.predictions; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.EntityCollision; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.projectile.ProjectileUtil; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tick-by-tick projectile trajectory simulation and entity collision resolution + * for the Predictions module. + */ +public final class PredictionsSimulation { + private PredictionsSimulation() { + } + + public static final int HIT_SELF_COLOR = 0xFFFF4444; + public static final int HIT_ENTITY_COLOR = 0xFFFFCC44; + + public static TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta, + NumberValue maxSimTicks, + NumberValue pearlMaxSimTicks, + BooleanValue pearlColorToggle, + RGBColorValue pearlColorValue) { + Minecraft mc = Minecraft.getInstance(); + ClientLevel world = mc.level; + if (world == null) return null; + + Vec3 pos = e.position(); + Vec3 vel = e.getDeltaMovement(); + + List points = new ArrayList<>(); + points.add(pos); + + double t = 0.0; + int baseMaxTicks = Math.max(1, maxSimTicks.get()); + int maxTicks = kind == Kind.PEARL_ENTITY + ? Math.max(1, pearlMaxSimTicks.get()) + : baseMaxTicks; + int hitColor = 0; + long impactTick = world.getGameTime(); + Vec3 impactPos = null; + Vec3 impactNormal = null; + Entity hitEntity = null; + boolean hitBlock = false; + + for (int i = 0; i < maxTicks; i++) { + Vec3 segmentStart = pos; + Vec3 simulatedVelocity = PredictionsGeometry.advanceTickVelocity(e, vel); + Vec3 simulatedPos = pos.add(simulatedVelocity); + double segmentLengthSq = segmentStart.distanceToSqr(simulatedPos); + + HitResult hit = world.clip(new ClipContext( + segmentStart, + simulatedPos, + ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, + e + )); + + EntityCollision entityCollision = getEntityCollision(e, segmentStart, simulatedPos); + boolean hasBlockHit = hit.getType() != HitResult.Type.MISS; + boolean useEntityHit = entityCollision != null + && (!hasBlockHit || segmentStart.distanceToSqr(entityCollision.hitPos()) <= segmentStart.distanceToSqr(hit.getLocation())); + + if (useEntityHit) { + hitColor = (entityCollision.entity() == mc.player) ? HIT_SELF_COLOR : HIT_ENTITY_COLOR; + Vec3 entityHitPos = entityCollision.hitPos(); + t += PredictionsGeometry.partialStepProgress(segmentStart, entityHitPos, segmentLengthSq); + points.add(entityHitPos); + impactTick = world.getGameTime() + (long) Math.ceil(t); + impactPos = entityHitPos; + impactNormal = entityCollision.hitNormal(); + hitEntity = entityCollision.entity(); + break; + } else if (hasBlockHit) { + Vec3 blockHitPos = hit.getLocation(); + t += PredictionsGeometry.partialStepProgress(segmentStart, blockHitPos, segmentLengthSq); + points.add(blockHitPos); + impactTick = world.getGameTime() + (long) Math.ceil(t); + impactPos = blockHitPos; + impactNormal = PredictionsGeometry.resolveImpactNormal(hit, simulatedVelocity); + hitBlock = true; + break; + } + + pos = simulatedPos; + vel = simulatedVelocity; + t += 1.0; + points.add(pos); + impactTick = world.getGameTime() + (long) Math.ceil(t); + } + + ItemStack icon = switch (kind) { + case PEARL_ENTITY -> new ItemStack(Items.ENDER_PEARL); + case TRIDENT -> new ItemStack(Items.TRIDENT); + case FIREBALL -> new ItemStack(Items.FIRE_CHARGE); + case WIND_CHARGE -> new ItemStack(Items.WIND_CHARGE); + case FIREWORK -> new ItemStack(Items.FIREWORK_ROCKET); + case SNOWBALL -> new ItemStack(Items.SNOWBALL); + case EGG -> new ItemStack(Items.EGG); + case POTION_SPLASH -> new ItemStack(Items.SPLASH_POTION); + case POTION_LINGERING -> new ItemStack(Items.LINGERING_POTION); + default -> new ItemStack(Items.ARROW); + }; + + int color = hitColor; + if (color == 0 && kind == Kind.PEARL_ENTITY && pearlColorToggle.get()) { + color = pearlColorValue.getArgb(); + } + + return new TrajectoryResult(points, t, impactTick, icon, color, kind, impactPos, impactNormal, hitEntity, hitBlock); + } + + private static EntityCollision getEntityCollision(Entity movingEntity, Vec3 startPos, Vec3 endPos) { + Vec3 direction = endPos.subtract(startPos); + if (direction.lengthSqr() == 0.0) { + return null; + } + + AABB projectileBox = movingEntity.getBoundingBox().move(startPos.subtract(movingEntity.position())); + AABB collisionBox = projectileBox.expandTowards(direction).inflate(0.5); + EntityHitResult hitResult = ProjectileUtil.getEntityHitResult( + movingEntity, + startPos, + endPos, + collisionBox, + entity -> canHitPredictedEntity(movingEntity, entity, collisionBox) + && entity instanceof LivingEntity + && !(entity instanceof ItemEntity) + && !(entity instanceof ExperienceOrb) + && entity != movingEntity, + direction.lengthSqr() + ); + if (hitResult == null) { + return null; + } + + return new EntityCollision( + hitResult.getEntity(), + hitResult.getLocation(), + PredictionsModel.resolveEntityImpactNormal(hitResult.getEntity().getBoundingBox(), hitResult.getLocation(), direction) + ); + } + + private static boolean canHitPredictedEntity(Entity projectile, Entity entity, AABB collisionBox) { + if (projectile == null || entity == null || !entity.isAlive() || !entity.canBeHitByProjectile()) { + return false; + } + + if (projectile instanceof Projectile projectileEntity) { + Entity owner = projectileEntity.getOwner(); + if (owner != null && !hasPredictedLeftOwner(owner, entity, collisionBox)) { + return !owner.isPassengerOfSameVehicle(entity); + } + } + + return true; + } + + private static boolean hasPredictedLeftOwner(Entity owner, Entity entity, AABB collisionBox) { + if (owner == null || collisionBox == null) { + return true; + } + return owner.getRootVehicle() + .getSelfAndPassengers() + .filter(Entity::isPickable) + .noneMatch(passenger -> collisionBox.intersects(passenger.getBoundingBox())); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java new file mode 100644 index 0000000..ceb7f94 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java @@ -0,0 +1,179 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.targetesp; + +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*; + +/** + * Crystal orbit geometry for the TargetESP "crystals" mode. + */ +public final class TargetEspCrystals { + private TargetEspCrystals() { + } + + private static final float CRYSTAL_BASE_SIZE = 0.05f; + private static final int CRYSTAL_SIDES = 8; + + public static final class CrystalInstance { + private final Vec3 position; + private final Vec3 rotation; + private final float rotationSpeed; + + public CrystalInstance(Vec3 position, Vec3 rotation) { + this.position = position; + this.rotation = rotation; + this.rotationSpeed = 0.5f + (float) (Math.random() * 1.5f); + } + + private float selfRotationDeg() { + return (GameClock.millis() % 36000L) / 100.0f * rotationSpeed; + } + + private float pulsation() { + return 1.0f + (float) (Math.sin(GameClock.millis() / 500.0) * 0.1f); + } + + public void append(MeshBuilder additiveMesh, + MeshBuilder fillMesh, + MeshBuilder lineMesh, + MeshBuilder bloomMesh, + Vec3 targetPos, + float orbitYawRad, + int baseColor) { + appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.2f), true); + appendCrystalGeometry(fillMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.3f), true); + appendCrystalGeometry(lineMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.8f), false); + appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.2f, applyOpacity(baseColor, 0.3f), true); + appendBloomSphere(bloomMesh, targetPos, this, orbitYawRad, baseColor); + } + } + + private static void appendCrystalGeometry(MeshBuilder mesh, + Vec3 targetPos, + CrystalInstance crystal, + float orbitYawRad, + float scale, + int argb, + boolean filled) { + if (mesh == null || crystal == null || targetPos == null) { + return; + } + + float s = CRYSTAL_BASE_SIZE * scale; + float hPrism = s; + float hPyramid = s * 1.5f; + + Vector3f[] topVertices = new Vector3f[CRYSTAL_SIDES]; + Vector3f[] bottomVertices = new Vector3f[CRYSTAL_SIDES]; + for (int i = 0; i < CRYSTAL_SIDES; i++) { + float angle = (float) (2.0 * Math.PI * i / CRYSTAL_SIDES); + float x = (float) (s * Math.cos(angle)); + float z = (float) (s * Math.sin(angle)); + topVertices[i] = new Vector3f(x, hPrism * 0.5f, z); + bottomVertices[i] = new Vector3f(x, -hPrism * 0.5f, z); + } + + Vector3f topPeak = new Vector3f(0.0f, hPrism * 0.5f + hPyramid, 0.0f); + Vector3f bottomPeak = new Vector3f(0.0f, -hPrism * 0.5f - hPyramid, 0.0f); + + if (filled) { + for (int i = 0; i < CRYSTAL_SIDES; i++) { + Vector3f v1 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos); + Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); + Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); + Vector3f v4 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos); + addTriangle(mesh, v1, v2, v3, argb); + addTriangle(mesh, v1, v3, v4, argb); + } + for (int i = 0; i < CRYSTAL_SIDES; i++) { + Vector3f v1 = transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos); + Vector3f v2 = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos); + Vector3f v3 = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); + addTriangle(mesh, v1, v2, v3, argb); + } + for (int i = 0; i < CRYSTAL_SIDES; i++) { + Vector3f v1 = transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos); + Vector3f v2 = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); + Vector3f v3 = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos); + addTriangle(mesh, v1, v2, v3, argb); + } + return; + } + + for (int i = 0; i < CRYSTAL_SIDES; i++) { + Vector3f bottomA = transformCrystalVertex(bottomVertices[i], crystal, orbitYawRad, targetPos); + Vector3f bottomB = transformCrystalVertex(bottomVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); + Vector3f topA = transformCrystalVertex(topVertices[i], crystal, orbitYawRad, targetPos); + Vector3f topB = transformCrystalVertex(topVertices[(i + 1) % CRYSTAL_SIDES], crystal, orbitYawRad, targetPos); + addLine(mesh, bottomA, bottomB, argb); + addLine(mesh, topA, topB, argb); + addLine(mesh, bottomA, topA, argb); + addLine(mesh, transformCrystalVertex(topPeak, crystal, orbitYawRad, targetPos), topA, argb); + addLine(mesh, transformCrystalVertex(bottomPeak, crystal, orbitYawRad, targetPos), bottomA, argb); + } + } + + private static Vector3f transformCrystalVertex(Vector3f vertex, + CrystalInstance crystal, + float orbitYawRad, + Vec3 targetPos) { + Vector3f transformed = new Vector3f(vertex).mul(crystal.pulsation()); + transformed.rotateX((float) Math.toRadians(crystal.rotation.x)); + transformed.rotateY((float) Math.toRadians(crystal.rotation.y + crystal.selfRotationDeg())); + transformed.rotateZ((float) Math.toRadians(crystal.rotation.z)); + transformed.add((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z); + transformed.rotateY(orbitYawRad); + transformed.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z); + return transformed; + } + + private static void appendBloomSphere(MeshBuilder mesh, + Vec3 targetPos, + CrystalInstance crystal, + float orbitYawRad, + int argb) { + if (mesh == null || crystal == null || targetPos == null) { + return; + } + + Vector3f center = new Vector3f((float) crystal.position.x, (float) crystal.position.y, (float) crystal.position.z); + center.rotateY(orbitYawRad); + center.add((float) targetPos.x, (float) targetPos.y, (float) targetPos.z); + + float size = CRYSTAL_BASE_SIZE * 13.0f; + Quaternionf camRot = RenderState.cameraRotation; + float yaw = orbitYawRad; + int bloomColor = applyOpacity(argb, (0.4f * 25.0f) / 255.0f); + + for (int i = 0; i < 6; i++) { + float angle = (360.0f / 6.0f) * i; + Quaternionf rotation = new Quaternionf() + .rotateY((float) Math.toRadians(angle)) + .rotateY(yaw) + .mul(camRot); + addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor); + } + + for (int i = 0; i < 6; i++) { + float angle = (360.0f / 6.0f) * i; + Quaternionf rotation = new Quaternionf() + .rotateX((float) Math.toRadians(90.0f)) + .rotateY((float) Math.toRadians(angle)) + .rotateY(yaw) + .mul(camRot); + addTexturedBillboard(mesh, center.x, center.y, center.z, size, rotation, bloomColor); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java new file mode 100644 index 0000000..cc1bfb9 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java @@ -0,0 +1,178 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.targetesp; + +import dev.loki.lovisual.render.engine.TextureStorage; +import dev.loki.lovisual.render.engine.math.RenderMath; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; + +import java.util.function.IntFunction; + +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*; + +public final class TargetEspEffects { + private TargetEspEffects() { + } + + private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY; + + private static final float[][] BODY_HOLES = { + {0.0f, 0.60f}, // голова + {-0.48f, 0.36f}, // левая рука + {0.48f, 0.36f}, // правая рука + {0.0f, 0.05f}, // грудь + {-0.30f, -0.42f},// левая нога + {0.30f, -0.42f} // правая нога + }; + + public static void renderGhosts(Renderer3D renderer, + int espLength, int factor, float shaking, float amplitude, + Entity target, float tickDelta, IntFunction palette) { + Minecraft mc = Minecraft.getInstance(); + if (renderer == null || mc == null || mc.player == null) return; + + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + double tPosX = pos.x; + double tPosY = pos.y; + double tPosZ = pos.z; + float iAge = (float) interpolate(target.tickCount - 1, target.tickCount, tickDelta); + + MeshBuilder mesh = renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, + GHOST_PARTICLE_TEXTURE, + Renderer3D.DepthMode.MAIN + ); + if (mesh == null) return; + + for (int j = 0; j < 3; j++) { + for (int i = 0; i <= espLength; i++) { + double radians = Math.toRadians((((float) i / 1.5f + iAge) * factor + (j * 120)) % (factor * 360)); + double sinQuad = Math.sin(Math.toRadians(iAge * 2.5f + i * (j + 1)) * amplitude) / shaking; + + float offset = ((float) i / espLength); + int color = applyOpacity(palette.apply((int) (180 * offset)), offset); + float scale = Math.max(0.24f * (offset), 0.2f); + + double cx = tPosX + Math.cos(radians) * target.getBbWidth(); + double cy = tPosY + 1 + sinQuad; + double cz = tPosZ + Math.sin(radians) * target.getBbWidth(); + + addBillboardQuad(mesh, cx, cy, cz, scale, color); + } + } + } + + public static void renderBlackhole(Renderer3D renderer, + LivingEntity target, + float tickDelta, + IntFunction palette, + float size, + float spinSpeed, + float alpha, + String style) { + if (renderer == null || target == null) return; + Minecraft mc = Minecraft.getInstance(); + if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return; + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + Vec3 baseCenter = new Vec3(pos.x, pos.y + target.getBbHeight() * 0.5, pos.z); + + // Камера-зависимый billboard-базис: правый/верх в мировых координатах + Camera cam = mc.gameRenderer.mainCamera(); + float yaw = (float) Math.toRadians(cam.yRot()); + float pitch = (float) Math.toRadians(cam.xRot()); + Vec3 fwd = new Vec3(-Math.sin(yaw) * Math.cos(pitch), -Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch)).normalize(); + Vec3 rgt = new Vec3(0.0, 1.0, 0.0).cross(fwd); + if (rgt.lengthSqr() < 1.0E-6) { + rgt = new Vec3(1.0, 0.0, 0.0); + } else { + rgt = rgt.normalize(); + } + Vec3 upp = fwd.cross(rgt).normalize(); + Vector3f right = new Vector3f((float) rgt.x, (float) rgt.y, (float) rgt.z); + Vector3f up = new Vector3f((float) upp.x, (float) upp.y, (float) upp.z); + + MeshBuilder diskMesh = renderer.batch( + LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, + Renderer3D.DepthMode.PRE_DEPTH + ); + MeshBuilder coreMesh = renderer.batch( + LoVisualRenderPipelines.WORLD_COLORED_DEPTH, + Renderer3D.DepthMode.PRE_DEPTH + ); + if (diskMesh == null || coreMesh == null) return; + + float time = (GameClock.millis() % 100000L) / 1000.0f; + + if ("Body".equals(style)) { + float holeSize = size * 0.34f; + for (float[] off : BODY_HOLES) { + Vec3 center = new Vec3( + baseCenter.x + rgt.x * off[0] + upp.x * off[1], + baseCenter.y + rgt.y * off[0] + upp.y * off[1], + baseCenter.z + rgt.z * off[0] + upp.z * off[1] + ); + appendBlackhole(diskMesh, coreMesh, center, right, up, holeSize, time, spinSpeed, palette, alpha); + } + } else { + appendBlackhole(diskMesh, coreMesh, baseCenter, right, up, size, time, spinSpeed, palette, alpha); + } + } + + private static void appendBlackhole(MeshBuilder diskMesh, + MeshBuilder coreMesh, + Vec3 center, + Vector3f right, + Vector3f up, + float size, + float time, + float spinSpeed, + IntFunction palette, + float alpha) { + float horizon = 0.35f * size; + int rings = 4; + int segs = 64; + + // аккреционный диск: вращающаяся спираль на аддитивном бленде + for (int ring = 0; ring < rings; ring++) { + float r0 = horizon + (size - horizon) * (ring / (float) rings); + float r1 = horizon + (size - horizon) * ((ring + 1) / (float) rings); + for (int s = 0; s < segs; s++) { + float a0 = (float) (Math.PI * 2.0 * s / segs); + float a1 = (float) (Math.PI * 2.0 * (s + 1) / segs); + int v0 = addBlackholeVertex(diskMesh, center, right, up, r0, a0, horizon, size, time, spinSpeed, palette); + int v1 = addBlackholeVertex(diskMesh, center, right, up, r1, a0, horizon, size, time, spinSpeed, palette); + int v2 = addBlackholeVertex(diskMesh, center, right, up, r1, a1, horizon, size, time, spinSpeed, palette); + int v3 = addBlackholeVertex(diskMesh, center, right, up, r0, a1, horizon, size, time, spinSpeed, palette); + diskMesh.triangle(v0, v1, v2); + diskMesh.triangle(v0, v2, v3); + } + } + + // чёрное ядро закрывает центр диска + int coreArgb = applyOpacity(0xFF080808, alpha); + int coreCenter = addBlackholeCoreVertex(coreMesh, center, right, up, 0f, 0f, coreArgb); + int coreSegs = 32; + for (int s = 0; s < coreSegs; s++) { + float a0 = (float) (Math.PI * 2.0 * s / coreSegs); + float a1 = (float) (Math.PI * 2.0 * (s + 1) / coreSegs); + int c0 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a0, coreArgb); + int c1 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a1, coreArgb); + coreMesh.triangle(coreCenter, c1, c0); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java new file mode 100644 index 0000000..9adb689 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java @@ -0,0 +1,199 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.targetesp; + +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.TextureStorage; +import dev.loki.lovisual.render.engine.math.RenderMath; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; + +import java.util.function.IntFunction; + +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*; + +/** + * State-driven TargetESP modes: animation state, the legacy ring, ghost + * particles and the capture mark. + */ +public final class TargetEspModes { + private TargetEspModes() { + } + + private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE; + private static final int VORTEX_HELIX_STEPS = 28; + private static final int VORTEX_RING_SEGMENTS = 24; + + private static float prevCircleStep; + private static float circleStep; + private static double captureValue = 1.0; + private static double prevCaptureValue; + private static double captureSpeed = 1.0; + private static double captureSpeedScale = 1.0; + private static boolean captureFlipSpeed; + + public static void tickTargetEsp() { + prevCircleStep = circleStep; + circleStep += 0.15f; + + prevCaptureValue = captureValue; + captureValue += captureSpeed * captureSpeedScale; + if (captureSpeed > 25.0) captureFlipSpeed = true; + if (captureSpeed < -25.0) captureFlipSpeed = false; + captureSpeed = captureFlipSpeed + ? captureSpeed - 0.5 * captureSpeedScale + : captureSpeed + 0.5 * captureSpeedScale; + } + + public static void setCaptureSpeedScale(float scale) { + if (scale <= 0f) { + captureSpeedScale = 0.05; + } else { + captureSpeedScale = scale; + } + } + + public static void drawOldTargetEsp(Renderer3D renderer, Entity target, float tickDelta, IntFunction palette) { + if (renderer == null || target == null) return; + double cs = prevCircleStep + (circleStep - prevCircleStep) * tickDelta; + double prevSinAnim = absSinAnimation(cs - 0.45f); + double sinAnim = absSinAnimation(cs); + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + double x = pos.x; + double y = pos.y + prevSinAnim * target.getBbHeight(); + double z = pos.z; + double nextY = pos.y + sinAnim * target.getBbHeight(); + + MeshBuilder mesh = renderer.tris(); + + final int segments = 96; + final double step = Math.PI * 2.0 / segments; + final double radius = target.getBbWidth() * 0.8; + + for (int i = 0; i < segments; i++) { + double a0 = i * step; + double a1 = (i + 1) * step; + float cos = (float) (x + Math.cos(a0) * radius); + float sin = (float) (z + Math.sin(a0) * radius); + float cosNext = (float) (x + Math.cos(a1) * radius); + float sinNext = (float) (z + Math.sin(a1) * radius); + + int idx0 = (int) Math.round(i * (360.0 / segments)); + int idx1 = (int) Math.round((i + 1) * (360.0 / segments)); + int cTop0 = applyOpacity(palette.apply(idx0), 170.0f / 255.0f); + int cTop1 = applyOpacity(palette.apply(idx1), 170.0f / 255.0f); + int cBottom0 = applyOpacity(palette.apply(idx0), 0.0f); + int cBottom1 = applyOpacity(palette.apply(idx1), 0.0f); + + mesh.ensureQuadCapacity(); + int i1 = addVertex(mesh, cos, nextY, sin, cTop0); + int i2 = addVertex(mesh, cosNext, nextY, sinNext, cTop1); + int i3 = addVertex(mesh, cosNext, y, sinNext, cBottom1); + int i4 = addVertex(mesh, cos, y, sin, cBottom0); + mesh.quad(i1, i2, i3, i4); + } + + } + + public static void renderVortex(Renderer3D renderer, + LivingEntity target, + float tickDelta, + IntFunction palette, + float spinSpeed) { + if (renderer == null || target == null) return; + + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + float radius = target.getBbWidth() * 0.7f; + float height = target.getBbHeight(); + double time = (GameClock.millis() / 1000.0) * spinSpeed; + + float prevWidth = RenderState.lineWidth; + RenderState.lineWidth = 1.4f; + MeshBuilder mesh; + try { + mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE); + } finally { + RenderState.lineWidth = prevWidth; + } + if (mesh == null) return; + + for (int strand = 0; strand < 2; strand++) { + double strandOffset = strand * Math.PI; + int prevIndex = -1; + for (int i = 0; i <= VORTEX_HELIX_STEPS; i++) { + double t = (double) i / VORTEX_HELIX_STEPS; + double angle = time * 2.4 + t * Math.PI * 3.0 + strandOffset; + double r = radius * (0.85 + 0.15 * Math.sin(t * Math.PI)); + double x = pos.x + Math.cos(angle) * r; + double y = pos.y + t * height; + double z = pos.z + Math.sin(angle) * r; + int color = applyOpacity(palette.apply((int) (t * 180)), (float) (1.0 - t * 0.35)); + + int index = addVertex(mesh, x, y, z, color); + if (prevIndex >= 0) { + mesh.line(prevIndex, index); + } + prevIndex = index; + } + } + + float[] ringHeights = {0.05f, height * 0.5f, height * 0.95f}; + float[] ringSpeeds = {0.4f, -0.55f, 0.35f}; + for (int ring = 0; ring < ringHeights.length; ring++) { + double ringY = pos.y + ringHeights[ring]; + double ringRotation = time * ringSpeeds[ring]; + int ringColor = applyOpacity(palette.apply(60), 0.4f); + int prevIndex = -1; + int firstIndex = -1; + for (int i = 0; i <= VORTEX_RING_SEGMENTS; i++) { + double angle = ringRotation + (i % VORTEX_RING_SEGMENTS) * 2.0 * Math.PI / VORTEX_RING_SEGMENTS; + double x = pos.x + Math.cos(angle) * radius; + double z = pos.z + Math.sin(angle) * radius; + int index = addVertex(mesh, x, ringY, z, ringColor); + if (firstIndex < 0) { + firstIndex = index; + } + if (prevIndex >= 0) { + mesh.line(prevIndex, index); + } + prevIndex = index; + } + } + } + + public static void renderCaptureMark(Renderer3D renderer, + Entity target, + float tickDelta, + IntFunction palette) { + if (renderer == null) return; + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + double tPosX = pos.x; + double tPosY = pos.y; + double tPosZ = pos.z; + + MeshBuilder mesh = renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, + CAPTURE_MARK_TEXTURE, + Renderer3D.DepthMode.MAIN + ); + if (mesh == null) return; + + float angle = interpolateFloat((float) prevCaptureValue, (float) captureValue, tickDelta); + float size = 0.75f; + + double cy = tPosY + target.getEyeHeight(target.getPose()) / 2f; + + addRotatedBillboard(mesh, tPosX, cy, tPosZ, size, angle, palette); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java new file mode 100644 index 0000000..5db1cf1 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java @@ -0,0 +1,199 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.combat.targetesp; + +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.function.IntFunction; + +public final class TargetEspRender { + private TargetEspRender() { + } + + public static void addTriangle(MeshBuilder mesh, Vector3f v1, Vector3f v2, Vector3f v3, int argb) { + mesh.ensureTriCapacity(); + int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb); + int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb); + int i3 = addVertex(mesh, v3.x, v3.y, v3.z, argb); + mesh.triangle(i1, i2, i3); + } + + public static void addLine(MeshBuilder mesh, Vector3f v1, Vector3f v2, int argb) { + mesh.ensureLineCapacity(); + int i1 = addVertex(mesh, v1.x, v1.y, v1.z, argb); + int i2 = addVertex(mesh, v2.x, v2.y, v2.z, argb); + mesh.line(i1, i2); + } + + public static int addVertex(MeshBuilder mesh, double x, double y, double z, int argb) { + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + mesh.vec3(x, y, z).color(r, g, b, a); + return mesh.next(); + } + + public static int addVertex(MeshBuilder mesh, double x, double y, double z, double u, double v, int argb) { + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + mesh.vec3(x, y, z).vec2(u, v).color(r, g, b, a); + return mesh.next(); + } + + public static void addBillboardQuad(MeshBuilder mesh, + double cx, double cy, double cz, + float size, int argb) { + Quaternionf camRot = RenderState.cameraRotation; + Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); + Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); + addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb); + } + + public static void addTexturedBillboard(MeshBuilder mesh, + double cx, double cy, double cz, + float size, Quaternionf rotation, int argb) { + Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(rotation).mul(size * 0.5f); + Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(rotation).mul(size * 0.5f); + addBillboardVertices(mesh, cx, cy, cz, right, up, argb, argb, argb, argb); + } + + public static void addRotatedBillboard(MeshBuilder mesh, double cx, double cy, double cz, + float size, float angle, IntFunction palette) { + Quaternionf camRot = RenderState.cameraRotation; + Vector3f right = new Vector3f(1, 0, 0).rotate(camRot); + Vector3f up = new Vector3f(0, 1, 0).rotate(camRot); + + Vector3f r = new Vector3f(right).mul(size); + Vector3f u = new Vector3f(up).mul(size); + + float sin = Mth.sin((float) Math.toRadians(angle)); + float cos = Mth.cos((float) Math.toRadians(angle)); + + Vector3f rRot = new Vector3f( + r.x() * cos - u.x() * sin, + r.y() * cos - u.y() * sin, + r.z() * cos - u.z() * sin + ); + Vector3f uRot = new Vector3f( + r.x() * sin + u.x() * cos, + r.y() * sin + u.y() * cos, + r.z() * sin + u.z() * cos + ); + + addBillboardVertices(mesh, cx, cy, cz, rRot, uRot, + palette.apply(90), + palette.apply(0), + palette.apply(180), + palette.apply(270)); + } + + public static void addBillboardVertices(MeshBuilder mesh, double cx, double cy, double cz, + Vector3f right, Vector3f up, + int c1, int c2, int c3, int c4) { + double p1x = cx - right.x() - up.x(); + double p1y = cy - right.y() - up.y(); + double p1z = cz - right.z() - up.z(); + double p2x = cx + right.x() - up.x(); + double p2y = cy + right.y() - up.y(); + double p2z = cz + right.z() - up.z(); + double p3x = cx + right.x() + up.x(); + double p3y = cy + right.y() + up.y(); + double p3z = cz + right.z() + up.z(); + double p4x = cx - right.x() + up.x(); + double p4y = cy - right.y() + up.y(); + double p4z = cz - right.z() + up.z(); + + mesh.ensureQuadCapacity(); + int i1 = addVertex(mesh, p1x, p1y, p1z, 0, 1, c1); + int i2 = addVertex(mesh, p2x, p2y, p2z, 1, 1, c2); + int i3 = addVertex(mesh, p3x, p3y, p3z, 1, 0, c3); + int i4 = addVertex(mesh, p4x, p4y, p4z, 0, 0, c4); + mesh.quad(i1, i2, i3, i4); + } + + public static int applyOpacity(int argb, float opacity) { + opacity = Math.min(1f, Math.max(0f, opacity)); + int baseA = (argb >>> 24) & 0xFF; + int a = Mth.clamp((int) (baseA * opacity), 0, 255); + return (a << 24) | (argb & 0x00FFFFFF); + } + + public static int addBlackholeVertex(MeshBuilder mesh, + Vec3 center, + Vector3f right, + Vector3f up, + float r, + float angle, + float horizon, + float size, + float time, + float spinSpeed, + IntFunction palette) { + float dist = r / size; + float rot = time * 2.5f * spinSpeed + 1.3f / (dist + 0.05f); + float ca = (float) Math.cos(rot); + float sa = (float) Math.sin(rot); + float ux = dist * (float) Math.cos(angle); + float uy = dist * (float) Math.sin(angle); + float rx = ux * ca - uy * sa; + float ry = ux * sa + uy * ca; + float spiral = Math.abs((float) Math.sin(rx * 14.0f + ry * 4.0f) * 0.5f + 0.5f); + float falloff = (float) Math.pow(horizon / Math.max(dist, 0.001f), 2.2); + float outerFade = smoothstep(1.0f, horizon, dist); + float brightness = falloff * (0.55f + 0.45f * spiral) * outerFade; + + int color = palette.apply((int) ((Math.toDegrees(angle) + time * 90f * spinSpeed) % 360f)); + color = applyOpacity(color, brightness); + + float wx = (float) center.x + right.x() * ux * size + up.x() * uy * size; + float wy = (float) center.y + right.y() * ux * size + up.y() * uy * size; + float wz = (float) center.z + right.z() * ux * size + up.z() * uy * size; + return addVertex(mesh, wx, wy, wz, color); + } + + public static int addBlackholeCoreVertex(MeshBuilder mesh, + Vec3 center, + Vector3f right, + Vector3f up, + float r, + float angle, + int argb) { + float ux = r * (float) Math.cos(angle); + float uy = r * (float) Math.sin(angle); + float wx = (float) center.x + right.x() * ux + up.x() * uy; + float wy = (float) center.y + right.y() * ux + up.y() * uy; + float wz = (float) center.z + right.z() * ux + up.z() * uy; + return addVertex(mesh, wx, wy, wz, argb); + } + + public static double absSinAnimation(double input) { + return Math.abs(1 + Math.sin(input)) / 2; + } + + public static double interpolate(double oldValue, double newValue, double interpolationValue) { + return (oldValue + (newValue - oldValue) * interpolationValue); + } + + public static float interpolateFloat(float oldValue, float newValue, double interpolationValue) { + return (float) interpolate(oldValue, newValue, interpolationValue); + } + + private static float smoothstep(float edge0, float edge1, float x) { + float t = (x - edge0) / (edge1 - edge0); + t = Math.max(0f, Math.min(1f, t)); + return t * t * (3f - 2f * t); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java new file mode 100644 index 0000000..e9b0e03 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoServerPack.java @@ -0,0 +1,105 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import dev.loki.lovisual.config.values.ModeValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import net.minecraft.client.Minecraft; + +import java.util.UUID; + +/** + * Blocks forced server resource packs. + * + *

Decline/Remove answer every push with a terminal + * {@code ServerboundResourcePackPacket.Action.DECLINED} before vanilla shows + * its prompt or starts a download; Remove additionally drops packs that are + * already applied.

+ */ +@ModuleInfo( + id = "noserverpack", + displayName = "No Server Pack", + aliases = {"noserverpack", "nopack"}, + category = ModuleCategory.MISC +) +public class NoServerPack extends Module { + private static final String SETTING_ACTION = "action"; + + private final ModeValue action = + mode("noServerPackAction", SETTING_ACTION, Action.DECLINE.id, Action.REMOVE.id, Action.ASK.id); + + private boolean clearedAppliedPacks; + + public Action action() { + return Action.byId(action.get()); + } + + /** + * Packet-hook decision. Never touches the network or GUI from here. + * + * @return true when the push must be cancelled and answered with DECLINED + */ + public boolean shouldDeclinePush() { + return shouldDeclinePush(isEnabled(), action.get()); + } + + /** Called from the packet hook after a push was answered with DECLINED. */ + public void onPackDeclined(UUID packId) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || packId == null) return; + mc.execute(() -> Notifier.info("Server resource pack declined (" + packId + ")")); + } + + @Override + public void onTick() { + if (!isEnabled()) { + clearedAppliedPacks = false; + return; + } + if (action() == Action.REMOVE) { + clearAppliedPacksOnce(); + } else { + clearedAppliedPacks = false; + } + } + + private void clearAppliedPacksOnce() { + if (clearedAppliedPacks) return; + clearedAppliedPacks = true; + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getDownloadedPackSource() == null) return; + mc.getDownloadedPackSource().popAll(); + Notifier.info("Applied server packs removed"); + } + + static boolean shouldDeclinePush(boolean enabled, String mode) { + return enabled && Action.byId(mode) != Action.ASK; + } + + public enum Action { + DECLINE("Decline"), + REMOVE("Remove"), + ASK("Ask"); + + public final String id; + + Action(String id) { + this.id = id; + } + + static Action byId(String id) { + for (Action value : values()) { + if (value.id.equals(id)) return value; + } + return DECLINE; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java new file mode 100644 index 0000000..9f93a31 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoRespawn.java @@ -0,0 +1,88 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Minecraft; + +/** + * Automatically presses the vanilla respawn path after death, once per death + * and only after a configurable delay. + */ +@ModuleInfo( + id = "autorespawn", + displayName = "Auto Respawn", + aliases = {"autorespawn", "respawn"}, + category = ModuleCategory.PLAYER +) +public class AutoRespawn extends Module { + private static final String SETTING_DELAY = "delay"; + + private final NumberValue delay = num("autoRespawnDelay", SETTING_DELAY, 500, 0, 5000); + + private final RespawnGate gate = new RespawnGate(); + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + gate.reset(); + return; + } + + boolean dead = mc.player.isDeadOrDying(); + long now = GameClock.millis(); + if (!gate.shouldRequestRespawn(dead, now, delay.get())) return; + gate.onRequestSent(); + + // Exactly what the vanilla death screen "Respawn" button does. + mc.player.respawn(); + } + + /** + * Pure death/delay state machine: one request per death, reset by coming + * back to life. Unit-testable without Minecraft. + */ + public static final class RespawnGate { + private boolean dead; + private long deathStartedMs; + private boolean requestSent; + + /** + * @return true when a respawn request should be sent right now + */ + public boolean shouldRequestRespawn(boolean isDead, long nowMs, long delayMs) { + if (!isDead) { + reset(); + return false; + } + if (!this.dead) { + this.dead = true; + this.deathStartedMs = nowMs; + this.requestSent = false; + return false; + } + if (requestSent) return false; + return nowMs - deathStartedMs >= Math.max(0L, delayMs); + } + + public void onRequestSent() { + requestSent = true; + } + + public void reset() { + dead = false; + requestSent = false; + deathStartedMs = 0L; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java new file mode 100644 index 0000000..1d336f0 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java @@ -0,0 +1,91 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.features.command.CommandOutput; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import net.minecraft.client.Minecraft; + +import java.util.Locale; + +/** + * Reports the position of your latest death as a client-side chat message and + * HUD notification, once per death. + */ +@ModuleInfo( + id = "deathcoords", + displayName = "Death Coords", + aliases = {"deathcoords"}, + category = ModuleCategory.PLAYER +) +public class DeathCoords extends Module { + private static final String SETTING_SHOW_DIMENSION = "show_dimension"; + + private final BooleanValue showDimension = + bool("deathCoordsShowDimension", SETTING_SHOW_DIMENSION, true); + + private final DeathDetector detector = new DeathDetector(); + + @Override + public void onDisable() { + detector.reset(); + } + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.level == null) { + detector.reset(); + return; + } + + if (!detector.onTick(mc.player.isDeadOrDying())) return; + + String message = formatDeathMessage( + mc.player.getX(), + mc.player.getY(), + mc.player.getZ(), + mc.level.dimension().identifier().toString(), + showDimension.get() + ); + CommandOutput.warning(message); + Notifier.warning(message); + } + + /** + * Pure alive-to-dead edge detector, unit-testable without Minecraft. + */ + public static final class DeathDetector { + private boolean wasDead; + + /** + * @return true exactly on the tick the player transitions to dead + */ + public boolean onTick(boolean dead) { + boolean justDied = dead && !wasDead; + wasDead = dead; + return justDied; + } + + public void reset() { + wasDead = false; + } + } + + static String formatDeathMessage(double x, double y, double z, String dimension, boolean withDimension) { + String coords = String.format(Locale.ROOT, "%.2f, %.2f, %.2f", x, y, z); + if (withDimension && dimension != null && !dimension.isBlank()) { + return "You died at " + coords + " (" + dimension + ")"; + } + return "You died at " + coords; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java new file mode 100644 index 0000000..7ff96d6 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/LockSlot.java @@ -0,0 +1,92 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BooleanMapValue; +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import dev.loki.lovisual.util.protection.NotifyThrottle; +import dev.loki.lovisual.util.protection.SlotLockPolicy; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Util; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.level.Level; + +/** + * Protects hotbar slots 1-9 from accidental drops. + * Covers the Q drop of the selected hotbar slot and THROW clicks on the + * player's hotbar slots inside container screens. + */ +@ModuleInfo( + id = "lockslot", + displayName = "Lock Slot", + aliases = {"lockslot"}, + category = ModuleCategory.PLAYER, + description = "module.lockslot.description" +) +public final class LockSlot extends Module { + + private static final double NEARBY_RADIUS_SQR = 32.0 * 32.0; + private static final long WARN_INTERVAL_MS = 1500L; + + private final BooleanMapValue slots = group("lockslot_slots", "slots", SlotLockPolicy.defaultSlotStates()); + private final BooleanValue onlyPlayersNearby = bool("lockslot_only_players_nearby", "only_players_nearby", true); + + private final NotifyThrottle warnThrottle = new NotifyThrottle(WARN_INTERVAL_MS); + + /** + * Path A: Q / Ctrl+Q drop of the selected hotbar slot (LocalPlayer.drop). + */ + public boolean shouldBlockSelectedDrop() { + if (!isEnabled()) return false; + LocalPlayer player = Minecraft.getInstance().player; + if (player == null) return false; + return shouldBlockDrop(player, player.getInventory().getSelectedSlot()); + } + + /** + * Path B: THROW container clicks targeting a hotbar slot in an open screen. + */ + public boolean shouldBlockContainerThrow(Player player, int containerId, int slotId) { + if (!isEnabled() || player == null) return false; + if (player.containerMenu == null || player.containerMenu.containerId != containerId) return false; + if (slotId < 0 || slotId >= player.containerMenu.slots.size()) return false; + Slot slot = player.containerMenu.slots.get(slotId); + if (slot.container != player.getInventory()) return false; + return shouldBlockDrop(player, slot.getContainerSlot()); + } + + private boolean shouldBlockDrop(Player player, int hotbarSlot) { + boolean playersNearby = hasPlayersNearby(player); + if (!SlotLockPolicy.isDropBlocked(slots.getAll(), hotbarSlot, playersNearby, onlyPlayersNearby.get())) { + return false; + } + notifyBlocked(hotbarSlot); + return true; + } + + private boolean hasPlayersNearby(Player self) { + Level level = self.level(); + if (level == null) return false; + for (Player other : level.players()) { + if (other == self || other.isSpectator()) continue; + if (other.distanceToSqr(self) <= NEARBY_RADIUS_SQR) return true; + } + return false; + } + + private void notifyBlocked(int hotbarSlot) { + if (!warnThrottle.tryAcquire(Util.getMillis())) return; + Notifier.warning("Slot " + (hotbarSlot + 1) + " is locked: drop blocked"); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java new file mode 100644 index 0000000..53bca79 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/TapeMouse.java @@ -0,0 +1,92 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.ModeValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.mixins.accessors.KeyMappingAccessor; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; + +/** + * Repeats the selected mouse button on an interval, mirroring real clicks by + * feeding vanilla {@code KeyMapping.click(...)} so {@code handleKeybinds} + * processes them itself. + */ +@ModuleInfo( + id = "tapemouse", + displayName = "Tape Mouse", + aliases = {"tapemouse", "tape"}, + category = ModuleCategory.PLAYER +) +public class TapeMouse extends Module { + private static final String SETTING_BUTTON = "button"; + private static final String SETTING_INTERVAL = "interval"; + private static final String SETTING_ONLY_HOLD = "only_hold"; + + private static final String BUTTON_ATTACK = "Attack"; + private static final String BUTTON_USE = "Use"; + + private final ModeValue button = mode("tapeMouseButton", SETTING_BUTTON, BUTTON_ATTACK, BUTTON_USE); + private final NumberValue interval = num("tapeMouseInterval", SETTING_INTERVAL, 1000, 100, 5000); + private final BooleanValue onlyHold = bool("tapeMouseOnlyHold", SETTING_ONLY_HOLD, true); + + private final ClickPacer pacer = new ClickPacer(); + + @Override + public void onEnable() { + pacer.reset(); + } + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.options == null) return; + if (mc.gui == null || mc.gui.screen() != null) return; + + boolean attack = BUTTON_ATTACK.equals(button.get()); + KeyMapping mapping = attack ? mc.options.keyAttack : mc.options.keyUse; + if (mapping == null) return; + if (onlyHold.get() && !mapping.isDown()) return; + + long now = GameClock.millis(); + if (!pacer.isDue(now, interval.get())) return; + pacer.mark(now); + + // Identical to a physical press: vanilla MouseHandler calls the same + // static KeyMapping.click for real input events. + KeyMapping.click(((KeyMappingAccessor) (Object) mapping).lovisual$key()); + } + + /** + * Pure interval pacing logic, unit-testable without Minecraft. + */ + public static final class ClickPacer { + private long lastClickMs; + private boolean primed; + + public boolean isDue(long nowMs, long intervalMs) { + long safeInterval = Math.max(1L, intervalMs); + return !primed || nowMs - lastClickMs >= safeInterval; + } + + public void mark(long nowMs) { + lastClickMs = nowMs; + primed = true; + } + + public void reset() { + primed = false; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java new file mode 100644 index 0000000..e9ce246 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Animations.java @@ -0,0 +1,110 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.util.animation.SmoothValue; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; + +/** + * Smooth appear/disappear animations for interface elements. + *

+ * Every animated quantity is a {@link SmoothValue} whose boolean target is + * computed and advanced in exactly one place: {@link #onFrame(float)}. Renderer + * integration points (custom hotbar, container screens) only read the settled + * per-frame values through the accessors below, so values can never be pulled + * toward two different goals in the same frame. + */ +@ModuleInfo( + id = "animations", + displayName = "Animations", + aliases = {"animations"}, + category = ModuleCategory.VISUALS, + description = "module.animations.description" +) +public final class Animations extends Module { + + private static final String SETTING_HOTBAR_RAISE = "hotbar_raise"; + private static final String SETTING_INVENTORY_OPEN = "inventory"; + + // Speeds are SmoothValue approach rates in 1/tick units (20 ticks per second). + // Inventory: ~0.15s to visually settle (0.9 -> ~1.0). + private static final float INVENTORY_OPEN_SPEED = 1.5f; + // Hotbar: softer slide-in, ~0.3s. + private static final float HOTBAR_RAISE_SPEED = 0.5f; + + private static final float INVENTORY_MIN_SCALE = 0.9f; + private static final float HOTBAR_RAISE_OFFSET_PX = 26.0f; + + private final BooleanValue hotbarRaise = + bool("animationsHotbarRaise", SETTING_HOTBAR_RAISE, true); + private final BooleanValue inventoryOpen = + bool("animationsInventoryOpen", SETTING_INVENTORY_OPEN, true); + + private final SmoothValue hotbarRaiseValue = new SmoothValue(HOTBAR_RAISE_SPEED); + private final SmoothValue inventoryOpenValue = new SmoothValue(INVENTORY_OPEN_SPEED); + + @Override + public void onEnable() { + Minecraft mc = Minecraft.getInstance(); + + // If a container screen is already open, start settled so enabling + // mid-game never pops the screen. Otherwise the open animation plays + // naturally the next time a screen appears. + boolean containerOpen = ClientScreen.current(mc) instanceof AbstractContainerScreen; + inventoryOpenValue.setInstant(containerOpen); + + // Reset the raise so the hotbar slides in when the module turns on. + hotbarRaiseValue.setInstant(false); + } + + /** + * The single update point for every animated value: compute the boolean + * target from the current game state, then advance each value exactly once. + */ + @Override + public void onFrame(float delta) { + Minecraft mc = Minecraft.getInstance(); + boolean inWorld = mc != null && mc.player != null; + boolean containerOpen = inWorld + && ClientScreen.current(mc) instanceof AbstractContainerScreen; + + hotbarRaiseValue.setTarget(inWorld); + hotbarRaiseValue.update(delta); + + inventoryOpenValue.setTarget(containerOpen); + inventoryOpenValue.update(delta); + } + + /** + * Vertical GUI-pixel offset the custom hotbar should be shifted down by while + * it slides into place. Reads only; the value is updated in {@link #onFrame(float)}. + */ + public float getHotbarRaiseOffset() { + if (!isEnabled() || !hotbarRaise.get()) { + return 0.0f; + } + return HOTBAR_RAISE_OFFSET_PX * (1.0f - hotbarRaiseValue.value()); + } + + /** + * Scale container screens should be drawn with while they open + * (1.0 when settled or when the animation is disabled). + */ + public float getInventoryScale() { + if (!isEnabled() || !inventoryOpen.get()) { + return 1.0f; + } + return INVENTORY_MIN_SCALE + (1.0f - INVENTORY_MIN_SCALE) * inventoryOpenValue.value(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java new file mode 100644 index 0000000..a97d291 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFog.java @@ -0,0 +1,145 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.config.values.ModeValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Modules; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.fog.FogData; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.FogType; + +/** + * Атмосферный туман зари: плотность тумана и цвет неба зависят от режима + * (рассвет/сумерки), времени суток, высоты камеры и мягкости градиента. + * В отличие от референса здесь эффект реально применяется к FogData и цвету. + */ +@ModuleInfo( + id = "atmodawnfog", + displayName = "AtmoDawnFog", + aliases = {"atmo", "dawnfog"}, + category = ModuleCategory.VISUALS, + description = "module.atmodawnfog.description" +) +public final class AtmoDawnFog extends Module { + + private static final float TIME_WINDOW = 0.1f; + private static final float ALTITUDE_FADE = 32.0f; + + private final ModeValue mode = + modeSetting("atmoDawnFogMode", "mode", "Dawn", "Dawn", "Dusk", "Custom"); + private final NumberValue density = + num("atmoDawnFogDensity", "density", 0.35f, 0.05f, 0.8f); + private final NumberValue scatterHeight = + num("atmoDawnFogScatterHeight", "scatter_height", 76.0f, 60.0f, 120.0f); + private final NumberValue softness = + num("atmoDawnFogSoftness", "softness", 0.6f, 0.0f, 1.0f); + private final RGBColorValue dawnColor = + visibleWhen(colorNoAlpha("atmoDawnFogColor", "dawn_color", "#FFAD7A"), () -> "Custom".equals(mode.get())); + + private static int dawnArgb() { + return 0xFFFFAD7A; + } + + private static int duskArgb() { + return 0xFF5C6BFF; + } + + /** Сила эффекта 0..1: время суток, режим и высота камеры. */ + public float strength(double cameraY) { + AtmoDawnFog module = Modules.get(AtmoDawnFog.class); + if (module == null || !module.isEnabled()) return 0.0f; + + float timeOfDay = (WorldTweaks.getServerTimeOfDay() % 24000L) / 24000.0f; + return strengthFor(timeOfDay, module.mode.get(), cameraY, module.scatterHeight.get()); + } + + /** Чистая математика силы эффекта — вынесена для тестов. */ + static float strengthFor(float timeOfDay, String mode, double cameraY, float scatterHeight) { + float byMode = switch (mode) { + case "Dusk" -> timeCloseness(timeOfDay, 0.5f); + case "Custom" -> 1.0f; + default -> timeCloseness(timeOfDay, 0.0f); + }; + if (byMode <= 0.0f) return 0.0f; + + float altitude = 1.0f - (float) Math.max(0.0, cameraY - scatterHeight) / ALTITUDE_FADE; + return byMode * Math.max(0.0f, Math.min(1.0f, altitude)); + } + + private static float timeCloseness(float timeOfDay, float target) { + float delta = Math.abs(timeOfDay - target); + delta = Math.min(delta, 1.0f - delta); + return Math.max(0.0f, 1.0f - delta / TIME_WINDOW); + } + + /** + * Применяет туман к FogData. Вызывается из миксина до WorldTweaks-веток; + * false — эффекта нет в этом кадре. + */ + public static boolean applyFog(FogData data, Camera camera, ClientLevel world, + float viewDistance, float tickDelta) { + AtmoDawnFog module = Modules.get(AtmoDawnFog.class); + if (module == null || !module.isEnabled() || data == null || world == null) return false; + if (world.dimension() != Level.OVERWORLD) return false; + if (camera.getFluidInCamera() != FogType.NONE) return false; + + WorldTweaks tweaks = Modules.get(WorldTweaks.class); + if (tweaks != null && tweaks.isFogModifyEnabled()) return false; + + float strength = module.strength(camera.position().y); + if (strength <= 0.0f) return false; + + float density = module.density.get(); + float far = Math.max(viewDistance * (1.0f - 0.75f * density * strength), viewDistance * 0.15f); + float start = far * Math.max(0.05f, module.softness.get()); + + data.environmentalStart = start; + data.environmentalEnd = far; + data.renderDistanceStart = start; + data.renderDistanceEnd = far; + data.skyEnd = far; + data.cloudEnd = far; + return true; + } + + /** Подмешивает цвет зари в туман; true — цвет уже изменён. */ + public static boolean tintColor(org.joml.Vector4f color, ClientLevel world, double cameraY) { + AtmoDawnFog module = Modules.get(AtmoDawnFog.class); + if (module == null || !module.isEnabled() || color == null || world == null) return false; + + WorldTweaks tweaks = Modules.get(WorldTweaks.class); + if (tweaks != null && tweaks.isFogModifyEnabled()) return false; + + float strength = module.strength(cameraY) * 0.8f; + if (strength <= 0.0f) return false; + + int argb = switch (module.mode.get()) { + case "Dusk" -> duskArgb(); + case "Custom" -> module.dawnColor.getArgb(); + default -> dawnArgb(); + }; + float r = ((argb >> 16) & 0xFF) / 255.0f; + float g = ((argb >> 8) & 0xFF) / 255.0f; + float b = (argb & 0xFF) / 255.0f; + + color.set( + color.x + (r - color.x) * strength, + color.y + (g - color.y) * strength, + color.z + (b - color.z) * strength, + color.w + ); + return true; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java index f8387e4..17e5137 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java @@ -43,6 +43,7 @@ import dev.loki.lovisual.render.engine.uniform.impl.HandBlendUniforms; import dev.loki.lovisual.render.engine.uniform.impl.HandChamsUniforms; import dev.loki.lovisual.render.engine.uniform.impl.HandGlassUniforms; import dev.loki.lovisual.render.engine.uniform.impl.HandMetallicUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.HandNoiseUniforms; import dev.loki.lovisual.render.engine.uniform.impl.HandSmokeUniforms; import dev.loki.lovisual.render.iris.IrisHandMaskState; import dev.loki.lovisual.render.iris.IrisRuntime; @@ -73,6 +74,7 @@ public class Chams extends Module { private static final String SETTING_SMOKE_CONTRAST = "smoke_contrast"; private static final String SETTING_SMOKE_SWIRL = "smoke_swirl"; private static final String SETTING_SMOKE_DENSITY = "smoke_density"; + private static final String SETTING_NOISE_ALPHA = "noise_alpha"; private static final String SETTING_METALLIC_BASE = "metallic_base"; private static final String SETTING_METALLIC_HIGHLIGHT = "metallic_highlight"; private static final String SETTING_METALLIC_INTENSITY = "metallic_intensity"; @@ -114,9 +116,16 @@ public class Chams extends Module { private static final String SETTING_BALATRO_CONTRAST = "balatro_contrast"; private static final String SETTING_BALATRO_LIGHTING = "balatro_lighting"; private static final String SETTING_HIDE_HAND = "hide_hand"; + // Noise mode tuning; intentionally fixed so the mode stays zero-config besides color/alpha. + private static final float NOISE_OCTAVES = 3.0f; + private static final float NOISE_SCALE = 3.4f; + private static final float NOISE_SPEED = 0.85f; + private static final float NOISE_CONTRAST = 1.15f; + private static final float NOISE_BREATHE = 0.55f; + private static final float NOISE_DENSITY = 1.0f; private final Minecraft mc = Minecraft.getInstance(); private final ModeValue mode = - modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"); + modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"); private final BooleanValue hands = bool("chamsHands", SETTING_HANDS, true); @@ -164,6 +173,8 @@ public class Chams extends Module { visibleWhen(num("handChamsSmokeSwirl", SETTING_SMOKE_SWIRL, 0.85f, 0.0f, 2.5f), this::isSmokeMode); private final NumberValue smokeDensity = visibleWhen(num("handChamsSmokeDensity", SETTING_SMOKE_DENSITY, 1.0f, 0.2f, 2.5f), this::isSmokeMode); + private final NumberValue noiseAlpha = + visibleWhen(num("handChamsNoiseAlpha", SETTING_NOISE_ALPHA, 0.85f, 0.0f, 1.0f), this::isNoiseMode); private final NumberValue metallicIntensity = visibleWhen(num("handChamsMetallicIntensity", SETTING_METALLIC_INTENSITY, 1.35f, 0.0f, 5.0f), this::isMetallicMode); private final NumberValue metallicSharpness = @@ -548,8 +559,12 @@ public class Chams extends Module { return "Metallic".equals(mode.get()); } + private boolean isNoiseMode() { + return "Noise".equals(mode.get()); + } + private boolean isHandEffectMode() { - return isSmokeMode() || isMetallicMode(); + return isSmokeMode() || isMetallicMode() || isNoiseMode(); } private boolean isGlowVisible() { @@ -766,6 +781,8 @@ public class Chams extends Module { .sampler("u_Src", src, PostProcessManager.getSampler()) .sampler("u_Mask", maskView, getHandMaskSampler()) .end(); + } else if ("Noise".equals(mode.get())) { + renderNoise(src, dst, maskView, rawTime); } else { renderChamsEffect(src, dst, maskView, mode.get(), rawTime); } @@ -844,6 +861,33 @@ public class Chams extends Module { .end(); } + private void renderNoise(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + int fillRgb = materialFillRgb(); + int glowRgb = glowRgb(fillRgb); + + float fillA = hideHand.get() ? 1.0f : clamp01(noiseAlpha.get()); + float glowA = glow.get() ? glowAlpha.get() : 0.0f; + + int w = mc.getWindow().getWidth(); + int h = mc.getWindow().getHeight(); + + HandNoiseUniforms.update( + channel(fillRgb, 16), channel(fillRgb, 8), channel(fillRgb, 0), fillA, + channel(glowRgb, 16), channel(glowRgb, 8), channel(glowRgb, 0), glowA, + edgeWidth.get(), quality.get(), NOISE_OCTAVES, rawTime, + w, h, NOISE_SCALE, NOISE_SPEED, + NOISE_CONTRAST, NOISE_BREATHE, glowStrength.get(), NOISE_DENSITY + ); + + FullScreenRenderer.begin("LoVisual Fullscreen Pass") + .attachment(dst) + .pipeline(LoVisualRenderPipelines.HAND_NOISE) + .uniform("HandNoise", HandNoiseUniforms.get()) + .sampler("u_Src", src, PostProcessManager.getSampler()) + .sampler("u_Mask", maskView, getHandMaskSampler()) + .end(); + } + private void renderDualShader(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { ensureDualBuffers(); if (dualA == null || dualB == null diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java index fad98d7..fd0ee0a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoldMyItems.java @@ -12,11 +12,18 @@ import dev.loki.lovisual.config.values.NumberValue; import dev.loki.lovisual.features.module.Module; import dev.loki.lovisual.features.module.ModuleCategory; import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.util.animation.EaseCurves; +import dev.loki.lovisual.util.animation.hand.HandAnimState; +import dev.loki.lovisual.render.helpers.TickDelta; import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; /** - * Smooths and adds a subtle hand-held sway to first-person item rendering, so weapons and tools - * settle into place with an ease-out-back curve instead of vanilla's linear equip motion. + * Плавная анимация предмета в руке: ease-out-back equip, sway по обзору, + * боб при ходьбе, дрейф при использовании. Состояние шагает ровно раз за + * кадр в {@link #onFrame} — вне рендер-пути, поэтому повторный рендер рук + * (маска Chams) всегда видит те же значения, что и основной проход. */ @ModuleInfo( id = "holdmyitems", @@ -27,30 +34,44 @@ import net.minecraft.client.Minecraft; ) public final class HoldMyItems extends Module { - public final Minecraft mc = Minecraft.getInstance(); + private final HandAnimState handAnim = new HandAnimState(); public final NumberValue swayIntensity = num("sway_intensity", 1.0f, 0.0f, 2.0f); public final NumberValue easeStrength = num("ease_strength", 1.6f, 0.5f, 3.5f); public final BooleanValue offhandToo = bool("offhand", true); + public final BooleanValue walkBob = bool("walk_bob", true); + public final NumberValue bobStrength = visibleWhen(num("bob_strength", 1.0f, 0.0f, 2.0f), walkBob::get); + public final BooleanValue useDrift = bool("use_drift", true); + public final NumberValue driftStrength = visibleWhen(num("drift_strength", 1.0f, 0.0f, 2.0f), useDrift::get); + public final BooleanValue joinAppear = bool("join_appear", true); - /** - * Ease-out-back: overshoots slightly past 1.0 before settling, giving the equip motion weight. - * Generalized with a configurable strength. - */ - public float easeOutBack(float x) { - float c1 = easeStrength.get(); - float c3 = c1 + 1.0f; - float t = x - 1.0f; - return 1.0f + c3 * t * t * t + c1 * t * t; + public HandAnimState handAnim() { + return handAnim; } - /** - * Reshapes the raw [0,1] equip progress with the ease-out-back curve, then blends by intensity - * so a intensity of 0 falls back to vanilla's linear motion. - */ + @Override + public void onFrame(float tickDelta) { + if (!isEnabled()) return; + LocalPlayer player = Minecraft.getInstance().player; + if (player == null) return; + handAnim.beginFrame( + player, + Minecraft.getInstance().level, + player.getViewYRot(tickDelta), + player.getViewXRot(tickDelta), + player.isUsingItem() && player.getUsedItemHand() == InteractionHand.MAIN_HAND, + player.isUsingItem() && player.getUsedItemHand() == InteractionHand.OFF_HAND, + TickDelta.fixedDeltaTicks() + ); + } + + @Override + public void onEnable() { + handAnim.reset(); + } + + /** Перекладывает линейный equip-прогресс через ease-out-back с настройками модуля. */ public float shapeEquipProgress(float linear) { - float eased = easeOutBack(linear); - float amount = swayIntensity.get(); - return linear + (eased - linear) * amount; + return EaseCurves.shapeEquipProgress(linear, easeStrength.get(), swayIntensity.get()); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java index 0a28949..54bcd27 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/InvisEsp.java @@ -7,23 +7,15 @@ package dev.loki.lovisual.features.module.modules.visuals; -import dev.loki.lovisual.config.values.BooleanValue; import dev.loki.lovisual.config.values.NumberValue; -import dev.loki.lovisual.config.values.RGBAColorValue; import dev.loki.lovisual.features.module.Module; import dev.loki.lovisual.features.module.ModuleCategory; import dev.loki.lovisual.features.module.ModuleInfo; -import dev.loki.lovisual.features.module.WorldPhase; -import dev.loki.lovisual.features.relations.PlayerRelations; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import net.minecraft.client.Minecraft; -import net.minecraft.world.effect.MobEffects; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.phys.AABB; +/** + * Делает невидимых игроков видимыми: модель рендерится полупрозрачной, + * сила прозрачности регулируется слайдером. + */ @ModuleInfo( id = "invisesp", displayName = "InvisESP", @@ -33,55 +25,19 @@ import net.minecraft.world.phys.AABB; ) public final class InvisEsp extends Module { - private final Minecraft mc = Minecraft.getInstance(); - - private final RGBAColorValue outlineColor = - color("invisesp_color", "color", "#FF00E5FF"); - private final BooleanValue highlightFriends = - bool("invisesp_highlight_friends", "highlight_friends", true); - private final RGBAColorValue friendColor = - visibleWhen(color("invisesp_friend_color", "friend_color", "#FF00FF00"), highlightFriends::get); - private final NumberValue lineWidth = - num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f); + private final NumberValue opacity = + num("invisesp_opacity", "opacity", 0.5f, 0.1f, 1.0f); { setDefaultBind("L"); } - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; + public float getOpacity() { + return opacity.get(); } - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - if (!isEnabled() || mc.level == null || mc.player == null) return; - - float previousLineWidth = RenderState.lineWidth; - MeshBuilder lines; - try { - RenderState.lineWidth = lineWidth.get(); - lines = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE); - } finally { - RenderState.lineWidth = previousLineWidth; - } - - int defaultColor = outlineColor.getArgb(); - boolean highlightFriendsEnabled = highlightFriends.get(); - for (Player player : mc.level.players()) { - if (player == mc.player) continue; - if (!isInvisible(player)) continue; - - int color = highlightFriendsEnabled && PlayerRelations.get().isFriendOrStaff(player.getGameProfile().name()) - ? friendColor.getArgb() - : defaultColor; - - AABB box = Esp2DRenderer.resolveWorldBox(player, tickDelta); - EspMeshBuilder.addOutlineBox(lines, box, color, color, color, color, color, color, color, color); - } - } - - private boolean isInvisible(Player player) { - return player.isInvisible() || player.hasEffect(MobEffects.INVISIBILITY); + /** Прозрачный рендер имеет смысл только ниже полной непрозрачности. */ + public boolean isTranslucentReveal() { + return opacity.get() < 0.99f; } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java new file mode 100644 index 0000000..46bb43f --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ItemPhysic.java @@ -0,0 +1,52 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; + +/** + * Физика предметов на земле: предметы лежат плашмя, крутятся в воздухе, + * без ваниллового покачивания; опционально уменьшенный размер. + */ +@ModuleInfo( + id = "itemphysic", + displayName = "Item Physic", + aliases = {"itemphysic", "physics"}, + category = ModuleCategory.VISUALS, + description = "module.itemphysic.description" +) +public final class ItemPhysic extends Module { + + private final BooleanValue reduceSize = + bool("itemphysicReduceSize", "reduce_size", false); + private final BooleanValue layFlat = + bool("itemphysicLayFlat", "lay_flat", true); + private final BooleanValue spinInAir = + bool("itemphysicSpinInAir", "spin_in_air", true); + private final BooleanValue noBob = + bool("itemphysicNoBob", "no_bob", true); + + public boolean shouldReduceSize() { + return isEnabled() && reduceSize.get(); + } + + public boolean shouldLayFlat() { + return isEnabled() && layFlat.get(); + } + + public boolean shouldSpinInAir() { + return isEnabled() && spinInAir.get(); + } + + public boolean shouldCancelBob() { + return isEnabled() && noBob.get(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java index 9c37bd0..3868c6c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java @@ -54,7 +54,7 @@ public class PostFX extends Module implements PostProcessPass { private final Minecraft mc = Minecraft.getInstance(); private final ModeValue preset = - modeSetting("postFxPreset", SETTING_PRESET, "Vanilla", "Vanilla", "Soft", "Warm", "Cool", "Custom"); + modeSetting("postFxPreset", SETTING_PRESET, "Cinematic", "Cinematic", "Vanilla", "Soft", "Warm", "Cool", "Custom"); private final ModeValue lutPreset = visibleWhen(modeSetting("postFxLutPreset", SETTING_LUT_PRESET, "Identity", "Identity", "Warm", "Cool"), this::isCustomPreset); @@ -221,7 +221,7 @@ public class PostFX extends Module implements PostProcessPass { private float resolveLutMode(String presetName, boolean isCustom) { String pick = isCustom ? lutPreset.get() : presetName; return switch (pick) { - case "Warm" -> 1.0f; + case "Warm", "Cinematic" -> 1.0f; case "Cool" -> 2.0f; default -> 0.0f; }; @@ -236,100 +236,135 @@ public class PostFX extends Module implements PostProcessPass { } private void applyPreset(String name) { - String presetName = name == null ? "Vanilla" : name; + applyPresetTo(name, ubo); + } + + /** Чистое заполнение UBO пресетом — вынесено для тестов. */ + static void applyPresetTo(String name, float[] out) { + String presetName = name == null ? "Cinematic" : name; switch (presetName) { - case "Soft" -> fillPresetSoft(); - case "Warm" -> fillPresetWarm(); - case "Cool" -> fillPresetCool(); - default -> fillPresetVanilla(); + case "Vanilla" -> fillPresetVanilla(out); + case "Warm" -> fillPresetWarm(out); + case "Cool" -> fillPresetCool(out); + case "Cinematic" -> fillPresetCinematic(out); + default -> fillPresetSoft(out); } } - private void fillPresetVanilla() { - ubo[0] = 0.0f; // exposure - ubo[1] = 1.0f; // gamma - ubo[2] = 1.0f; // contrast - ubo[3] = 1.0f; // saturation - ubo[4] = 1.0f; // vibrance - ubo[5] = 0.0f; // temp - ubo[6] = 0.0f; // tint - ubo[7] = 0.0f; // lut strength - ubo[8] = 0.08f; // vignette strength - ubo[9] = 0.55f; // vignette radius - ubo[10] = 0.35f; // vignette softness - ubo[11] = 0.0f; // chroma - ubo[12] = 0.0f; // grain amount - ubo[13] = 1.0f; // grain size - ubo[14] = 0.05f; // sharpen - ubo[15] = 0.0f; // tone map none - fillColor(ubo, 16, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 20, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 24, 0xFFFFFFFF, 0.0f); + private static void setTint(float[] out, int offset, float r, float g, float b, float strength) { + out[offset] = r; + out[offset + 1] = g; + out[offset + 2] = b; + out[offset + 3] = strength; } - private void fillPresetSoft() { - ubo[0] = 0.0f; - ubo[1] = 1.0f; - ubo[2] = 1.0f; - ubo[3] = 1.01f; - ubo[4] = 1.0f; - ubo[5] = 0.0f; - ubo[6] = 0.0f; - ubo[7] = 0.18f; - ubo[8] = 0.08f; - ubo[9] = 0.55f; - ubo[10] = 0.35f; - ubo[11] = 0.01f; - ubo[12] = 0.0f; - ubo[13] = 1.0f; - ubo[14] = 0.05f; - ubo[15] = 0.0f; - fillColor(ubo, 16, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 20, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 24, 0xFFFFFFFF, 0.0f); + private static void fillPresetCinematic(float[] out) { + out[0] = 0.02f; // exposure + out[1] = 1.0f; // gamma + out[2] = 1.10f; // contrast + out[3] = 1.14f; // saturation + out[4] = 1.08f; // vibrance + out[5] = 0.06f; // temp (теплее) + out[6] = 0.0f; // tint + out[7] = 0.30f; // LUT strength (warm) + out[8] = 0.18f; // vignette strength + out[9] = 0.55f; // vignette radius + out[10] = 0.35f; // vignette softness + out[11] = 0.0f; // chroma + out[12] = 0.0f; // grain + out[13] = 1.0f; // grain size + out[14] = 0.10f; // sharpen + out[15] = 1.0f; // tone map: filmic + setTint(out, 16, 0.72f, 0.80f, 1.0f, 0.22f); // тени — холодные + setTint(out, 20, 1.0f, 1.0f, 1.0f, 0.0f); // полутона — нейтральные + setTint(out, 24, 1.0f, 0.93f, 0.80f, 0.18f); // света — тёплые } - private void fillPresetWarm() { - ubo[0] = 0.0f; - ubo[1] = 1.0f; - ubo[2] = 1.01f; - ubo[3] = 1.02f; - ubo[4] = 1.01f; - ubo[5] = 0.03f; - ubo[6] = 0.01f; - ubo[7] = 0.18f; - ubo[8] = 0.08f; - ubo[9] = 0.55f; - ubo[10] = 0.35f; - ubo[11] = 0.01f; - ubo[12] = 0.0f; - ubo[13] = 1.0f; - ubo[14] = 0.05f; - ubo[15] = 0.0f; - fillColor(ubo, 16, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 20, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 24, 0xFFFFFFFF, 0.0f); + private static void fillPresetVanilla(float[] out) { + out[0] = 0.0f; // exposure + out[1] = 1.0f; // gamma + out[2] = 1.0f; // contrast + out[3] = 1.0f; // saturation + out[4] = 1.0f; // vibrance + out[5] = 0.0f; // temp + out[6] = 0.0f; // tint + out[7] = 0.0f; // lut strength + out[8] = 0.08f; // vignette strength + out[9] = 0.55f; // vignette radius + out[10] = 0.35f; // vignette softness + out[11] = 0.0f; // chroma + out[12] = 0.0f; // grain amount + out[13] = 1.0f; // grain size + out[14] = 0.05f; // sharpen + out[15] = 0.0f; // tone map none + setTint(out, 16, 1f, 1f, 1f, 0.0f); + setTint(out, 20, 1f, 1f, 1f, 0.0f); + setTint(out, 24, 1f, 1f, 1f, 0.0f); } - private void fillPresetCool() { - ubo[0] = 0.0f; - ubo[1] = 1.0f; - ubo[2] = 1.01f; - ubo[3] = 1.0f; - ubo[4] = 1.01f; - ubo[5] = -0.04f; - ubo[6] = 0.0f; - ubo[7] = 0.18f; - ubo[8] = 0.08f; - ubo[9] = 0.55f; - ubo[10] = 0.35f; - ubo[11] = 0.01f; - ubo[12] = 0.0f; - ubo[13] = 1.0f; - ubo[14] = 0.05f; - ubo[15] = 0.0f; - fillColor(ubo, 16, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 20, 0xFFFFFFFF, 0.0f); - fillColor(ubo, 24, 0xFFFFFFFF, 0.0f); + private static void fillPresetSoft(float[] out) { + out[0] = 0.0f; + out[1] = 1.0f; + out[2] = 1.01f; + out[3] = 1.01f; + out[4] = 1.0f; + out[5] = 0.0f; + out[6] = 0.0f; + out[7] = 0.18f; + out[8] = 0.08f; + out[9] = 0.55f; + out[10] = 0.35f; + out[11] = 0.01f; + out[12] = 0.0f; + out[13] = 1.0f; + out[14] = 0.05f; + out[15] = 0.0f; + setTint(out, 16, 1f, 1f, 1f, 0.0f); + setTint(out, 20, 1f, 1f, 1f, 0.0f); + setTint(out, 24, 1f, 1f, 1f, 0.0f); + } + + private static void fillPresetWarm(float[] out) { + out[0] = 0.0f; + out[1] = 1.0f; + out[2] = 1.01f; + out[3] = 1.02f; + out[4] = 1.01f; + out[5] = 0.03f; + out[6] = 0.01f; + out[7] = 0.18f; + out[8] = 0.08f; + out[9] = 0.55f; + out[10] = 0.35f; + out[11] = 0.01f; + out[12] = 0.0f; + out[13] = 1.0f; + out[14] = 0.05f; + out[15] = 0.0f; + setTint(out, 16, 1f, 1f, 1f, 0.0f); + setTint(out, 20, 1f, 1f, 1f, 0.0f); + setTint(out, 24, 1f, 1f, 1f, 0.0f); + } + + private static void fillPresetCool(float[] out) { + out[0] = 0.0f; + out[1] = 1.0f; + out[2] = 1.01f; + out[3] = 1.0f; + out[4] = 1.01f; + out[5] = -0.04f; + out[6] = 0.0f; + out[7] = 0.18f; + out[8] = 0.08f; + out[9] = 0.55f; + out[10] = 0.35f; + out[11] = 0.01f; + out[12] = 0.0f; + out[13] = 1.0f; + out[14] = 0.05f; + out[15] = 0.0f; + setTint(out, 16, 1f, 1f, 1f, 0.0f); + setTint(out, 20, 1f, 1f, 1f, 0.0f); + setTint(out, 24, 1f, 1f, 1f, 0.0f); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java new file mode 100644 index 0000000..4cff9ae --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShulkerPreview.java @@ -0,0 +1,154 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponents; +import net.minecraft.util.Mth; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.block.ShulkerBoxBlock; + +@ModuleInfo( + id = "shulkerpreview", + displayName = "Shulker Preview", + aliases = {"shulkerpreview", "preview"}, + category = ModuleCategory.VISUALS, + description = "module.shulkerpreview.description" +) +public final class ShulkerPreview extends Module { + + private static final int COLS = 9; + private static final int ROWS = 3; + private static final int SLOT_SIZE = 18; + private static final int PAD = 4; + private static final int HEADER_HEIGHT = 10; + private static final int HEADER_GAP = 3; + private static final int CURSOR_OFFSET = 14; + private static final int BG_ALPHA = 0xE6; + private static final int STROKE_ALPHA = 0xDC; + private static final int CELL_ALPHA = 0x8C; + + private final NumberValue scale = + num("shulkerpreview_scale", "scale", 1.0f, 0.5f, 2.0f); + + /** + * Called from AbstractContainerScreenMixin at the end of screen extraction. + */ + public static void onScreenExtract(GuiGraphicsExtractor ctx, ItemStack stack, int mouseX, int mouseY) { + ShulkerPreview module = Modules.get(ShulkerPreview.class); + if (module == null || !module.isEnabled()) return; + module.drawPreview(ctx, stack, mouseX, mouseY); + } + + private void drawPreview(GuiGraphicsExtractor ctx, ItemStack stack, int mouseX, int mouseY) { + if (ctx == null || !isShulkerBox(stack)) return; + + NonNullList contents = collectContents(stack); + if (contents == null) return; + + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.font == null) return; + + float panelScale = Mth.clamp(scale.get(), 0.5f, 2.0f); + int width = PAD * 2 + COLS * SLOT_SIZE; + int height = PAD * 2 + HEADER_HEIGHT + HEADER_GAP + ROWS * SLOT_SIZE; + + int x = horizontalOrigin(mouseX, width * panelScale, ctx.guiWidth()); + int y = verticalOrigin(mouseY, height * panelScale, ctx.guiHeight()); + + Themes.Theme theme = Theme.theme(); + int bg = HudRenderUtil.setAlpha(theme.windowBg(), BG_ALPHA); + int stroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.34f), + STROKE_ALPHA); + int headerColor = HudRenderUtil.setAlpha(theme.textPrimary(), 0xFF); + int cellColor = HudRenderUtil.setAlpha(theme.surface(), CELL_ALPHA); + + Font font = mc.font; + ctx.pose().pushMatrix(); + ctx.pose().translate(x, y); + ctx.pose().scale(panelScale, panelScale); + try { + ctx.fill(RenderPipelines.GUI, 0, 0, width, height, bg); + ctx.outline(0, 0, width, height, stroke); + ctx.text(font, stack.getHoverName(), PAD, PAD, headerColor); + + int gridY = PAD + HEADER_HEIGHT + HEADER_GAP; + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + int cellX = PAD + col * SLOT_SIZE; + int cellY = gridY + row * SLOT_SIZE; + ctx.fill(RenderPipelines.GUI, + cellX, cellY, cellX + SLOT_SIZE, cellY + SLOT_SIZE, + cellColor); + + ItemStack item = contents.get(row * COLS + col); + if (item == null || item.isEmpty()) continue; + ctx.item(item, cellX + 1, cellY + 1); + ctx.itemDecorations(font, item, cellX + 1, cellY + 1); + } + } + } finally { + ctx.pose().popMatrix(); + } + } + + private static NonNullList collectContents(ItemStack stack) { + ItemContainerContents container = stack.get(DataComponents.CONTAINER); + if (container == null) return null; + + NonNullList contents = NonNullList.withSize(COLS * ROWS, ItemStack.EMPTY); + container.copyInto(contents); + + for (ItemStack item : contents) { + if (item != null && !item.isEmpty()) { + return contents; + } + } + return null; + } + + private static boolean isShulkerBox(ItemStack stack) { + if (stack == null || stack.isEmpty()) return false; + if (!(stack.getItem() instanceof BlockItem blockItem)) return false; + return blockItem.getBlock() instanceof ShulkerBoxBlock; + } + + private static int horizontalOrigin(int mouseX, float width, int bounds) { + int right = (int) (mouseX + CURSOR_OFFSET); + if (right + width <= bounds) return right; + + int left = (int) (mouseX - CURSOR_OFFSET - width); + if (left >= 0) return left; + + int clamped = (int) (bounds - width); + return Math.max(0, Math.min(right, clamped)); + } + + private static int verticalOrigin(int mouseY, float height, int bounds) { + int top = (int) (mouseY - height / 2f); + if (top + height >= bounds) { + top = (int) (bounds - height); + } + return Math.max(0, top); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java index 61d111d..d7b92a2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldTweaks.java @@ -61,7 +61,6 @@ public class WorldTweaks extends Module { private static final String SETTING_CHANGE_TIME = "change_time"; private static final String SETTING_TIME_HOURS = "time_hours"; private static final String SETTING_SYNC_SYSTEM_TIME = "sync_system_time"; - private static final String SETTING_ITEM_PHYSICS = "item_physics"; private static volatile long serverTimeOfDay; private final BooleanValue fogControlEnabled = bool("worldTweaksFogControl", SETTING_FOG_CONTROL, true); @@ -128,8 +127,6 @@ public class WorldTweaks extends Module { private final NumberValue timeHours = visibleWhen(num("worldTweaksTimeHours", SETTING_TIME_HOURS, 21, 0, 23), () -> changeTime.get() && !syncSystemTime.get()); - private final BooleanValue itemPhysicsEnabled = - bool("worldTweaksItemPhysics", SETTING_ITEM_PHYSICS, true); private boolean wasOverridingTime; private static int getHourTicks(int hour) { @@ -224,9 +221,6 @@ public class WorldTweaks extends Module { return isEnabled() && changeTime.get(); } - public boolean isItemPhysicsEnabled() { - return isEnabled() && itemPhysicsEnabled.get(); - } public int getTimeOfDayTicks() { if (syncSystemTime.get()) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java index d9e80db..ffe1fd3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java @@ -7,8 +7,17 @@ package dev.loki.lovisual.mixins; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.visuals.Animations; +import dev.loki.lovisual.features.module.modules.visuals.ShulkerPreview; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.joml.Matrix3x2fStack; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -16,8 +25,77 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(AbstractContainerScreen.class) public abstract class AbstractContainerScreenMixin { + @Shadow + protected Slot hoveredSlot; + + @Unique + private boolean lovisual$animations$scaleApplied; + @Inject(method = "onClose", at = @At("HEAD"), cancellable = true) private void lovisual$inventoryMove$deferClose(CallbackInfo ci) { // InventoryMove module removed; keep hook for compatibility. } + + /** + * Smooth open animation from the Animations module: the whole screen is drawn + * with a scale between 0.9 and 1.0 around the screen center. The animated + * value itself is advanced once per frame inside the module; this only reads + * the settled value and wraps the extraction in a matching push/pop. + */ + @Inject( + method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", + at = @At("HEAD") + ) + private void lovisual$animations$beginOpenScale(GuiGraphicsExtractor graphics, + int mouseX, + int mouseY, + float partialTick, + CallbackInfo ci) { + lovisual$animations$scaleApplied = false; + + Animations animations = Modules.get(Animations.class); + if (animations == null) return; + + float scale = animations.getInventoryScale(); + if (scale >= 0.999f) return; + + Matrix3x2fStack pose = graphics.pose(); + float centerX = graphics.guiWidth() * 0.5f; + float centerY = graphics.guiHeight() * 0.5f; + + pose.pushMatrix(); + pose.translate(centerX, centerY); + pose.scale(scale, scale); + pose.translate(-centerX, -centerY); + lovisual$animations$scaleApplied = true; + } + + @Inject( + method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", + at = @At("RETURN") + ) + private void lovisual$animations$endOpenScale(GuiGraphicsExtractor graphics, + int mouseX, + int mouseY, + float partialTick, + CallbackInfo ci) { + if (!lovisual$animations$scaleApplied) return; + lovisual$animations$scaleApplied = false; + graphics.pose().popMatrix(); + } + + @Inject( + method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", + at = @At("TAIL") + ) + private void lovisual$shulkerPreview$draw(GuiGraphicsExtractor graphics, + int mouseX, + int mouseY, + float partialTick, + CallbackInfo ci) { + Slot slot = this.hoveredSlot; + ItemStack stack = slot == null ? ItemStack.EMPTY : slot.getItem(); + if (stack.isEmpty()) return; + ShulkerPreview.onScreenExtract(graphics, stack, mouseX, mouseY); + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java index bb143af..f0bf7b9 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AtmosphericFogEnvironmentMixin.java @@ -18,6 +18,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.visuals.AtmoDawnFog; import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks; @Mixin(value = AtmosphericFogEnvironment.class, priority = 1200) @@ -30,6 +31,12 @@ public class AtmosphericFogEnvironmentMixin { float viewDistance, DeltaTracker tickCounter, CallbackInfo ci) { + if (AtmoDawnFog.applyFog(data, camera, world, viewDistance, + tickCounter.getGameTimeDeltaPartialTick(false))) { + ci.cancel(); + return; + } + WorldTweaks module = Modules.get(WorldTweaks.class); if (module == null || !module.isFogControlEnabled()) return; if (module.isFogModifyEnabled()) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java index c69d1cd..2a0423b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ClientCommonPacketListenerImplMixin.java @@ -7,10 +7,14 @@ package dev.loki.lovisual.mixins; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.misc.NoServerPack; import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.network.Connection; import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; +import net.minecraft.network.protocol.common.ServerboundResourcePackPacket; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -30,4 +34,20 @@ public abstract class ClientCommonPacketListenerImplMixin { private void lovisual$flightSendPacket(Packet packet, CallbackInfo ci) { // Flight module removed; no move-packet override remains. } + + @Inject( + method = "handleResourcePackPush(Lnet/minecraft/network/protocol/common/ClientboundResourcePackPushPacket;)V", + at = @At("HEAD"), + cancellable = true + ) + private void lovisual$onResourcePackPush(ClientboundResourcePackPushPacket packet, CallbackInfo ci) { + NoServerPack module = Modules.get(NoServerPack.class); + if (module == null || !module.shouldDeclinePush()) return; + // Same terminal answer vanilla sends when the pack is refused, but + // before any prompt screen or download can start. + connection.send(new ServerboundResourcePackPacket( + packet.id(), ServerboundResourcePackPacket.Action.DECLINED)); + module.onPackDeclined(packet.id()); + ci.cancel(); + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java index 8dad537..3c4b912 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/FogRendererMixin.java @@ -19,6 +19,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.visuals.AtmoDawnFog; import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks; @Mixin(value = FogRenderer.class, priority = 1200) @@ -67,6 +68,10 @@ public class FogRendererMixin { int viewDistance, float skyDarkness, Vector4f color, CallbackInfo ci) { + if (AtmoDawnFog.tintColor(color, world, camera.position().y)) { + return; + } + WorldTweaks module = Modules.get(WorldTweaks.class); if (module == null || !module.isFogModifyEnabled()) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java new file mode 100644 index 0000000..eb8f275 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java @@ -0,0 +1,93 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.mixins; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.HumanoidArm; +import dev.loki.lovisual.features.module.modules.visuals.HoldMyItems; +import dev.loki.lovisual.render.helpers.TickDelta; +import dev.loki.lovisual.util.animation.hand.BobMath; +import dev.loki.lovisual.util.animation.hand.HandAnimState; +import dev.loki.lovisual.util.animation.hand.HandPoseMath; + +/** + * Клей между рендером руки и чистой математикой динамики: собирает ввод + * (дельты взгляда, скорость, использование предмета), собирает позу и + * применяет её к матрице вокруг ванильных трансформов. Состояние не хранит — + * оно живёт в {@link HandAnimState}, обновляемом раз за кадр. + */ +final class HandDynamics { + + /** Смещения/углы слабее этого считаются нулевыми и не применяются. */ + private static final float SIGNIFICANT_EPSILON = 1.0e-4f; + + private HandDynamics() { + } + + /** Один шаг динамики за кадр; вызывается из начала submitHandsWithItems. */ + static void beginFrame(HandAnimState state, AbstractClientPlayer player, float tickDelta) { + state.beginFrame( + player, + Minecraft.getInstance().level, + player.getViewYRot(tickDelta), + player.getViewXRot(tickDelta), + player.isUsingItem() && player.getUsedItemHand() == InteractionHand.MAIN_HAND, + player.isUsingItem() && player.getUsedItemHand() == InteractionHand.OFF_HAND, + TickDelta.fixedDeltaTicks() + ); + } + + /** + * Применяет позу динамики к матрице руки: pushPose, затем смещение и + * повороты вокруг точки взгляда до ванильных трансформов. Вызывающий + * обязан сделать popPose, если вернулось true. + * + * @return true, если был pushPose и нужен popPose + */ + static boolean apply( + HoldMyItems module, HandAnimState state, AbstractClientPlayer player, + float tickDelta, InteractionHand hand, PoseStack matrices + ) { + boolean mainHand = hand == InteractionHand.MAIN_HAND; + float bobStrength = module.walkBob.get() ? module.bobStrength.get() : 0.0f; + float driftStrength = module.useDrift.get() ? module.driftStrength.get() : 0.0f; + + boolean usingThisHand = player.isUsingItem() && player.getUsedItemHand() == hand; + float drift = usingThisHand ? state.drift(mainHand) : 0.0f; + + HandPoseMath.Pose pose = HandPoseMath.sway( + state.sway().yawLag(), state.sway().pitchLag(), module.swayIntensity.get()); + if (bobStrength > 0.0f) { + pose = HandPoseMath.add(pose, HandPoseMath.bob( + player.walkAnimation.position(tickDelta), + BobMath.amplitude(player.walkAnimation.speed(tickDelta)), + bobStrength)); + } + if (driftStrength > 0.0f && drift > 0.0f) { + boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand; + pose = HandPoseMath.add(pose, HandPoseMath.drift(drift, driftStrength, rightArm)); + } + if (module.joinAppear.get()) { + pose = HandPoseMath.add(pose, HandPoseMath.joinDrop(state.joinProgress())); + } + if (!pose.isSignificant(SIGNIFICANT_EPSILON)) { + return false; + } + + matrices.pushPose(); + matrices.translate(pose.dx(), pose.dy(), pose.dz()); + if (pose.rx() != 0.0f) matrices.mulPose(Axis.XP.rotationDegrees(pose.rx())); + if (pose.ry() != 0.0f) matrices.mulPose(Axis.YP.rotationDegrees(pose.ry())); + if (pose.rz() != 0.0f) matrices.mulPose(Axis.ZP.rotationDegrees(pose.rz())); + return true; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java index 6a6772b..2bcfde6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemEntityRendererMixin.java @@ -25,8 +25,9 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import dev.loki.lovisual.features.module.Modules; -import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks; +import dev.loki.lovisual.features.module.modules.visuals.ItemPhysic; import dev.loki.lovisual.mixininterface.IItemEntityRenderState; +import net.minecraft.util.Mth; @Mixin(ItemEntityRenderer.class) public abstract class ItemEntityRendererMixin extends EntityRenderer { @@ -51,19 +52,26 @@ public abstract class ItemEntityRendererMixin extends EntityRenderer cir) { + LockSlot lockSlot = Modules.get(LockSlot.class); + if (lockSlot != null && lockSlot.shouldBlockSelectedDrop()) { + cir.setReturnValue(false); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java index 733badd..4bb1cc1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java @@ -30,6 +30,7 @@ import dev.loki.lovisual.events.impl.AttackEntityEvent; import dev.loki.lovisual.events.impl.EventBreakBlock; import dev.loki.lovisual.features.gui.hud.draggable.impl.TargetHud; import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.combat.NoFriendDamage; import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns; import dev.loki.lovisual.features.module.modules.misc.HitSounds; import dev.loki.lovisual.features.module.modules.visuals.Freecam; @@ -141,8 +142,14 @@ public class MultiPlayerGameModeMixin { } @Inject(method = "attack(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;)V", - at = @At("HEAD")) + at = @At("HEAD"), cancellable = true) private void onAttackEntity(Player player, Entity target, CallbackInfo ci) { + NoFriendDamage noFriendDamage = Modules.get(NoFriendDamage.class); + if (noFriendDamage != null && noFriendDamage.shouldBlockAttack(target)) { + ci.cancel(); + return; + } + if (!((WorldAccessor) player.level()).lovisual$isClient()) return; if (Events.BUS.hasListeners(AttackEntityEvent.class)) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java new file mode 100644 index 0000000..d2bf8ec --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeThrowMixin.java @@ -0,0 +1,36 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.mixins; + +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.player.LockSlot; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerInput; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Cancels THROW container clicks (Q / Ctrl+Q over a hovered slot and + * throw clicks) targeting locked hotbar slots while LockSlot is enabled. + */ +@Mixin(MultiPlayerGameMode.class) +public class MultiPlayerGameModeThrowMixin { + + @Inject(method = "handleContainerInput", at = @At("HEAD"), cancellable = true) + private void lovisual$lockSlot$blockThrowClick(int containerId, int slotId, int button, + ContainerInput input, Player player, CallbackInfo ci) { + if (input != ContainerInput.THROW) return; + LockSlot lockSlot = Modules.get(LockSlot.class); + if (lockSlot != null && lockSlot.shouldBlockContainerThrow(player, containerId, slotId)) { + ci.cancel(); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java new file mode 100644 index 0000000..a66b3f4 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/KeyMappingAccessor.java @@ -0,0 +1,20 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.mixins.accessors; + +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(KeyMapping.class) +public interface KeyMappingAccessor { + + @Accessor("key") + InputConstants.Key lovisual$key(); +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java new file mode 100644 index 0000000..b2cc78d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesReveal.java @@ -0,0 +1,55 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render; + +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.visuals.InvisEsp; +import dev.loki.lovisual.render.iris.IrisRuntime; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; + +/** + * Логика See Invisibles: невидимый игрок раскрывается — флаг невидимости + * снимается перед рендером, а render type форсится в полупрозрачный. + */ +public enum SeeInvisiblesReveal { + ; + + public static void reveal(T entity, LivingEntityRenderState state) { + if (!(entity instanceof Player player) || player == Minecraft.getInstance().player) { + return; + } + InvisEsp module = Modules.get(InvisEsp.class); + if (module == null || !module.isEnabled() || !entity.isInvisible()) { + return; + } + if (state instanceof SeeInvisiblesState seeInvis) { + seeInvis.lovisual$setSeeInvisRevealed(true); + } + state.isInvisible = false; + } + + public static RenderType renderTypeFor(RenderType original, Identifier texture) { + InvisEsp module = Modules.get(InvisEsp.class); + if (module == null || !module.isEnabled() || IrisRuntime.isRenderingShadowPass()) { + return original; + } + if (!module.isTranslucentReveal()) { + return original; + } + if (original != null && original.hasBlending()) { + return original; + } + return RenderTypes.entityTranslucent(texture, true); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java new file mode 100644 index 0000000..149af2e --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/SeeInvisiblesState.java @@ -0,0 +1,18 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render; + +/** + * Marker injected into entity render states when InvisESP reveals + * an invisible player this frame. + */ +public interface SeeInvisiblesState { + boolean lovisual$isSeeInvisRevealed(); + + void lovisual$setSeeInvisRevealed(boolean revealed); +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java index 14ea855..0b4021b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java @@ -7,21 +7,23 @@ package dev.loki.lovisual.render.engine.pipeline; +import com.mojang.blaze3d.PrimitiveTopology; import com.mojang.blaze3d.pipeline.BlendFunction; import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.platform.BlendFactor; -import com.mojang.blaze3d.shaders.UniformType; import com.mojang.blaze3d.systems.GpuDevice; import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import net.minecraft.resources.Identifier; -import net.minecraft.server.packs.resources.ResourceManager; import dev.loki.lovisual.mixininterface.IRenderPipeline; +import dev.loki.lovisual.render.engine.pipeline.pipelines.PipelineTemplates; +import dev.loki.lovisual.render.engine.pipeline.pipelines.ScreenPipelines; +import dev.loki.lovisual.render.engine.pipeline.pipelines.UiPipelines; +import dev.loki.lovisual.render.engine.pipeline.pipelines.WorldPipelines; import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract; import dev.loki.lovisual.render.engine.rhi.pipeline.RenderPipelineRegistry; import dev.loki.lovisual.render.engine.shader.LoVisualShaderSources; import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats; import dev.loki.lovisual.util.logging.DebugLog; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceManager; import java.util.ArrayList; import java.util.Collections; @@ -30,1428 +32,299 @@ import java.util.List; /** * Pipeline registry for LoVisual's custom rendering engine. * + *

Public API facade: every shader identifier and pipeline constant keeps its original + * name and value. The repetitive builder chains live in the template classes of + * {@link dev.loki.lovisual.render.engine.pipeline.pipelines}.

+ * *

Keep this class free of module-specific logic: it is the foundation for all custom 3D/2D effects.

*/ public enum LoVisualRenderPipelines { ; + // ====================== // Shaders (assets/lovisual/shaders/*) - public static final Identifier SHADER_POS_COLOR_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color.vert"); - public static final Identifier SHADER_WIDE_LINE_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/wide_line.vert"); - public static final Identifier SHADER_POS_COLOR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color.frag"); - public static final Identifier SHADER_POS_COLOR_FOG_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_fog.vert"); - public static final Identifier SHADER_POS_COLOR_FOG_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_fog.frag"); - public static final Identifier SHADER_POS_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_rect_params.vert"); - public static final Identifier SHADER_POS_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_rect_params2.vert"); - public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params.vert"); - public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params2.vert"); - public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params5.vert"); - public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_local_color_rect_params5_generic.vert"); - public static final Identifier SHADER_POS_TEX_COLOR_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color.vert"); - public static final Identifier SHADER_POS_TEX_COLOR_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_params2.vert"); - public static final Identifier SHADER_POS_TEX_COLOR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color.frag"); - public static final Identifier SHADER_GUI_TEXTURE_LOOKUP_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/gui_texture_lookup.frag"); - public static final Identifier SHADER_POS_TEX_COLOR_TINT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_tint.frag"); - public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog.vert"); - public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog.frag"); - public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog_additive.frag"); - public static final Identifier SHADER_POS_COLOR_SKYBOX_SHADER_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_skybox_shader.vert"); - public static final Identifier SHADER_REIMAGINED_SKYBOX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/reimagined_skybox.frag"); - public static final Identifier SHADER_SHADER_SKY_THUNDER_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_sky_thunder.frag"); - public static final Identifier SHADER_SHADER_SKY_PULSAR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_sky_pulsar.frag"); - public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_rect_params.vert"); - public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_local_color_rect_params.vert"); - public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_local_color_rect_params2.vert"); - public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_rect_params2.vert"); - public static final Identifier SHADER_TEXT_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/text.vert"); - public static final Identifier SHADER_TEXT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text.frag"); - public static final Identifier SHADER_TEXT_MSDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text_msdf.frag"); - public static final Identifier SHADER_SVG_MSDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/svg_msdf.frag"); - public static final Identifier SHADER_TEXT_LIQUID_GLASS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text_liquid_glass.frag"); - public static final Identifier SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/text_liquid_glass_msdf.frag"); - public static final Identifier SHADER_CIRCLE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/circle_batch.frag"); - public static final Identifier SHADER_ARC_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/arc_batch.frag"); - public static final Identifier SHADER_ORBIZ_RING_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/orbiz_ring_batch.frag"); - public static final Identifier SHADER_WORLD_DECAL_SDF_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/world_decal_sdf.frag"); - public static final Identifier SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_stroke_corners_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_batch.frag"); - public static final Identifier SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_rounded_fill_smoke_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_stroke_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_stroke_angular_batch.frag"); - public static final Identifier SHADER_CHAMFERED_RECT_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/chamfered_rect_batch.frag"); - public static final Identifier SHADER_UI_SHAPE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_shape_batch.frag"); - public static final Identifier SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/chamfered_rect_stroke_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_corners_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_glow_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_shadow_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_soft_shadow_batch.frag"); - public static final Identifier SHADER_RADIAL_GLOW_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/radial_glow_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_TEX_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_tex_batch.frag"); - public static final Identifier SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/rounded_rect_tex_mask_batch.frag"); - public static final Identifier SHADER_UI_BLUR_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur_batch.frag"); - public static final Identifier SHADER_UI_BLUR_BATCH_CORNERS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur_batch_corners.frag"); - public static final Identifier SHADER_UI_LIQUID_GLASS_BATCH_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_liquid_glass_batch.frag"); - public static final Identifier SHADER_DAMAGE_TINT_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/damage_tint.vert"); - public static final Identifier SHADER_DAMAGE_TINT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/damage_tint.frag"); - public static final Identifier SHADER_POST_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/post_fx.frag"); - public static final Identifier SHADER_MOTION_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/motion_blur.frag"); - public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/depth_of_field.frag"); - public static final Identifier SHADER_WET_SURFACE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/wet_surface.frag"); - public static final Identifier SHADER_HEAT_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/heat_fx.frag"); - public static final Identifier SHADER_ESP_GRADIENT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_gradient.frag"); - public static final Identifier SHADER_ESP_SHADOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_shadow.frag"); - public static final Identifier SHADER_ESP_SMOKE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_smoke.frag"); - public static final Identifier SHADER_PORTAL_RIFT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/portal_rift.frag"); - public static final Identifier SHADER_SLEEP_OVERLAY_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sleep_overlay.frag"); - public static final Identifier SHADER_HAND_SMOKE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_smoke.frag"); - public static final Identifier SHADER_HAND_METALLIC_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_metallic.frag"); - public static final Identifier SHADER_UI_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur.frag"); - public static final Identifier SHADER_HAND_GLASS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_glass.frag"); - public static final Identifier SHADER_HAND_CHAMS_SOLID_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_solid.frag"); - public static final Identifier SHADER_HAND_CHAMS_CHROMA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_chroma.frag"); - public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_stripes.frag"); - public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_glow.frag"); - public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_snow.frag"); - public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_balatro.frag"); - public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_blend.frag"); - public static final Identifier SHADER_SKY_SUN_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_sun.frag"); - public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_waves.frag"); - public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_aurora.frag"); - public static final Identifier SHADER_SKY_SHADER_SKY_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_sky.frag"); - public static final Identifier SHADER_SKY_SHADER_WATER_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_water.frag"); - public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_caustic.frag"); + // ====================== + // Core colored / line / fog shaders + public static final Identifier SHADER_POS_COLOR_VERT = shader("pos_color.vert"); + public static final Identifier SHADER_WIDE_LINE_VERT = shader("wide_line.vert"); + public static final Identifier SHADER_POS_COLOR_FRAG = shader("pos_color.frag"); + public static final Identifier SHADER_POS_COLOR_FOG_VERT = shader("pos_color_fog.vert"); + public static final Identifier SHADER_POS_COLOR_FOG_FRAG = shader("pos_color_fog.frag"); + // Rect-parameter vertex shaders (batched shapes) + public static final Identifier SHADER_POS_COLOR_RECT_PARAMS_VERT = shader("pos_color_rect_params.vert"); + public static final Identifier SHADER_POS_COLOR_RECT_PARAMS2_VERT = shader("pos_color_rect_params2.vert"); + public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT = shader("pos_local_color_rect_params.vert"); + public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT = shader("pos_local_color_rect_params2.vert"); + public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT = shader("pos_local_color_rect_params5.vert"); + public static final Identifier SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT = shader("pos_local_color_rect_params5_generic.vert"); + // Textured primitives & GUI lookup + public static final Identifier SHADER_POS_TEX_COLOR_VERT = shader("pos_tex_color.vert"); + public static final Identifier SHADER_POS_TEX_COLOR_PARAMS2_VERT = shader("pos_tex_color_params2.vert"); + public static final Identifier SHADER_POS_TEX_COLOR_FRAG = shader("pos_tex_color.frag"); + public static final Identifier SHADER_GUI_TEXTURE_LOOKUP_FRAG = shader("gui_texture_lookup.frag"); + public static final Identifier SHADER_POS_TEX_COLOR_TINT_FRAG = shader("pos_tex_color_tint.frag"); + public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_VERT = shader("pos_tex_color_sky_fog.vert"); + public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_FRAG = shader("pos_tex_color_sky_fog.frag"); + public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG = shader("pos_tex_color_sky_fog_additive.frag"); + // Skyboxes & shader skies + public static final Identifier SHADER_POS_COLOR_SKYBOX_SHADER_VERT = shader("pos_color_skybox_shader.vert"); + public static final Identifier SHADER_REIMAGINED_SKYBOX_FRAG = shader("reimagined_skybox.frag"); + public static final Identifier SHADER_SHADER_SKY_THUNDER_FRAG = shader("shader_sky_thunder.frag"); + public static final Identifier SHADER_SHADER_SKY_PULSAR_FRAG = shader("shader_sky_pulsar.frag"); + public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS_VERT = shader("pos_tex_color_rect_params.vert"); + public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT = shader("pos_tex_local_color_rect_params.vert"); + public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT = shader("pos_tex_local_color_rect_params2.vert"); + public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS2_VERT = shader("pos_tex_color_rect_params2.vert"); + // Text & MSDF + public static final Identifier SHADER_TEXT_VERT = shader("text.vert"); + public static final Identifier SHADER_TEXT_FRAG = shader("text.frag"); + public static final Identifier SHADER_TEXT_MSDF_FRAG = shader("text_msdf.frag"); + public static final Identifier SHADER_SVG_MSDF_FRAG = shader("svg_msdf.frag"); + public static final Identifier SHADER_TEXT_LIQUID_GLASS_FRAG = shader("text_liquid_glass.frag"); + public static final Identifier SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG = shader("text_liquid_glass_msdf.frag"); + // Batched UI shapes (SDF fragments) + public static final Identifier SHADER_CIRCLE_BATCH_FRAG = shader("circle_batch.frag"); + public static final Identifier SHADER_ARC_BATCH_FRAG = shader("arc_batch.frag"); + public static final Identifier SHADER_ORBIZ_RING_BATCH_FRAG = shader("orbiz_ring_batch.frag"); + public static final Identifier SHADER_WORLD_DECAL_SDF_FRAG = shader("world_decal_sdf.frag"); + public static final Identifier SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG = shader("rounded_rect_stroke_corners_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_BATCH_FRAG = shader("rounded_rect_batch.frag"); + public static final Identifier SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG = shader("ui_rounded_fill_smoke_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG = shader("rounded_rect_stroke_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG = shader("rounded_rect_stroke_angular_batch.frag"); + public static final Identifier SHADER_CHAMFERED_RECT_BATCH_FRAG = shader("chamfered_rect_batch.frag"); + public static final Identifier SHADER_UI_SHAPE_BATCH_FRAG = shader("ui_shape_batch.frag"); + public static final Identifier SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG = shader("chamfered_rect_stroke_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG = shader("rounded_rect_corners_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG = shader("rounded_rect_glow_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG = shader("rounded_rect_shadow_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG = shader("rounded_rect_soft_shadow_batch.frag"); + public static final Identifier SHADER_RADIAL_GLOW_BATCH_FRAG = shader("radial_glow_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_TEX_BATCH_FRAG = shader("rounded_rect_tex_batch.frag"); + public static final Identifier SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG = shader("rounded_rect_tex_mask_batch.frag"); + public static final Identifier SHADER_UI_BLUR_BATCH_FRAG = shader("ui_blur_batch.frag"); + public static final Identifier SHADER_UI_BLUR_BATCH_CORNERS_FRAG = shader("ui_blur_batch_corners.frag"); + public static final Identifier SHADER_UI_LIQUID_GLASS_BATCH_FRAG = shader("ui_liquid_glass_batch.frag"); + // Fullscreen post-processing & overlays + public static final Identifier SHADER_DAMAGE_TINT_VERT = shader("damage_tint.vert"); + public static final Identifier SHADER_DAMAGE_TINT_FRAG = shader("damage_tint.frag"); + public static final Identifier SHADER_POST_FX_FRAG = shader("post_fx.frag"); + public static final Identifier SHADER_MOTION_BLUR_FRAG = shader("motion_blur.frag"); + public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = shader("depth_of_field.frag"); + public static final Identifier SHADER_WET_SURFACE_FRAG = shader("wet_surface.frag"); + public static final Identifier SHADER_HEAT_FX_FRAG = shader("heat_fx.frag"); + public static final Identifier SHADER_ESP_GRADIENT_FRAG = shader("shader_esp_gradient.frag"); + public static final Identifier SHADER_ESP_SHADOW_FRAG = shader("shader_esp_shadow.frag"); + public static final Identifier SHADER_ESP_SMOKE_FRAG = shader("shader_esp_smoke.frag"); + public static final Identifier SHADER_PORTAL_RIFT_FRAG = shader("portal_rift.frag"); + public static final Identifier SHADER_SLEEP_OVERLAY_FRAG = shader("sleep_overlay.frag"); + // Hand effects & chams + public static final Identifier SHADER_HAND_SMOKE_FRAG = shader("hand_smoke.frag"); + public static final Identifier SHADER_HAND_METALLIC_FRAG = shader("hand_metallic.frag"); + public static final Identifier SHADER_HAND_NOISE_FRAG = shader("hand_noise.frag"); + public static final Identifier SHADER_UI_BLUR_FRAG = shader("ui_blur.frag"); + public static final Identifier SHADER_HAND_GLASS_FRAG = shader("hand_glass.frag"); + public static final Identifier SHADER_HAND_CHAMS_SOLID_FRAG = shader("hand_chams_solid.frag"); + public static final Identifier SHADER_HAND_CHAMS_CHROMA_FRAG = shader("hand_chams_chroma.frag"); + public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = shader("hand_chams_stripes.frag"); + public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = shader("hand_chams_glow.frag"); + public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = shader("hand_chams_snow.frag"); + public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = shader("hand_chams_balatro.frag"); + public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = shader("hand_chams_blend.frag"); + // Sky & menu backgrounds + public static final Identifier SHADER_SKY_SUN_FRAG = shader("sky_sun.frag"); + public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = shader("menu_background_waves.frag"); + public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = shader("menu_background_aurora.frag"); + public static final Identifier SHADER_SKY_SHADER_SKY_FRAG = shader("sky_shader_sky.frag"); + public static final Identifier SHADER_SKY_SHADER_WATER_FRAG = shader("sky_shader_water.frag"); + public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = shader("sky_shader_caustic.frag"); + private static final List PIPELINES = new ArrayList<>(); - public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(new ExtendedRenderPipelineBuilder(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED_SNIPPET) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/gui_texture_lookup")) - .withFragmentShader(SHADER_GUI_TEXTURE_LOOKUP_FRAG) - .build() - ); - // Snippets - private static final RenderPipeline.Snippet MESH_UNIFORMS = new ExtendedRenderPipelineBuilder() - .withUniform("MeshData", UniformType.UNIFORM_BUFFER) - .buildSnippet(); - /** - * No depth test; translucent; triangles. - */ - public static final RenderPipeline WORLD_COLORED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); + + public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(ScreenPipelines.guiTextureLookup()); // ====================== // World (3D) // ====================== - /** - * No depth test; translucent; lines. - */ - public static final RenderPipeline WORLD_COLORED_LINES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLineSmooth() - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Screen-space wide line expansion for WORLD_COLORED_LINES. Uses triangles; never depends on glLineWidth. - */ - public static final RenderPipeline WORLD_WIDE_COLORED_LINES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines")) - .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_WIDE_LINE_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - - /** - * Depth test (GEQUAL); translucent; triangles. - */ - public static final RenderPipeline WORLD_COLORED_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * No depth test; translucent; textured triangles. - */ - public static final RenderPipeline WORLD_TEXTURED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; textured triangles. - */ - public static final RenderPipeline WORLD_TEXTURED_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); additive; colored triangles. - */ - public static final RenderPipeline WORLD_COLORED_ADDITIVE_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_additive_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * No depth test; translucent; world decal quads with SDF fill/stroke. - */ - public static final RenderPipeline WORLD_DECAL_SDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_decal_sdf")) - .withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_PARAMS2_VERT) - .withFragmentShader(SHADER_WORLD_DECAL_SDF_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; world decal quads with SDF fill/stroke. - */ - public static final RenderPipeline WORLD_DECAL_SDF_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_decal_sdf_depth")) - .withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_PARAMS2_VERT) - .withFragmentShader(SHADER_WORLD_DECAL_SDF_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * No depth test; translucent; world text quads. - */ - public static final RenderPipeline WORLD_TEXT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; world text quads. - */ - public static final RenderPipeline WORLD_TEXT_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * No depth test; translucent; world MSDF text quads. - */ - public static final RenderPipeline WORLD_TEXT_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text_msdf")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_MSDF_FRAG) - .withSampler("u_Texture") - .withUniform("MsdfText", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; world MSDF text quads. - */ - public static final RenderPipeline WORLD_TEXT_MSDF_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_text_msdf_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_MSDF_FRAG) - .withSampler("u_Texture") - .withUniform("MsdfText", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * No depth test; additive; textured triangles. - */ - public static final RenderPipeline WORLD_TEXTURED_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * No depth test; additive; textured triangles with shader-side tint. - */ - public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_tint_additive")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_TINT_FRAG) - .withSampler("u_Texture") - .withUniform("TextureTint", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); additive; textured triangles. - */ - public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; lines. - */ - public static final RenderPipeline WORLD_COLORED_LINES_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLineSmooth() - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines_depth")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Screen-space wide line expansion for WORLD_COLORED_LINES_DEPTH. Uses triangles; never depends on glLineWidth. - */ - public static final RenderPipeline WORLD_WIDE_COLORED_LINES_DEPTH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines_depth")) - .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_WIDE_LINE_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - - /** - * Depth test (GEQUAL); translucent; triangles; lets liquids blend over (no depth write). - */ - public static final RenderPipeline WORLD_COLORED_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_liquid_blend")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; lines; lets liquids blend over (no depth write). - */ - public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLineSmooth() - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines_liquid_blend")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_BLEND. Uses triangles; never depends on glLineWidth. - */ - public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines_liquid_blend")) - .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_WIDE_LINE_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - - /** - * Depth test (GEQUAL); translucent; textured triangles; lets liquids blend over (no depth write). - */ - public static final RenderPipeline WORLD_TEXTURED_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_liquid_blend")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); additive; textured triangles; lets liquids blend over (no depth write). - */ - public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive_liquid_blend")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; triangles; intended for above-liquids pass (no depth write). - */ - public static final RenderPipeline WORLD_COLORED_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_liquid_ignore")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; lines; writes depth so liquids render below (above-liquids). - */ - public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLineSmooth() - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_colored_lines_liquid_ignore")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_IGNORE. Uses triangles; never depends on glLineWidth. - */ - public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_wide_colored_lines_liquid_ignore")) - .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_WIDE_LINE_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - - /** - * Depth test (GEQUAL); additive; textured triangles; intended for pre-water depth. - */ - public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_additive_liquid_ignore")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); additive; textured triangles with shader-side tint; intended for pre-water depth. - */ - public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_tint_additive_liquid_ignore")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_TINT_FRAG) - .withSampler("u_Texture") - .withUniform("TextureTint", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Depth test (GEQUAL); translucent; textured triangles; intended for pre-water depth. - */ - public static final RenderPipeline WORLD_TEXTURED_LIQUID_IGNORE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_textured_liquid_ignore")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.GEQUAL_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * UI triangles (pos2 + color). - */ - public static final RenderPipeline UI_COLORED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_colored")) - .withVertexFormat(LoVisualVertexFormats.POS2_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - /** - * UI lines (pos2 + color). - */ - public static final RenderPipeline UI_COLORED_LINES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_colored_lines")) - .withVertexFormat(LoVisualVertexFormats.POS2_COLOR, com.mojang.blaze3d.PrimitiveTopology.DEBUG_LINES) - .withVertexShader(SHADER_POS_COLOR_VERT) - .withFragmentShader(SHADER_POS_COLOR_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - /** - * UI textured triangles (pos2 + tex + color). - */ - public static final RenderPipeline UI_TEXTURED = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_textured")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - /** - * UI MSDF SVG quads (pos2 + tex + color). - */ - public static final RenderPipeline UI_SVG_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_svg_msdf")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_SVG_MSDF_FRAG) - .withSampler("u_Texture") - .withUniform("MsdfText", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - /** - * UI textured triangles (pos2 + tex + color), additive blend. - */ - public static final RenderPipeline UI_TEXTURED_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_textured_additive")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(true) - .build() - ); + /** No depth test; translucent; triangles. */ + public static final RenderPipeline WORLD_COLORED = add(WorldPipelines.colored("world_colored", false, BlendFunction.TRANSLUCENT)); + /** No depth test; translucent; lines. */ + public static final RenderPipeline WORLD_COLORED_LINES = add(WorldPipelines.coloredLines("world_colored_lines", false)); + /** Screen-space wide line expansion for WORLD_COLORED_LINES. Uses triangles; never depends on glLineWidth. */ + public static final RenderPipeline WORLD_WIDE_COLORED_LINES = add(WorldPipelines.wideLines("world_wide_colored_lines", false)); + /** Depth test (GEQUAL); translucent; triangles. */ + public static final RenderPipeline WORLD_COLORED_DEPTH = add(WorldPipelines.colored("world_colored_depth", true, BlendFunction.TRANSLUCENT)); + /** No depth test; translucent; textured triangles. */ + public static final RenderPipeline WORLD_TEXTURED = add(WorldPipelines.textured("world_textured", false, BlendFunction.TRANSLUCENT)); + /** Depth test (GEQUAL); translucent; textured triangles. */ + public static final RenderPipeline WORLD_TEXTURED_DEPTH = add(WorldPipelines.textured("world_textured_depth", true, BlendFunction.TRANSLUCENT)); + /** Depth test (GEQUAL); additive; colored triangles. */ + public static final RenderPipeline WORLD_COLORED_ADDITIVE_DEPTH = add(WorldPipelines.colored("world_colored_additive_depth", true, PipelineTemplates.ADDITIVE)); + /** No depth test; translucent; world decal quads with SDF fill/stroke. */ + public static final RenderPipeline WORLD_DECAL_SDF = add(WorldPipelines.decalSdf("world_decal_sdf", false)); + /** Depth test (GEQUAL); translucent; world decal quads with SDF fill/stroke. */ + public static final RenderPipeline WORLD_DECAL_SDF_DEPTH = add(WorldPipelines.decalSdf("world_decal_sdf_depth", true)); + /** No depth test; translucent; world text quads. */ + public static final RenderPipeline WORLD_TEXT = add(WorldPipelines.worldText("world_text", false, SHADER_TEXT_FRAG, false)); + /** Depth test (GEQUAL); translucent; world text quads. */ + public static final RenderPipeline WORLD_TEXT_DEPTH = add(WorldPipelines.worldText("world_text_depth", true, SHADER_TEXT_FRAG, false)); + /** No depth test; translucent; world MSDF text quads. */ + public static final RenderPipeline WORLD_TEXT_MSDF = add(WorldPipelines.worldText("world_text_msdf", false, SHADER_TEXT_MSDF_FRAG, true)); + /** Depth test (GEQUAL); translucent; world MSDF text quads. */ + public static final RenderPipeline WORLD_TEXT_MSDF_DEPTH = add(WorldPipelines.worldText("world_text_msdf_depth", true, SHADER_TEXT_MSDF_FRAG, true)); + /** No depth test; additive; textured triangles. */ + public static final RenderPipeline WORLD_TEXTURED_ADDITIVE = add(WorldPipelines.textured("world_textured_additive", false, PipelineTemplates.ADDITIVE)); + /** No depth test; additive; textured triangles with shader-side tint. */ + public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE = add(WorldPipelines.tintedTextured("world_textured_tint_additive", false)); + /** Depth test (GEQUAL); additive; textured triangles. */ + public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_DEPTH = add(WorldPipelines.textured("world_textured_additive_depth", true, PipelineTemplates.ADDITIVE)); + /** Depth test (GEQUAL); translucent; lines. */ + public static final RenderPipeline WORLD_COLORED_LINES_DEPTH = add(WorldPipelines.coloredLines("world_colored_lines_depth", true)); + /** Screen-space wide line expansion for WORLD_COLORED_LINES_DEPTH. Uses triangles; never depends on glLineWidth. */ + public static final RenderPipeline WORLD_WIDE_COLORED_LINES_DEPTH = add(WorldPipelines.wideLines("world_wide_colored_lines_depth", true)); + /** Depth test (GEQUAL); translucent; triangles; lets liquids blend over (no depth write). */ + public static final RenderPipeline WORLD_COLORED_LIQUID_BLEND = add(WorldPipelines.colored("world_colored_liquid_blend", true, BlendFunction.TRANSLUCENT)); + /** Depth test (GEQUAL); translucent; lines; lets liquids blend over (no depth write). */ + public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_BLEND = add(WorldPipelines.coloredLines("world_colored_lines_liquid_blend", true)); + /** Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_BLEND. Uses triangles; never depends on glLineWidth. */ + public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_BLEND = add(WorldPipelines.wideLines("world_wide_colored_lines_liquid_blend", true)); + /** Depth test (GEQUAL); translucent; textured triangles; lets liquids blend over (no depth write). */ + public static final RenderPipeline WORLD_TEXTURED_LIQUID_BLEND = add(WorldPipelines.textured("world_textured_liquid_blend", true, BlendFunction.TRANSLUCENT)); + /** Depth test (GEQUAL); additive; textured triangles; lets liquids blend over (no depth write). */ + public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_BLEND = add(WorldPipelines.textured("world_textured_additive_liquid_blend", true, PipelineTemplates.ADDITIVE)); + /** Depth test (GEQUAL); translucent; triangles; intended for above-liquids pass (no depth write). */ + public static final RenderPipeline WORLD_COLORED_LIQUID_IGNORE = add(WorldPipelines.colored("world_colored_liquid_ignore", true, BlendFunction.TRANSLUCENT)); + /** Depth test (GEQUAL); translucent; lines; writes depth so liquids render below (above-liquids). */ + public static final RenderPipeline WORLD_COLORED_LINES_LIQUID_IGNORE = add(WorldPipelines.coloredLines("world_colored_lines_liquid_ignore", true)); + /** Screen-space wide line expansion for WORLD_COLORED_LINES_LIQUID_IGNORE. Uses triangles; never depends on glLineWidth. */ + public static final RenderPipeline WORLD_WIDE_COLORED_LINES_LIQUID_IGNORE = add(WorldPipelines.wideLines("world_wide_colored_lines_liquid_ignore", true)); + /** Depth test (GEQUAL); additive; textured triangles; intended for pre-water depth. */ + public static final RenderPipeline WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE = add(WorldPipelines.textured("world_textured_additive_liquid_ignore", true, PipelineTemplates.ADDITIVE)); + /** Depth test (GEQUAL); additive; textured triangles with shader-side tint; intended for pre-water depth. */ + public static final RenderPipeline WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE = add(WorldPipelines.tintedTextured("world_textured_tint_additive_liquid_ignore", true)); + /** Depth test (GEQUAL); translucent; textured triangles; intended for pre-water depth. */ + public static final RenderPipeline WORLD_TEXTURED_LIQUID_IGNORE = add(WorldPipelines.textured("world_textured_liquid_ignore", true, BlendFunction.TRANSLUCENT)); // ====================== // UI (2D) // ====================== - /** - * UI text (pos2 + tex + color). - */ - public static final RenderPipeline UI_TEXT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - /** - * UI MSDF text (pos2 + tex + color). - */ - public static final RenderPipeline UI_TEXT_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text_msdf")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_MSDF_FRAG) - .withSampler("u_Texture") - .withUniform("MsdfText", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_TEXT_LIQUID_GLASS = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text_liquid_glass")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_LIQUID_GLASS_FRAG) - .withSampler("u_Texture") - .withSampler("u_SceneTexture") - .withSampler("u_BlurTexture") - .withUniform("UIBatch", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_TEXT_LIQUID_GLASS_MSDF = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_text_liquid_glass_msdf")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_TEXT_VERT) - .withFragmentShader(SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG) - .withSampler("u_Texture") - .withSampler("u_SceneTexture") - .withSampler("u_BlurTexture") - .withUniform("UIBatch", UniformType.UNIFORM_BUFFER) - .withUniform("MsdfText", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_BLUR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_blur")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_UI_BLUR_FRAG) - .withSampler("u_Texture") - .withUniform("UIBlur", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen menu background (pos2). - */ - public static final RenderPipeline MENU_BACKGROUND_WAVES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/menu_background_waves")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_MENU_BACKGROUND_WAVES_FRAG) - .withUniform("MenuBackground", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen SkyShader passes (pos2). Sky/water/caustic share the same uniform layout. - */ - public static final RenderPipeline SKY_SHADER_SKY = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_sky")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_SKY_SHADER_SKY_FRAG) - .withUniform("SkyShader", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline SKY_SHADER_WATER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_water")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_SKY_SHADER_WATER_FRAG) - .withUniform("SkyShader", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline SKY_SHADER_CAUSTIC = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_caustic")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_SKY_SHADER_CAUSTIC_FRAG) - .withUniform("SkyShader", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen main menu background (pos2). - */ - public static final RenderPipeline MENU_BACKGROUND_AURORA = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/menu_background_aurora")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_MENU_BACKGROUND_AURORA_FRAG) - .withUniform("MenuBackground", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen postprocess (pos2). - */ - public static final RenderPipeline DAMAGE_TINT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/damage_tint")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_DAMAGE_TINT_FRAG) - .withSampler("u_Texture") - .withUniform("PostProcess", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen visual grading + LUT (pos2). - */ - public static final RenderPipeline POST_FX = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/post_fx")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_POST_FX_FRAG) - .withSampler("u_Texture") - .withSampler("u_Lut") - .withUniform("PostFX", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen camera-only perceptual motion blur (pos2). - */ - public static final RenderPipeline MOTION_BLUR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/motion_blur")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_MOTION_BLUR_FRAG) - .withSampler("u_Texture") - .withSampler("u_PreviousColor") - .withUniform("MotionBlur", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen scene-depth-aware far depth of field (pos2). - */ - public static final RenderPipeline DEPTH_OF_FIELD = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/depth_of_field")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_DEPTH_OF_FIELD_FRAG) - .withSampler("u_Texture") - .withSampler("u_MainDepth") - .withSampler("u_TranslucentDepth") - .withSampler("u_ItemEntityDepth") - .withSampler("u_ParticlesDepth") - .withSampler("u_WeatherDepth") - .withSampler("u_CloudsDepth") - .withUniform("DepthOfField", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen wet-ground screen-space reflection pass (pos2). - */ - public static final RenderPipeline WET_SURFACE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/wet_surface")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_WET_SURFACE_FRAG) - .withSampler("u_Texture") - .withSampler("u_MainDepth") - .withUniform("WetSurface", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen heat distortion + vignette (pos2). - */ - public static final RenderPipeline HEAT_FX = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/heat_fx")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HEAT_FX_FRAG) - .withSampler("u_Texture") - .withUniform("Heat", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen shader ESP gradient composite (pos2). - */ - public static final RenderPipeline SHADER_ESP_GRADIENT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/shader_esp_gradient")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_ESP_GRADIENT_FRAG) - .withSampler("u_Texture") - .withUniform("ShaderEspGradient", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen shader ESP separable shadow blur (pos2). - */ - public static final RenderPipeline SHADER_ESP_SHADOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/shader_esp_shadow")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_ESP_SHADOW_FRAG) - .withSampler("u_Texture") - .withSampler("u_Mask") - .withUniform("ShaderEspBlur", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen shader ESP procedural smoke fill (pos2). - */ - public static final RenderPipeline SHADER_ESP_SMOKE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/shader_esp_smoke")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_ESP_SMOKE_FRAG) - .withSampler("u_Texture") - .withUniform("ShaderEspSmoke", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * No depth test; additive; textured-color triangles with procedural sun shading. - */ - public static final RenderPipeline WORLD_SKY_SUN = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_sky_sun")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_VERT) - .withFragmentShader(SHADER_SKY_SUN_FRAG) - .withUniform("SkySun", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Fullscreen nether portal rift (pos2). - */ - public static final RenderPipeline PORTAL_RIFT = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/portal_rift")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_PORTAL_RIFT_FRAG) - .withSampler("u_Texture") - .withUniform("Heat", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen sleep overlay (pos2). - */ - public static final RenderPipeline SLEEP_OVERLAY = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sleep_overlay")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_SLEEP_OVERLAY_FRAG) - .withSampler("u_Texture") - .withUniform("PostProcess", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen hand glass (pos2). - */ - public static final RenderPipeline HAND_GLASS = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_glass")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_GLASS_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandGlass", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_SMOKE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_smoke")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_SMOKE_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandSmoke", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_METALLIC = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_metallic")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_METALLIC_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandMetallic", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_SOLID = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_solid")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_SOLID_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandChams", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_CHROMA = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_chroma")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_CHROMA_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandChams", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_STRIPES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_stripes")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_STRIPES_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandChams", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_GLOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_glow")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_GLOW_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandChams", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_SNOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_snow")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_SNOW_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandChams", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_BALATRO = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_balatro")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_BALATRO_FRAG) - .withSampler("u_Src") - .withSampler("u_Mask") - .withUniform("HandChams", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline HAND_CHAMS_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_blend")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_HAND_CHAMS_BLEND_FRAG) - .withSampler("u_Shader1") - .withSampler("u_Shader2") - .withSampler("u_Mask") - .withUniform("HandBlend", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - private static final RenderPipeline.Snippet FOG_UNIFORMS = new ExtendedRenderPipelineBuilder() - .withUniform("Fog", UniformType.UNIFORM_BUFFER) - .buildSnippet(); - /** - * No depth test; translucent; textured skybox with vanilla sky/fog blending. - */ - public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, FOG_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_skybox_textured_fog")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_SKY_FOG_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_SKY_FOG_FRAG) - .withSampler("u_Texture") - .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * No depth test; additive; textured skybox with vanilla sky/fog blending. - */ - public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG_ADDITIVE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, FOG_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_skybox_textured_fog_additive")) - .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_COLOR_SKY_FOG_VERT) - .withFragmentShader(SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG) - .withSampler("u_Texture") - .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(false) - .build() - ); - /** - * Fullscreen procedural Reimagined skybox with sky/fog blending. - */ - public static final RenderPipeline WORLD_REIMAGINED_SKYBOX_SHADER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, FOG_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_reimagined_skybox_shader")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_REIMAGINED_SKYBOX_FRAG) - .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen procedural ShaderSky (Thunder) with sky/fog blending. - */ - public static final RenderPipeline WORLD_SHADER_SKY_THUNDER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_shader_sky_thunder")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_SHADER_SKY_THUNDER_FRAG) - .withUniform("ShaderSky", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - /** - * Fullscreen procedural ShaderSky (Pulsar) with sky/fog blending. - */ - public static final RenderPipeline WORLD_SHADER_SKY_PULSAR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_shader_sky_pulsar")) - .withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_DAMAGE_TINT_VERT) - .withFragmentShader(SHADER_SHADER_SKY_PULSAR_FRAG) - .withUniform("ShaderSky", UniformType.UNIFORM_BUFFER) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - private static final RenderPipeline.Snippet UI_BATCH_UNIFORMS = new ExtendedRenderPipelineBuilder() - .withUniform("UIBatch", UniformType.UNIFORM_BUFFER) - .buildSnippet(); - public static final RenderPipeline UI_CIRCLE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_circle_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_CIRCLE_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ARC_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_arc_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_ARC_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ORBIZ_RING_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_orbiz_ring_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT) - .withFragmentShader(SHADER_ORBIZ_RING_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_FILL_SMOKE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_fill_smoke_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT) - .withFragmentShader(SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_STROKE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_stroke_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_STROKE_ANGULAR_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_stroke_angular_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_CHAMFERED_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_chamfered_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_CHAMFERED_RECT_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_SHAPE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_shape_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT) - .withFragmentShader(SHADER_UI_SHAPE_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_CHAMFERED_STROKE_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_chamfered_stroke_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_STROKE_CORNERS_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_stroke_corners_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_CORNERS_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_corners_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_GLOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_glow_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_SHADOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_shadow_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_SOFT_SHADOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_soft_shadow_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_RADIAL_GLOW_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_radial_glow_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_RADIAL_GLOW_BATCH_FRAG) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE)) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_TEXTURED_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_textured_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_TEX_BATCH_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_ROUNDED_TEXTURED_MASK_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_rounded_textured_mask_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(true) - .build() - ); - public static final RenderPipeline UI_BLUR_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_blur_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT) - .withFragmentShader(SHADER_UI_BLUR_BATCH_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline UI_BLUR_BATCH_CORNERS = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_blur_batch_corners")) - .withVertexFormat(LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_UI_BLUR_BATCH_CORNERS_FRAG) - .withSampler("u_Texture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); - public static final RenderPipeline UI_LIQUID_GLASS_BATCH = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS, UI_BATCH_UNIFORMS) - .withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/ui_liquid_glass_batch")) - .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES) - .withVertexShader(SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT) - .withFragmentShader(SHADER_UI_LIQUID_GLASS_BATCH_FRAG) - .withSampler("u_Texture") - .withSampler("u_BlurTexture") - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - .withBlend(BlendFunction.TRANSLUCENT) - .withCull(false) - .build() - ); + /** UI triangles (pos2 + color). */ + public static final RenderPipeline UI_COLORED = add(UiPipelines.colored("ui_colored", PrimitiveTopology.TRIANGLES)); + /** UI lines (pos2 + color). */ + public static final RenderPipeline UI_COLORED_LINES = add(UiPipelines.colored("ui_colored_lines", PrimitiveTopology.DEBUG_LINES)); + /** UI textured triangles (pos2 + tex + color). */ + public static final RenderPipeline UI_TEXTURED = add(UiPipelines.textured("ui_textured", SHADER_POS_TEX_COLOR_VERT, SHADER_POS_TEX_COLOR_FRAG, BlendFunction.TRANSLUCENT, true, null, "u_Texture")); + /** UI MSDF SVG quads (pos2 + tex + color). */ + public static final RenderPipeline UI_SVG_MSDF = add(UiPipelines.textured("ui_svg_msdf", SHADER_POS_TEX_COLOR_VERT, SHADER_SVG_MSDF_FRAG, BlendFunction.TRANSLUCENT, true, "MsdfText", "u_Texture")); + /** UI textured triangles (pos2 + tex + color), additive blend. */ + public static final RenderPipeline UI_TEXTURED_ADDITIVE = add(UiPipelines.textured("ui_textured_additive", SHADER_POS_TEX_COLOR_VERT, SHADER_POS_TEX_COLOR_FRAG, PipelineTemplates.ADDITIVE, true, null, "u_Texture")); + /** UI text (pos2 + tex + color). */ + public static final RenderPipeline UI_TEXT = add(UiPipelines.textured("ui_text", SHADER_TEXT_VERT, SHADER_TEXT_FRAG, BlendFunction.TRANSLUCENT, true, null, "u_Texture")); + /** UI MSDF text (pos2 + tex + color). */ + public static final RenderPipeline UI_TEXT_MSDF = add(UiPipelines.textured("ui_text_msdf", SHADER_TEXT_VERT, SHADER_TEXT_MSDF_FRAG, BlendFunction.TRANSLUCENT, true, "MsdfText", "u_Texture")); + public static final RenderPipeline UI_TEXT_LIQUID_GLASS = add(UiPipelines.textLiquidGlass("ui_text_liquid_glass", SHADER_TEXT_LIQUID_GLASS_FRAG, false)); + public static final RenderPipeline UI_TEXT_LIQUID_GLASS_MSDF = add(UiPipelines.textLiquidGlass("ui_text_liquid_glass_msdf", SHADER_TEXT_LIQUID_GLASS_MSDF_FRAG, true)); + public static final RenderPipeline UI_BLUR = add(UiPipelines.textured("ui_blur", SHADER_POS_TEX_COLOR_VERT, SHADER_UI_BLUR_FRAG, BlendFunction.TRANSLUCENT, false, "UIBlur", "u_Texture")); + /** Fullscreen menu background (pos2). */ + public static final RenderPipeline MENU_BACKGROUND_WAVES = add(ScreenPipelines.screen("menu_background_waves", SHADER_MENU_BACKGROUND_WAVES_FRAG, "MenuBackground")); + /** Fullscreen SkyShader passes (pos2). Sky/water/caustic share the same uniform layout. */ + public static final RenderPipeline SKY_SHADER_SKY = add(ScreenPipelines.screen("sky_shader_sky", SHADER_SKY_SHADER_SKY_FRAG, "SkyShader")); + public static final RenderPipeline SKY_SHADER_WATER = add(ScreenPipelines.screen("sky_shader_water", SHADER_SKY_SHADER_WATER_FRAG, "SkyShader")); + public static final RenderPipeline SKY_SHADER_CAUSTIC = add(ScreenPipelines.screen("sky_shader_caustic", SHADER_SKY_SHADER_CAUSTIC_FRAG, "SkyShader")); + /** Fullscreen main menu background (pos2). */ + public static final RenderPipeline MENU_BACKGROUND_AURORA = add(ScreenPipelines.screen("menu_background_aurora", SHADER_MENU_BACKGROUND_AURORA_FRAG, "MenuBackground")); + /** Fullscreen postprocess (pos2). */ + public static final RenderPipeline DAMAGE_TINT = add(ScreenPipelines.screen("damage_tint", SHADER_DAMAGE_TINT_FRAG, "PostProcess", "u_Texture")); + /** Fullscreen visual grading + LUT (pos2). */ + public static final RenderPipeline POST_FX = add(ScreenPipelines.screen("post_fx", SHADER_POST_FX_FRAG, "PostFX", "u_Texture", "u_Lut")); + /** Fullscreen camera-only perceptual motion blur (pos2). */ + public static final RenderPipeline MOTION_BLUR = add(ScreenPipelines.screen("motion_blur", SHADER_MOTION_BLUR_FRAG, "MotionBlur", "u_Texture", "u_PreviousColor")); + /** Fullscreen scene-depth-aware far depth of field (pos2). */ + public static final RenderPipeline DEPTH_OF_FIELD = add(ScreenPipelines.screen("depth_of_field", SHADER_DEPTH_OF_FIELD_FRAG, "DepthOfField", + "u_Texture", "u_MainDepth", "u_TranslucentDepth", "u_ItemEntityDepth", "u_ParticlesDepth", "u_WeatherDepth", "u_CloudsDepth")); + /** Fullscreen wet-ground screen-space reflection pass (pos2). */ + public static final RenderPipeline WET_SURFACE = add(ScreenPipelines.screen("wet_surface", SHADER_WET_SURFACE_FRAG, "WetSurface", "u_Texture", "u_MainDepth")); + /** Fullscreen heat distortion + vignette (pos2). */ + public static final RenderPipeline HEAT_FX = add(ScreenPipelines.screen("heat_fx", SHADER_HEAT_FX_FRAG, "Heat", "u_Texture")); + /** Fullscreen shader ESP gradient composite (pos2). */ + public static final RenderPipeline SHADER_ESP_GRADIENT = add(ScreenPipelines.screen("shader_esp_gradient", SHADER_ESP_GRADIENT_FRAG, "ShaderEspGradient", "u_Texture")); + /** Fullscreen shader ESP separable shadow blur (pos2). */ + public static final RenderPipeline SHADER_ESP_SHADOW = add(ScreenPipelines.screen("shader_esp_shadow", SHADER_ESP_SHADOW_FRAG, "ShaderEspBlur", "u_Texture", "u_Mask")); + /** Fullscreen shader ESP procedural smoke fill (pos2). */ + public static final RenderPipeline SHADER_ESP_SMOKE = add(ScreenPipelines.screen("shader_esp_smoke", SHADER_ESP_SMOKE_FRAG, "ShaderEspSmoke", "u_Texture")); + /** No depth test; additive; textured-color triangles with procedural sun shading. */ + public static final RenderPipeline WORLD_SKY_SUN = add(WorldPipelines.skySun("world_sky_sun")); + /** Fullscreen nether portal rift (pos2). */ + public static final RenderPipeline PORTAL_RIFT = add(ScreenPipelines.screen("portal_rift", SHADER_PORTAL_RIFT_FRAG, "Heat", "u_Texture")); + /** Fullscreen sleep overlay (pos2). */ + public static final RenderPipeline SLEEP_OVERLAY = add(ScreenPipelines.screen("sleep_overlay", SHADER_SLEEP_OVERLAY_FRAG, "PostProcess", "u_Texture")); + /** Fullscreen hand glass (pos2). */ + public static final RenderPipeline HAND_GLASS = add(ScreenPipelines.handEffect("hand_glass", SHADER_HAND_GLASS_FRAG, "HandGlass")); + public static final RenderPipeline HAND_SMOKE = add(ScreenPipelines.handEffect("hand_smoke", SHADER_HAND_SMOKE_FRAG, "HandSmoke")); + public static final RenderPipeline HAND_METALLIC = add(ScreenPipelines.handEffect("hand_metallic", SHADER_HAND_METALLIC_FRAG, "HandMetallic")); + public static final RenderPipeline HAND_NOISE = add(ScreenPipelines.handEffect("hand_noise", SHADER_HAND_NOISE_FRAG, "HandNoise")); + public static final RenderPipeline HAND_CHAMS_SOLID = add(ScreenPipelines.handEffect("hand_chams_solid", SHADER_HAND_CHAMS_SOLID_FRAG, "HandChams")); + public static final RenderPipeline HAND_CHAMS_CHROMA = add(ScreenPipelines.handEffect("hand_chams_chroma", SHADER_HAND_CHAMS_CHROMA_FRAG, "HandChams")); + public static final RenderPipeline HAND_CHAMS_STRIPES = add(ScreenPipelines.handEffect("hand_chams_stripes", SHADER_HAND_CHAMS_STRIPES_FRAG, "HandChams")); + public static final RenderPipeline HAND_CHAMS_GLOW = add(ScreenPipelines.handEffect("hand_chams_glow", SHADER_HAND_CHAMS_GLOW_FRAG, "HandChams")); + public static final RenderPipeline HAND_CHAMS_SNOW = add(ScreenPipelines.handEffect("hand_chams_snow", SHADER_HAND_CHAMS_SNOW_FRAG, "HandChams")); + public static final RenderPipeline HAND_CHAMS_BALATRO = add(ScreenPipelines.handEffect("hand_chams_balatro", SHADER_HAND_CHAMS_BALATRO_FRAG, "HandChams")); + public static final RenderPipeline HAND_CHAMS_BLEND = add(ScreenPipelines.handBlendEffect("hand_chams_blend", SHADER_HAND_CHAMS_BLEND_FRAG)); + /** No depth test; translucent; textured skybox with vanilla sky/fog blending. */ + public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG = add(WorldPipelines.texturedSkyboxFog("world_skybox_textured_fog", SHADER_POS_TEX_COLOR_SKY_FOG_FRAG, BlendFunction.TRANSLUCENT)); + /** No depth test; additive; textured skybox with vanilla sky/fog blending. */ + public static final RenderPipeline WORLD_SKYBOX_TEXTURED_FOG_ADDITIVE = add(WorldPipelines.texturedSkyboxFog("world_skybox_textured_fog_additive", SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG, PipelineTemplates.ADDITIVE)); + /** Fullscreen procedural Reimagined skybox with sky/fog blending. */ + public static final RenderPipeline WORLD_REIMAGINED_SKYBOX_SHADER = add(WorldPipelines.proceduralSkyboxFog("world_reimagined_skybox_shader", SHADER_REIMAGINED_SKYBOX_FRAG)); + /** Fullscreen procedural ShaderSky (Thunder) with sky/fog blending. */ + public static final RenderPipeline WORLD_SHADER_SKY_THUNDER = add(ScreenPipelines.screen("world_shader_sky_thunder", SHADER_SHADER_SKY_THUNDER_FRAG, "ShaderSky")); + /** Fullscreen procedural ShaderSky (Pulsar) with sky/fog blending. */ + public static final RenderPipeline WORLD_SHADER_SKY_PULSAR = add(ScreenPipelines.screen("world_shader_sky_pulsar", SHADER_SHADER_SKY_PULSAR_FRAG, "ShaderSky")); + + // ====================== + // Batched UI shapes (2D) + // ====================== + public static final RenderPipeline UI_CIRCLE_BATCH = add(UiPipelines.batch("ui_circle_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_CIRCLE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ARC_BATCH = add(UiPipelines.batch("ui_arc_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ARC_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ORBIZ_RING_BATCH = add(UiPipelines.batch("ui_orbiz_ring_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_GENERIC_VERT, SHADER_ORBIZ_RING_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_BATCH = add(UiPipelines.batch("ui_rounded_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_FILL_SMOKE_BATCH = add(UiPipelines.batch("ui_rounded_fill_smoke_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_ROUNDED_FILL_SMOKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_STROKE_BATCH = add(UiPipelines.batch("ui_rounded_stroke_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_STROKE_ANGULAR_BATCH = add(UiPipelines.batch("ui_rounded_stroke_angular_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_STROKE_ANGULAR_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_CHAMFERED_BATCH = add(UiPipelines.batch("ui_chamfered_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_CHAMFERED_RECT_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_SHAPE_BATCH = add(UiPipelines.batch("ui_shape_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS5, SHADER_POS_LOCAL_COLOR_RECT_PARAMS5_VERT, SHADER_UI_SHAPE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_CHAMFERED_STROKE_BATCH = add(UiPipelines.batch("ui_chamfered_stroke_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_CHAMFERED_RECT_STROKE_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_STROKE_CORNERS_BATCH = add(UiPipelines.batch("ui_rounded_stroke_corners_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_STROKE_CORNERS_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_CORNERS_BATCH = add(UiPipelines.batch("ui_rounded_corners_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_ROUNDED_RECT_CORNERS_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_GLOW_BATCH = add(UiPipelines.batch("ui_rounded_glow_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_GLOW_BATCH_FRAG, PipelineTemplates.ADDITIVE, true)); + public static final RenderPipeline UI_ROUNDED_SHADOW_BATCH = add(UiPipelines.batch("ui_rounded_shadow_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_SHADOW_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_ROUNDED_SOFT_SHADOW_BATCH = add(UiPipelines.batch("ui_rounded_soft_shadow_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_SOFT_SHADOW_BATCH_FRAG, BlendFunction.TRANSLUCENT, true)); + public static final RenderPipeline UI_RADIAL_GLOW_BATCH = add(UiPipelines.batch("ui_radial_glow_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_RADIAL_GLOW_BATCH_FRAG, PipelineTemplates.ADDITIVE, true)); + public static final RenderPipeline UI_ROUNDED_TEXTURED_BATCH = add(UiPipelines.batch("ui_rounded_textured_batch", + LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_TEX_BATCH_FRAG, BlendFunction.TRANSLUCENT, true, "u_Texture")); + public static final RenderPipeline UI_ROUNDED_TEXTURED_MASK_BATCH = add(UiPipelines.batch("ui_rounded_textured_mask_batch", + LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_ROUNDED_RECT_TEX_MASK_BATCH_FRAG, BlendFunction.TRANSLUCENT, true, "u_Texture")); + public static final RenderPipeline UI_BLUR_BATCH = add(UiPipelines.batch("ui_blur_batch", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS, SHADER_POS_LOCAL_COLOR_RECT_PARAMS_VERT, SHADER_UI_BLUR_BATCH_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture")); + public static final RenderPipeline UI_BLUR_BATCH_CORNERS = add(UiPipelines.batch("ui_blur_batch_corners", + LoVisualVertexFormats.POS2_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_UI_BLUR_BATCH_CORNERS_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture")); + public static final RenderPipeline UI_LIQUID_GLASS_BATCH = add(UiPipelines.batch("ui_liquid_glass_batch", + LoVisualVertexFormats.POS2_TEXTURE_LOCAL_COLOR_RECT_PARAMS2, SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT, SHADER_UI_LIQUID_GLASS_BATCH_FRAG, BlendFunction.TRANSLUCENT, false, "u_Texture", "u_BlurTexture")); /** * Compile all registered pipelines (use after resource reload). @@ -1492,7 +365,7 @@ public enum LoVisualRenderPipelines { } public static RenderPipeline.Snippet meshUniforms() { - return MESH_UNIFORMS; + return PipelineTemplates.MESH_UNIFORMS; } public static RenderPipeline registerAddonPipeline(RenderPipeline pipeline) { @@ -1518,4 +391,11 @@ public enum LoVisualRenderPipelines { } } + /** + * LoVisual shader asset identifier for "shaders/<name>". + */ + private static Identifier shader(String name) { + return Identifier.fromNamespaceAndPath("lovisual", "shaders/" + name); + } + } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java new file mode 100644 index 0000000..7276174 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java @@ -0,0 +1,60 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.pipeline.pipelines; + +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.platform.BlendFactor; +import com.mojang.blaze3d.shaders.UniformType; +import dev.loki.lovisual.render.engine.pipeline.DepthTestFunction; +import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder; +import net.minecraft.resources.Identifier; + +/** + * Shared building blocks for the LoVisual pipeline factories: uniform snippets, blend + * presets and the common depth/blend/cull tail that every custom pipeline applies. + */ +public final class PipelineTemplates { + /** Additive blending (SRC_ALPHA, ONE) shared by all additive LoVisual pipelines. */ + public static final BlendFunction ADDITIVE = new BlendFunction(BlendFactor.SRC_ALPHA, BlendFactor.ONE); + + public static final RenderPipeline.Snippet MESH_UNIFORMS = uniformSnippet("MeshData"); + public static final RenderPipeline.Snippet FOG_UNIFORMS = uniformSnippet("Fog"); + public static final RenderPipeline.Snippet UI_BATCH_UNIFORMS = uniformSnippet("UIBatch"); + + private PipelineTemplates() { + } + + /** + * Snippet declaring a single {@link UniformType#UNIFORM_BUFFER} uniform. + */ + public static RenderPipeline.Snippet uniformSnippet(String name) { + return new ExtendedRenderPipelineBuilder() + .withUniform(name, UniformType.UNIFORM_BUFFER) + .buildSnippet(); + } + + /** + * Location identifier for a "pipeline/<name>" entry in the lovisual namespace. + */ + public static Identifier location(String name) { + return Identifier.fromNamespaceAndPath("lovisual", "pipeline/" + name); + } + + /** + * Applies the shared pipeline tail (depth test, depth write off, blend, cull) and builds. + */ + public static RenderPipeline finish(ExtendedRenderPipelineBuilder builder, boolean depthTest, BlendFunction blend, boolean cull) { + return builder + .withDepthTestFunction(depthTest ? DepthTestFunction.GEQUAL_DEPTH_TEST : DepthTestFunction.NO_DEPTH_TEST) + .withDepthWrite(false) + .withBlend(blend) + .withCull(cull) + .build(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java new file mode 100644 index 0000000..6acea68 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java @@ -0,0 +1,72 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.pipeline.pipelines; + +import com.mojang.blaze3d.PrimitiveTopology; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.shaders.UniformType; +import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.resources.Identifier; + +/** + * Factory templates for LoVisual's fullscreen pos2 pipelines (post effects, hand effects, + * shader skies) plus the vanilla-textured GUI lookup pipeline. Samplers are declared + * before the single uniform buffer, matching the original declaration order. + */ +public final class ScreenPipelines { + private ScreenPipelines() { + } + + /** + * Minimal vanilla-textured pipeline used for GUI texture lookups. + */ + public static RenderPipeline guiTextureLookup() { + return new ExtendedRenderPipelineBuilder(RenderPipelines.GUI_TEXTURED_SNIPPET) + .withLocation(PipelineTemplates.location("gui_texture_lookup")) + .withFragmentShader(LoVisualRenderPipelines.SHADER_GUI_TEXTURE_LOOKUP_FRAG) + .build(); + } + + /** + * Fullscreen pos2 pass: damage-tint vertex shader, no depth test, translucent, no culling. + */ + public static RenderPipeline screen(String name, Identifier fragmentShader, String uniform, String... samplers) { + ExtendedRenderPipelineBuilder builder = base(name) + .withVertexFormat(LoVisualVertexFormats.POS2, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_DAMAGE_TINT_VERT) + .withFragmentShader(fragmentShader); + for (String sampler : samplers) { + builder.withSampler(sampler); + } + builder.withUniform(uniform, UniformType.UNIFORM_BUFFER); + return PipelineTemplates.finish(builder, false, BlendFunction.TRANSLUCENT, false); + } + + /** + * Fullscreen hand effect with the standard "u_Src"/"u_Mask" sampler pair. + */ + public static RenderPipeline handEffect(String name, Identifier fragmentShader, String uniform) { + return screen(name, fragmentShader, uniform, "u_Src", "u_Mask"); + } + + /** + * Fullscreen chams blend effect sampling two shader textures plus the mask. + */ + public static RenderPipeline handBlendEffect(String name, Identifier fragmentShader) { + return screen(name, fragmentShader, "HandBlend", "u_Shader1", "u_Shader2", "u_Mask"); + } + + private static ExtendedRenderPipelineBuilder base(String name) { + return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS) + .withLocation(PipelineTemplates.location(name)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java new file mode 100644 index 0000000..40ae811 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java @@ -0,0 +1,96 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.pipeline.pipelines; + +import com.mojang.blaze3d.PrimitiveTopology; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.shaders.UniformType; +import com.mojang.blaze3d.vertex.VertexFormat; +import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats; +import net.minecraft.resources.Identifier; + +/** + * Factory templates for LoVisual's UI (2D) pipelines: the base colored/textured set and + * the batched SDF shape pipelines built on the local-color rect-params vertex formats. + * Samplers are declared before extra uniform buffers, matching the original order. + */ +public final class UiPipelines { + private UiPipelines() { + } + + /** + * UI colored primitives (pos2 + color); the canvas check in the builder still applies. + */ + public static RenderPipeline colored(String name, PrimitiveTopology topology) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(LoVisualVertexFormats.POS2_COLOR, topology) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), false, BlendFunction.TRANSLUCENT, true); + } + + /** + * UI textured triangles (pos2 + tex + color); {@code uniform} may be null. + */ + public static RenderPipeline textured(String name, Identifier vertexShader, Identifier fragmentShader, + BlendFunction blend, boolean cull, String uniform, String... samplers) { + ExtendedRenderPipelineBuilder builder = base(name) + .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(vertexShader) + .withFragmentShader(fragmentShader); + for (String sampler : samplers) { + builder.withSampler(sampler); + } + if (uniform != null) { + builder.withUniform(uniform, UniformType.UNIFORM_BUFFER); + } + return PipelineTemplates.finish(builder, false, blend, cull); + } + + /** + * UI text with liquid glass sampling; MSDF variants add the "MsdfText" uniform. + */ + public static RenderPipeline textLiquidGlass(String name, Identifier fragmentShader, boolean msdf) { + ExtendedRenderPipelineBuilder builder = base(name) + .withVertexFormat(LoVisualVertexFormats.POS2_TEXTURE_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_TEXT_VERT) + .withFragmentShader(fragmentShader) + .withSampler("u_Texture") + .withSampler("u_SceneTexture") + .withSampler("u_BlurTexture") + .withUniform("UIBatch", UniformType.UNIFORM_BUFFER); + if (msdf) { + builder.withUniform("MsdfText", UniformType.UNIFORM_BUFFER); + } + return PipelineTemplates.finish(builder, false, BlendFunction.TRANSLUCENT, true); + } + + /** + * Batched SDF shape pipeline (local-color rect-params formats) with the shared UI batch + * uniforms; optional texture samplers keep their original declaration order. + */ + public static RenderPipeline batch(String name, VertexFormat format, Identifier vertexShader, Identifier fragmentShader, + BlendFunction blend, boolean cull, String... samplers) { + ExtendedRenderPipelineBuilder builder = new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS, PipelineTemplates.UI_BATCH_UNIFORMS) + .withLocation(PipelineTemplates.location(name)) + .withVertexFormat(format, PrimitiveTopology.TRIANGLES) + .withVertexShader(vertexShader) + .withFragmentShader(fragmentShader); + for (String sampler : samplers) { + builder.withSampler(sampler); + } + return PipelineTemplates.finish(builder, false, blend, cull); + } + + private static ExtendedRenderPipelineBuilder base(String name) { + return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS) + .withLocation(PipelineTemplates.location(name)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java new file mode 100644 index 0000000..1bb0e5d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java @@ -0,0 +1,151 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.pipeline.pipelines; + +import com.mojang.blaze3d.PrimitiveTopology; +import com.mojang.blaze3d.pipeline.BlendFunction; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.shaders.UniformType; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats; +import net.minecraft.resources.Identifier; + +/** + * Factory templates for LoVisual's world-space (3D) pipelines. They all share the mesh + * uniform snippet, disable depth writing and culling; only depth test, blending, shaders + * and vertex format vary. Shader identifiers come from the public facade constants. + */ +public final class WorldPipelines { + private WorldPipelines() { + } + + /** + * Solid colored triangles; {@code depthTest} selects GEQUAL vs no depth test. + */ + public static RenderPipeline colored(String name, boolean depthTest, BlendFunction blend) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, blend, false); + } + + /** + * Smooth debug lines rendered with the plain color shader. + */ + public static RenderPipeline coloredLines(String name, boolean depthTest) { + return PipelineTemplates.finish(base(name) + .withLineSmooth() + .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, PrimitiveTopology.DEBUG_LINES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_COLOR_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, BlendFunction.TRANSLUCENT, false); + } + + /** + * Screen-space wide line expansion; triangles, never depends on glLineWidth. + */ + public static RenderPipeline wideLines(String name, boolean depthTest) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(LoVisualVertexFormats.POS3_COLOR_LINE, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_WIDE_LINE_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_COLOR_FRAG), depthTest, BlendFunction.TRANSLUCENT, false); + } + + /** + * Textured triangles sampling "u_Texture". + */ + public static RenderPipeline textured(String name, boolean depthTest, BlendFunction blend) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_FRAG) + .withSampler("u_Texture"), depthTest, blend, false); + } + + /** + * Textured triangles with shader-side tint ("TextureTint" uniform), additive blending. + */ + public static RenderPipeline tintedTextured(String name, boolean depthTest) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_TINT_FRAG) + .withSampler("u_Texture") + .withUniform("TextureTint", UniformType.UNIFORM_BUFFER), depthTest, PipelineTemplates.ADDITIVE, false); + } + + /** + * World decal quads with SDF fill/stroke. + */ + public static RenderPipeline decalSdf(String name, boolean depthTest) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(LoVisualVertexFormats.POS3_TEXTURE_COLOR_PARAMS2, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_PARAMS2_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_WORLD_DECAL_SDF_FRAG), depthTest, BlendFunction.TRANSLUCENT, false); + } + + /** + * World text quads; MSDF variants add the "MsdfText" uniform after "u_Texture". + */ + public static RenderPipeline worldText(String name, boolean depthTest, Identifier fragmentShader, boolean msdf) { + ExtendedRenderPipelineBuilder builder = base(name) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_TEXT_VERT) + .withFragmentShader(fragmentShader) + .withSampler("u_Texture"); + if (msdf) { + builder.withUniform("MsdfText", UniformType.UNIFORM_BUFFER); + } + return PipelineTemplates.finish(builder, depthTest, BlendFunction.TRANSLUCENT, false); + } + + /** + * Textured triangles with procedural sun shading ("SkySun" uniform), additive. + */ + public static RenderPipeline skySun(String name) { + return PipelineTemplates.finish(base(name) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_VERT) + .withFragmentShader(LoVisualRenderPipelines.SHADER_SKY_SUN_FRAG) + .withUniform("SkySun", UniformType.UNIFORM_BUFFER), false, PipelineTemplates.ADDITIVE, false); + } + + /** + * Textured skybox with vanilla sky/fog blending ("SkyboxShader" uniform + fog snippet). + */ + public static RenderPipeline texturedSkyboxFog(String name, Identifier fragmentShader, BlendFunction blend) { + return PipelineTemplates.finish(fogBase(name) + .withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_POS_TEX_COLOR_SKY_FOG_VERT) + .withFragmentShader(fragmentShader) + .withSampler("u_Texture") + .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER), false, blend, false); + } + + /** + * Fullscreen procedural skybox with sky/fog blending ("SkyboxShader" uniform + fog snippet). + */ + public static RenderPipeline proceduralSkyboxFog(String name, Identifier fragmentShader) { + return PipelineTemplates.finish(fogBase(name) + .withVertexFormat(LoVisualVertexFormats.POS2, PrimitiveTopology.TRIANGLES) + .withVertexShader(LoVisualRenderPipelines.SHADER_DAMAGE_TINT_VERT) + .withFragmentShader(fragmentShader) + .withUniform("SkyboxShader", UniformType.UNIFORM_BUFFER), false, BlendFunction.TRANSLUCENT, false); + } + + private static ExtendedRenderPipelineBuilder base(String name) { + return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS) + .withLocation(PipelineTemplates.location(name)); + } + + private static ExtendedRenderPipelineBuilder fogBase(String name) { + return new ExtendedRenderPipelineBuilder(PipelineTemplates.MESH_UNIFORMS, PipelineTemplates.FOG_UNIFORMS) + .withLocation(PipelineTemplates.location(name)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java new file mode 100644 index 0000000..dd8a0c7 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/HandNoiseUniforms.java @@ -0,0 +1,88 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.uniform.impl; + +import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem; + +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.buffers.Std140Builder; +import com.mojang.blaze3d.buffers.Std140SizeCalculator; +import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator; + +public enum HandNoiseUniforms { + ; + public static final int SIZE = new Std140SizeCalculator() + .putFloat().putFloat().putFloat().putFloat() // u_Color + .putFloat().putFloat().putFloat().putFloat() // u_Glow + .putFloat().putFloat().putFloat().putFloat() // u_Params0 + .putFloat().putFloat().putFloat().putFloat() // u_Params1 + .putFloat().putFloat().putFloat().putFloat() // u_Params2 + .get(); + + private static final Data DATA = new Data(); + private static final String UNIFORM_NAME = "LoVisual - Hand Noise UBO"; + private static final int EXPECTED_WRITES_PER_FRAME = 16; + + public static void update( + float colorR, float colorG, float colorB, float colorA, + float glowR, float glowG, float glowB, float glowA, + float edgeWidth, float quality, float octaves, float time, + float resX, float resY, float scale, float speed, + float contrast, float breathe, float glowStrength, float density + ) { + DATA.colorR = colorR; + DATA.colorG = colorG; + DATA.colorB = colorB; + DATA.colorA = colorA; + DATA.glowR = glowR; + DATA.glowG = glowG; + DATA.glowB = glowB; + DATA.glowA = glowA; + DATA.edgeWidth = edgeWidth; + DATA.quality = quality; + DATA.octaves = octaves; + DATA.time = time; + DATA.resX = resX; + DATA.resY = resY; + DATA.scale = scale; + DATA.speed = speed; + DATA.contrast = contrast; + DATA.breathe = breathe; + DATA.glowStrength = glowStrength; + DATA.density = density; + LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA); + } + + public static GpuBufferSlice get() { + return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME); + } + + private static final class Data implements LoVisualUniformAllocator.UniformWriter { + private float colorR, colorG, colorB, colorA; + private float glowR, glowG, glowB, glowA; + private float edgeWidth, quality, octaves, time; + private float resX, resY, scale, speed; + private float contrast, breathe, glowStrength, density; + + @Override + public void write(java.nio.ByteBuffer buffer) { + Std140Builder.intoBuffer(buffer) + .putFloat(colorR).putFloat(colorG).putFloat(colorB).putFloat(colorA) + .putFloat(glowR).putFloat(glowG).putFloat(glowB).putFloat(glowA) + .putFloat(edgeWidth).putFloat(quality).putFloat(octaves).putFloat(time) + .putFloat(resX).putFloat(resY).putFloat(scale).putFloat(speed) + .putFloat(contrast).putFloat(breathe).putFloat(glowStrength).putFloat(density); + } + + @Override + public boolean equals(Object o) { + return false; + } + } + +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java new file mode 100644 index 0000000..0788c45 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java @@ -0,0 +1,36 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation; + +/** + * Кривые анимации для equip-движений руки. Чистая математика без Minecraft. + */ +public enum EaseCurves { + ; + + /** + * Ease-out-back: перелетает чуть дальше 1.0 перед установкой, давая движению вес. + * + * @param c1 сила «отдачи» (оригинал — 1.70158) + */ + public static float easeOutBack(float x, float c1) { + float c3 = c1 + 1.0f; + float t = x - 1.0f; + return 1.0f + c3 * t * t * t + c1 * t * t; + } + + /** + * Перекладывает линейный [0,1] equip-прогресс через ease-out-back, + * затем подмешивает по intensity: 0 — ванильная линейность, 1 — полная кривая. + */ + public static float shapeEquipProgress(float linear, float c1, float intensity) { + float eased = easeOutBack(linear, c1); + float amount = Math.max(0.0f, Math.min(1.0f, intensity)); + return linear + (eased - linear) * amount; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java new file mode 100644 index 0000000..7f48244 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java @@ -0,0 +1,32 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation; + +/** + * Единая цепочка обработки equip-прогресса руки: сначала кривая HoldMyItems + * (перелёт через ease-out-back на полном диапазоне 0..1), затем занижение + * амплитуды от ViewModel. Порядок важен: кривая должна получать полный + * диапазон, а ViewModel — масштабировать уже готовое движение, иначе + * конечная позиция предмета уезжает. + */ +public enum EquipProgressComposer { + ; + + public static float compose(float linear, + boolean curveEnabled, float curveC1, float curveIntensity, + boolean lowerEnabled, float lowering) { + float progress = linear; + if (curveEnabled) { + progress = EaseCurves.shapeEquipProgress(progress, curveC1, curveIntensity); + } + if (lowerEnabled) { + progress *= lowering; + } + return progress; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java new file mode 100644 index 0000000..1f0c442 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java @@ -0,0 +1,119 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation; + +/** + * Frame-rate independent smooth 0..1 transition value driven by a boolean state. + *

+ * The contract is intentionally strict so animations can never fight each other: + * the owner computes the target once per frame with {@link #setTarget(boolean)} + * and advances the value exactly once per frame with {@link #update(float)}. + * Renderers only read {@link #value()}. The value approaches the target with + * exponential easing {@code value += (target - value) * (1 - exp(-speed * delta))}, + * which is monotonic, symmetric for appear/disappear, and identical in wall-clock + * time regardless of frame rate. + *

+ * This class is pure Java: no Minecraft types, safe to unit test. + */ +public final class SmoothValue { + + /** + * When the remaining distance to the target drops below this, the value snaps + * exactly onto the target so it converges in finite time instead of forever + * trailing a microscopic difference. + */ + private static final float CONVERGENCE_EPSILON = 1.0e-4f; + + private final float speed; + private float value; + private float target; + + /** + * @param speed approach rate in 1/delta units; higher is faster. + * With delta measured in ticks (20 per second), a speed of + * {@code s} needs roughly {@code ln(100) / s} ticks to close 99% of a transition. + */ + public SmoothValue(float speed) { + this(speed, 0.0f); + } + + public SmoothValue(float speed, float initialValue) { + if (!isFinite(speed) || speed <= 0.0f) { + throw new IllegalArgumentException("speed must be finite and positive: " + speed); + } + this.speed = speed; + this.value = clamp01(initialValue); + this.target = this.value; + } + + /** + * Sets the goal for the next {@link #update(float)} calls: 1 when the animated + * state is active, 0 when it is not. Does not move the value by itself. + */ + public void setTarget(boolean active) { + target = active ? 1.0f : 0.0f; + } + + /** + * Advances the value one step toward the target. Call exactly once per frame. + * + * @param delta elapsed time since the previous update, in the same units the + * speed was configured for (ticks in this client). Non-positive + * or broken deltas are ignored. + */ + public void update(float delta) { + if (!isFinite(delta) || delta <= 0.0f) { + return; + } + + float factor = 1.0f - (float) Math.exp(-speed * delta); + if (factor <= 0.0f) { + return; + } + + value += (target - value) * factor; + value = clamp01(value); + + if (Math.abs(target - value) <= CONVERGENCE_EPSILON) { + value = target; + } + } + + /** + * Teleports both value and target: no transition, the next frame renders settled. + */ + public void setInstant(boolean active) { + target = active ? 1.0f : 0.0f; + value = target; + } + + /** Current smoothed position in 0..1. */ + public float value() { + return value; + } + + /** Current goal in 0..1 as last set by {@link #setTarget(boolean)}. */ + public float target() { + return target; + } + + /** True once the value has fully reached the target. */ + public boolean isSettled() { + return value == target; + } + + private static float clamp01(float v) { + if (v < 0.0f) return 0.0f; + if (v > 1.0f) return 1.0f; + return v; + } + + private static boolean isFinite(float v) { + return !Float.isNaN(v) && !Float.isInfinite(v); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java new file mode 100644 index 0000000..32c92fd --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java @@ -0,0 +1,60 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +/** + * Чистая математика синусоидального покачивания предмета при ходьбе/беге. + *

+ * Фаза берётся из накопителя шагов игрока ({@code walkAnimation.position()}), + * амплитуда — из нормализованной скорости ({@code walkAnimation.speed()}, + * 0..1). Один цикл латерали соответствует двум шагам, вертикаль проседает + * дважды за цикл с лёгким сдвигом фазы (как у ванильного боба камеры). + * Класс чистый: без Minecraft-типов, покрыт юнит-тестами. + */ +public enum BobMath { + ; + + /** Радиан на единицу пройденной фазы (та же частота шагов, что у конечностей). */ + public static final float STEP_RATE = 0.6662f; + + /** Сдвиг фазы вертикали, чтобы проседание не совпадало с крайними точками латерали. */ + static final float VERTICAL_PHASE_OFFSET = 0.2f; + + /** + * Амплитуда боба по нормализованной скорости: 0 — стоим, 1 — бег/полная ходьба. + * Некорректные входы (NaN/Inf) дают 0. + */ + public static float amplitude(float speed01) { + if (Float.isNaN(speed01) || Float.isInfinite(speed01)) { + return 0.0f; + } + if (speed01 <= 0.0f) { + return 0.0f; + } + return Math.min(1.0f, speed01); + } + + /** Латеральная компонента, -1..1, период — два шага. */ + public static float lateral(float phase) { + return (float) Math.sin(phase * STEP_RATE); + } + + /** Вертикальная компонента, -1..0: проседание дважды за цикл. */ + public static float vertical(float phase) { + float c = (float) Math.cos(phase * STEP_RATE - VERTICAL_PHASE_OFFSET); + return -Math.abs(c); + } + + /** + * Точка фазы, где латеральная компонента достигает максимума: + * полезна тестам для проверки симметрии цикла. + */ + public static float lateralPeakPhase() { + return (float) (Math.PI / 2.0 / STEP_RATE); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java new file mode 100644 index 0000000..305ef0d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandAnimState.java @@ -0,0 +1,125 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +import dev.loki.lovisual.util.animation.SmoothValue; + +/** + * Покадровое состояние динамики рук: сводит sway, дрейф использования и + * появление при входе в мир. Обновляется ровно один раз за кадр + * ({@link #beginFrame}), рендер только читает значения — это исключает + * двойные шаги, когда кадр рендерит обе руки. + *

+ * Player/level передаются как Object-идентичности: смена игрока или мира + * перезапускает анимацию появления и гасит ложные дельты взгляда. + * Класс чистый: без Minecraft-типов, покрыт юнит-тестами. + */ +public final class HandAnimState { + + /** Резкая смена взгляда больше этих пределов считается телепортом, а не движением мыши. */ + static final float TELEPORT_YAW_DEG = 45.0f; + static final float TELEPORT_PITCH_DEG = 25.0f; + static final float MAX_FRAME_DELTA_TICKS = 5.0f; + + /** Скорость дрейфа использования (99% за ~ln(100)/speed тиков). */ + static final float DRIFT_SPEED = 1.8f; + /** Скорость появления при входе: ~10 тиков до почти полной посадки. */ + static final float JOIN_SPEED = 0.45f; + + private final SwayState sway = new SwayState(); + private final SmoothValue driftMain = new SmoothValue(DRIFT_SPEED); + private final SmoothValue driftOff = new SmoothValue(DRIFT_SPEED); + private final SmoothValue join = new SmoothValue(JOIN_SPEED); + + private float lastYaw = Float.NaN; + private float lastPitch = Float.NaN; + private Object lastPlayer; + private Object lastLevel; + + /** + * Один шаг на кадр. + * + * @param player идентичность игрока (для смены мира/респавна) + * @param level идентичность мира + * @param yaw текущий yaw взгляда, градусы (любой диапазон) + * @param pitch текущий pitch взгляда, градусы + * @param usingMain предмет используется в главной руке + * @param usingOff предмет используется во второй руке + * @param deltaTicks реальное время кадра в тиках + */ + public void beginFrame(Object player, Object level, float yaw, float pitch, + boolean usingMain, boolean usingOff, float deltaTicks) { + if (player == null) { + reset(); + return; + } + + if (player != lastPlayer || level != lastLevel) { + lastPlayer = player; + lastLevel = level; + join.setInstant(false); + lastYaw = Float.NaN; + lastPitch = Float.NaN; + } + + float dt = SwayState.sanitize(deltaTicks); + if (Float.isNaN(lastYaw)) { + lastYaw = yaw; + lastPitch = pitch; + return; + } + + float deltaYaw = SwayState.wrapDegrees(yaw - lastYaw); + float deltaPitch = pitch - lastPitch; + lastYaw = yaw; + lastPitch = pitch; + + boolean lookJump = Math.abs(deltaYaw) > TELEPORT_YAW_DEG + || Math.abs(deltaPitch) > TELEPORT_PITCH_DEG; + + if (dt > 0.0f) { + if (lookJump) { + sway.reset(); + } else { + sway.update(deltaYaw, deltaPitch, Math.min(dt, MAX_FRAME_DELTA_TICKS)); + } + driftMain.setTarget(usingMain); + driftMain.update(dt); + driftOff.setTarget(usingOff); + driftOff.update(dt); + join.setTarget(true); + join.update(dt); + } + } + + /** Полный сброс: следующий кадр считается первым после входа в мир. */ + public void reset() { + sway.reset(); + driftMain.setInstant(false); + driftOff.setInstant(false); + join.setInstant(false); + lastYaw = Float.NaN; + lastPitch = Float.NaN; + lastPlayer = null; + lastLevel = null; + } + + public SwayState sway() { + return sway; + } + + /** Сглаженный прогресс дрейфа для указанной руки, 0..1. */ + public float drift(boolean mainHand) { + return (mainHand ? driftMain : driftOff).value(); + } + + /** Прогресс появления при входе в мир, 0..1 (1 — предмет встал на место). */ + public float joinProgress() { + return join.value(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java new file mode 100644 index 0000000..9d9e5dd --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java @@ -0,0 +1,160 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +/** + * Сборка итоговой позы руки из отдельных эффектов: sway от обзора, боб от + * ходьбы, дрейф к центру при использовании предмета и «въезд» при входе в мир. + *

+ * Конвенции знаков соответствуют пространству ванильного рендера руки: + * +X — влево на экране (правая рука стоит при X<0), +Y — вверх, +Z — к + * зрителю; углы в градусах. Смещения — в условных блоках. + * Класс чистый: без Minecraft-типов, покрыт юнит-тестами. + */ +public enum HandPoseMath { + ; + + /** Нулевая поза — применять трансформы не нужно. */ + public static final Pose ZERO = new Pose(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); + + private static final float SWAY_POS_PER_DEG = 0.010f; + private static final float SWAY_ROT_Y_PER_DEG = 1.4f; + private static final float SWAY_ROT_X_PER_DEG = 1.1f; + private static final float SWAY_ROT_Z_PER_DEG = 0.5f; + + private static final float BOB_POS_X = 0.045f; + private static final float BOB_POS_Y = 0.035f; + private static final float BOB_ROT_Z_DEG = 2.5f; + private static final float BOB_ROT_X_DEG = 3.0f; + + private static final float DRIFT_POS_X = 0.10f; + private static final float DRIFT_POS_Z = 0.05f; + private static final float DRIFT_ROT_Y_DEG = 10.0f; + private static final float DRIFT_ROT_Z_DEG = 9.0f; + + private static final float JOIN_DROP = 0.85f; + private static final float JOIN_TILT_DEG = 18.0f; + + /** + * Отставание от поворота камеры: камера повернула вправо (yaw+) — предмет + * сместился влево на экране (+X) и довернулся назад (ry-), взгляд вниз + * (pitch+) — предмет приподнялся (+Y) и задрался (rx-). + */ + public static Pose sway(float yawLagDeg, float pitchLagDeg, float strength) { + float s = scale(strength); + if (s <= 0.0f) { + return ZERO; + } + return new Pose( + yawLagDeg * SWAY_POS_PER_DEG * s, + pitchLagDeg * SWAY_POS_PER_DEG * s, + 0.0f, + -pitchLagDeg * SWAY_ROT_X_PER_DEG * s, + -yawLagDeg * SWAY_ROT_Y_PER_DEG * s, + yawLagDeg * SWAY_ROT_Z_PER_DEG * s + ); + } + + /** Покачивание при ходьбе: {@code phase} — позиция шага, {@code amplitude01} — 0..1 от скорости. */ + public static Pose bob(float phase, float amplitude01, float strength) { + float a = clamp01(amplitude01) * scale(strength); + if (a <= 0.0f) { + return ZERO; + } + float lateral = BobMath.lateral(phase); + float vertical = BobMath.vertical(phase); + return new Pose( + -lateral * BOB_POS_X * a, + vertical * BOB_POS_Y * a, + 0.0f, + vertical * BOB_ROT_X_DEG * a, + 0.0f, + -lateral * BOB_ROT_Z_DEG * a + ); + } + + /** + * Дрейф используемого предмета (еда/зелье/щит) к центру экрана. + * Правая рука тянется влево (+X), левая — вправо (-X), плюс лёгкий + * наклон внутрь. + */ + public static Pose drift(float progress01, float strength, boolean rightArm) { + float s = clamp01(progress01) * scale(strength); + if (s <= 0.0f) { + return ZERO; + } + float side = rightArm ? 1.0f : -1.0f; + return new Pose( + side * DRIFT_POS_X * s, + 0.0f, + -DRIFT_POS_Z * s, + 0.0f, + -side * DRIFT_ROT_Y_DEG * s, + side * DRIFT_ROT_Z_DEG * s + ); + } + + /** Появление при входе в мир: предмет въезжает снизу и распрямляется. */ + public static Pose joinDrop(float progress01) { + float p = clamp01(progress01); + float remain = 1.0f - p; + if (remain <= 0.0f) { + return ZERO; + } + float square = remain * remain; + return new Pose(0.0f, -JOIN_DROP * square, 0.0f, JOIN_TILT_DEG * square, 0.0f, 0.0f); + } + + /** Поэлементная сумма поз. */ + public static Pose add(Pose a, Pose b) { + if (a == ZERO) return b; + if (b == ZERO) return a; + return new Pose( + a.dx() + b.dx(), + a.dy() + b.dy(), + a.dz() + b.dz(), + a.rx() + b.rx(), + a.ry() + b.ry(), + a.rz() + b.rz() + ); + } + + static float clamp01(float v) { + if (Float.isNaN(v)) return 0.0f; + if (v < 0.0f) return 0.0f; + if (v > 1.0f) return 1.0f; + return v; + } + + /** + * Множитель силы настройки: линейный, без потолка — диапазон настроек 0..2 + * должен именно усиливать эффект, а не обрезаться. Отрицательные и + * сломанные значения дают ноль. + */ + static float scale(float strength) { + if (Float.isNaN(strength) || Float.isInfinite(strength)) return 0.0f; + return Math.max(0.0f, strength); + } + + /** + * Итоговая поза руки: смещение (dx, dy, dz) и поворот (rx, ry, rz в градусах), + * применяемые вокруг точки взгляда до ванильных трансформов руки. + */ + public record Pose(float dx, float dy, float dz, float rx, float ry, float rz) { + + /** Все компоненты по модулю не больше epsilon — трансформ можно не применять. */ + public boolean isSignificant(float epsilon) { + return Math.abs(dx) > epsilon + || Math.abs(dy) > epsilon + || Math.abs(dz) > epsilon + || Math.abs(rx) > epsilon + || Math.abs(ry) > epsilon + || Math.abs(rz) > epsilon; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java new file mode 100644 index 0000000..c4c2a44 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/SwayState.java @@ -0,0 +1,117 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +/** + * Накопитель «отставания» предмета от поворота камеры (view sway). + *

+ * Модель: дельты взгляда за кадр (в градусах) накапливаются в отставание + * {@code lag}, ограниченное сверху, и экспоненциально затухают к нулю + * (RELAX). Отображаемое значение догоняет накопленное отставание + * экспонентой (RESPONSE). Всё в градусах, время — в тиках, поэтому + * поведение не зависит от частоты кадров. + *

+ * Класс чистый: без Minecraft-типов, покрыт юнит-тестами. + */ +public final class SwayState { + + public static final float MAX_YAW_LAG_DEG = 6.0f; + public static final float MAX_PITCH_LAG_DEG = 4.0f; + + /** Скорость догоняния отображаемого отставания, 1/тик. */ + static final float RESPONSE_PER_TICK = 6.0f; + /** Скорость возврата накопленного отставания к нулю, 1/тик. */ + static final float RELAX_PER_TICK = 0.25f; + /** Верхняя граница шага, чтобы пауза/лаг не давали рывок. */ + static final float MAX_DELTA_TICKS = 5.0f; + + /** Ниже этого всё состояние защёлкивается в точный ноль — конечное время затухания. */ + private static final float SETTLE_EPSILON = 1.0e-4f; + + private float yawLag; // накопленное отставание по yaw, градусы + private float pitchLag; // накопленное отставание по pitch, градусы + private float yawSmooth; // отображаемое отставание по yaw, градусы + private float pitchSmooth; + + /** + * Продвигает состояние на один кадр. + * + * @param deltaYawDeg изменение yaw камеры за кадр (может быть любым по знаку) + * @param deltaPitchDeg изменение pitch камеры за кадр + * @param deltaTicks реальное время кадра в тиках; неположительное пропускается + */ + public void update(float deltaYawDeg, float deltaPitchDeg, float deltaTicks) { + float dt = sanitize(deltaTicks); + if (dt <= 0.0f) { + return; + } + yawLag = clamp(yawLag + sanitize(deltaYawDeg), -MAX_YAW_LAG_DEG, MAX_YAW_LAG_DEG); + pitchLag = clamp(pitchLag + sanitize(deltaPitchDeg), -MAX_PITCH_LAG_DEG, MAX_PITCH_LAG_DEG); + + float follow = 1.0f - (float) Math.exp(-RESPONSE_PER_TICK * dt); + yawSmooth += (yawLag - yawSmooth) * follow; + pitchSmooth += (pitchLag - pitchSmooth) * follow; + + float relax = (float) Math.exp(-RELAX_PER_TICK * dt); + yawLag *= relax; + pitchLag *= relax; + + if (Math.abs(yawLag) <= SETTLE_EPSILON && Math.abs(pitchLag) <= SETTLE_EPSILON + && Math.abs(yawSmooth) <= SETTLE_EPSILON && Math.abs(pitchSmooth) <= SETTLE_EPSILON) { + reset(); + } + } + + /** Мгновенный сброс: предмет стоит ровно, без хвостов. */ + public void reset() { + yawLag = 0.0f; + pitchLag = 0.0f; + yawSmooth = 0.0f; + pitchSmooth = 0.0f; + } + + /** Сглаженное отставание по yaw в градусах. */ + public float yawLag() { + return yawSmooth; + } + + /** Сглаженное отставание по pitch в градусах. */ + public float pitchLag() { + return pitchSmooth; + } + + /** Обе оси в нуле — считать можно не применять трансформ вовсе. */ + public boolean isSettled() { + return yawLag == 0.0f && pitchLag == 0.0f && yawSmooth == 0.0f && pitchSmooth == 0.0f; + } + + /** + * Приводит разницу углов к диапазону [-180, 180): переход через + * границу мира не даёт ложного рывка на 360 градусов. + */ + public static float wrapDegrees(float degrees) { + float d = sanitize(degrees) % 360.0f; + if (d >= 180.0f) { + d -= 360.0f; + } + if (d < -180.0f) { + d += 360.0f; + } + return d; + } + + static float sanitize(float v) { + return (Float.isNaN(v) || Float.isInfinite(v)) ? 0.0f : v; + } + + static float clamp(float v, float min, float max) { + if (v < min) return min; + if (v > max) return max; + return v; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java new file mode 100644 index 0000000..a5f7db8 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java @@ -0,0 +1,61 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.protection; + +/** + * Minimal pure throttle for repeated warnings. + * + *

The caller supplies the current time, so the class has no clock + * dependency and stays unit-testable.

+ */ +public final class NotifyThrottle { + + private final long intervalMs; + private boolean fired; + private long lastMs; + + public NotifyThrottle(long intervalMs) { + this.intervalMs = Math.max(0L, intervalMs); + } + + /** + * Returns true when the caller may emit a notification now, recording + * the given time as the last emission. Returns false while the previous + * emission is still inside the interval. + */ + public boolean tryAcquire(long nowMs) { + if (remainingMs(nowMs) > 0L) { + return false; + } + fired = true; + lastMs = nowMs; + return true; + } + + /** + * Milliseconds left until the next notification is allowed. + * Zero when the throttle has never fired or the window elapsed. + */ + public long remainingMs(long nowMs) { + if (!fired) { + return 0L; + } + long elapsed = nowMs - lastMs; + if (elapsed < 0L) { + // Clock moved backwards: do not lock out the caller. + return 0L; + } + long left = intervalMs - elapsed; + return Math.max(0L, left); + } + + public void reset() { + fired = false; + lastMs = 0L; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java new file mode 100644 index 0000000..935c5fa --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java @@ -0,0 +1,71 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.protection; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Pure decision logic for the LockSlot module: whether dropping an item + * from a given hotbar slot must be blocked. + */ +public final class SlotLockPolicy { + + public static final int HOTBAR_SIZE = 9; + + private SlotLockPolicy() { + } + + /** + * Default setting state: all hotbar slots unlocked. + * Keys are ordered "slot1".."slot9". + */ + public static Map defaultSlotStates() { + Map out = new LinkedHashMap<>(); + for (int i = 0; i < HOTBAR_SIZE; i++) { + out.put(slotKey(i), false); + } + return out; + } + + /** + * Setting key for a zero-based hotbar slot: 0 -> "slot1", 8 -> "slot9". + */ + public static String slotKey(int hotbarSlot) { + return "slot" + (hotbarSlot + 1); + } + + /** + * True when the slot is explicitly locked by the given states map. + * Missing or out-of-range slots are treated as unlocked. + */ + public static boolean isSlotLocked(Map slotStates, int hotbarSlot) { + if (slotStates == null || slotStates.isEmpty()) return false; + if (hotbarSlot < 0 || hotbarSlot >= HOTBAR_SIZE) return false; + Boolean locked = slotStates.get(slotKey(hotbarSlot)); + return locked != null && locked; + } + + /** + * Full drop decision for a hotbar slot. + * + * @param slotStates locked-slot setting states keyed by {@link #slotKey(int)} + * @param selectedSlot zero-based hotbar slot the drop comes from + * @param playersNearby whether other players are within the guard radius + * @param onlyNearby the "only_players_nearby" setting value + */ + public static boolean isDropBlocked( + Map slotStates, + int selectedSlot, + boolean playersNearby, + boolean onlyNearby + ) { + if (!isSlotLocked(slotStates, selectedSlot)) return false; + return !onlyNearby || playersNearby; + } +} diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json index c64ab3d..8586cb7 100644 --- a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json +++ b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json @@ -522,7 +522,6 @@ "setting.worldtweaks.change_time": "Change time", "setting.worldtweaks.time_hours": "Time (hours)", "setting.worldtweaks.sync_system_time": "Sync with system time", - "setting.worldtweaks.item_physics": "Item physics", "setting.fovcontrol.use_custom_fov": "Use custom FOV", "setting.fovcontrol.custom_fov": "Custom FOV", "setting.fovcontrol.expand_scale": "Expand scale", @@ -1297,8 +1296,6 @@ "setting.blockhighlight.breaking_animation.option.static": "Static", "setting.blockhighlight.breaking_color": "Breaking color", "setting.blockhighlight.breaking_color2": "Second breaking color", - "setting.invisesp.color": "Color", - "setting.invisesp.line_width": "Line width", "setting.holeesp.range_xz": "Range XZ", "setting.holeesp.range_y": "Range Y", "setting.holeesp.height": "Height", @@ -1913,7 +1910,19 @@ "setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime", "setting.draggable.triangulator.triangulator_auto_reset": "Auto reset", "module.autobed.description": "Automatically places and detonates beds in explosive dimensions.", - "module.invisesp.description": "Outlines players hidden by invisibility.", + "module.invisesp.description": "Makes invisible players visible with adjustable translucency.", + "setting.invisesp.opacity": "Opacity", + "module.itemphysic.description": "Gives dropped items physical behavior: they lie flat on the ground and spin in the air.", + "setting.itemphysic.reduce_size": "Reduce item size", + "setting.itemphysic.lay_flat": "Lie flat on ground", + "setting.itemphysic.spin_in_air": "Spin in air", + "setting.itemphysic.no_bob": "No bobbing", + "module.atmodawnfog.description": "Cinematic atmosphere: dawn/dusk fog that follows the time of day and camera altitude.", + "setting.atmodawnfog.mode": "Mode", + "setting.atmodawnfog.density": "Density", + "setting.atmodawnfog.scatter_height": "Scatter height", + "setting.atmodawnfog.softness": "Softness", + "setting.atmodawnfog.dawn_color": "Dawn color", "module.holeesp.description": "Highlights safe holes with a distance-faded fill.", "setting.autobed.place": "Place", "setting.autobed.explode": "Explode", @@ -1963,6 +1972,8 @@ "setting.autobed.text_color": "Text color", "setting.autobed.slide_delay": "Slide delay", "setting.autobed.fade_time": "Fade time", + "module.shulkerpreview.description": "Preview shulker box contents by hovering it in an inventory.", + "setting.shulkerpreview.scale": "Scale", "module.rubberhand.description": "Allows attacking and module block-use actions while an item is actively being used.", "setting.rubberhand.actions": "Actions", "setting.rubberhand.actions.option.attack_while_shield": "Hit while shielding", @@ -3213,5 +3224,44 @@ "setting.chinahat.brim_thickness": "Brim thickness", "setting.chinahat.head_top_offset": "Head height", "setting.chinahat.head_forward_offset": "Forward offset", - "setting.chinahat.head_right_offset": "Right offset" + "setting.chinahat.head_right_offset": "Right offset", + "module.animations.description": "Animates interface transitions.", + "setting.animations.hotbar_raise": "Hotbar raise", + "setting.animations.inventory": "Inventory open", + "module.nofrienddamage.description": "Blocks attacks on players added as friends.", + "module.lockslot.description": "Blocks dropping items from locked hotbar slots (anti-accidental Q).", + "setting.lockslot.slots": "Locked slots", + "setting.lockslot.only_players_nearby": "Only with players nearby", + "setting.lockslot.slots.option.slot1": "Slot 1", + "setting.lockslot.slots.option.slot2": "Slot 2", + "setting.lockslot.slots.option.slot3": "Slot 3", + "setting.lockslot.slots.option.slot4": "Slot 4", + "setting.lockslot.slots.option.slot5": "Slot 5", + "setting.lockslot.slots.option.slot6": "Slot 6", + "setting.lockslot.slots.option.slot7": "Slot 7", + "setting.lockslot.slots.option.slot8": "Slot 8", + "setting.lockslot.slots.option.slot9": "Slot 9", + "module.noserverpack.description": "Blocks forced server resource packs: auto-declines pushes and can remove already applied packs.", + "setting.noserverpack.action": "Action", + "setting.noserverpack.action.option.Decline": "Decline", + "setting.noserverpack.action.option.Remove": "Remove", + "setting.noserverpack.action.option.Ask": "Ask", + "module.tapemouse.description": "Repeats the selected mouse button on an interval, e.g. for AFK farms.", + "setting.tapemouse.button": "Button", + "setting.tapemouse.button.option.Attack": "Attack", + "setting.tapemouse.button.option.Use": "Use", + "setting.tapemouse.interval": "Interval", + "setting.tapemouse.only_hold": "Only while held", + "module.autorespawn.description": "Automatically respawns after death once the configured delay passes.", + "setting.autorespawn.delay": "Delay", + "module.deathcoords.description": "Sends the coordinates of your latest death to the chat.", + "setting.deathcoords.show_dimension": "Show dimension", + "setting.holdmyitems.sway_intensity": "Sway intensity", + "setting.holdmyitems.ease_strength": "Equip ease strength", + "setting.holdmyitems.offhand": "Apply to offhand", + "setting.holdmyitems.walk_bob": "Walk bobbing", + "setting.holdmyitems.bob_strength": "Bobbing strength", + "setting.holdmyitems.use_drift": "Using-item drift", + "setting.holdmyitems.drift_strength": "Drift strength", + "setting.holdmyitems.join_appear": "Smooth world entry" } diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json index 485bc83..3f95ed6 100644 --- a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json +++ b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json @@ -457,7 +457,6 @@ "setting.worldtweaks.change_time": "Изменять время", "setting.worldtweaks.time_hours": "Время (часы)", "setting.worldtweaks.sync_system_time": "Синхронизировать с временем системы", - "setting.worldtweaks.item_physics": "Физика предметов", "setting.fovcontrol.use_custom_fov": "Свой FOV", "setting.fovcontrol.custom_fov": "Пользовательский FOV", "setting.fovcontrol.expand_scale": "Масштаб расширения", @@ -1092,8 +1091,6 @@ "setting.blockhighlight.breaking_color": "Цвет ломания", "setting.blockhighlight.breaking_color2": "Второй цвет ломания", "setting.aspectratio.aspect_multiplier": "Множитель соотношения сторон", - "setting.invisesp.color": "Цвет", - "setting.invisesp.line_width": "Толщина линии", "setting.holeesp.range_xz": "Радиус XZ", "setting.holeesp.range_y": "Радиус Y", "setting.holeesp.height": "Высота", @@ -1708,7 +1705,19 @@ "setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша", "setting.draggable.triangulator.triangulator_auto_reset": "Автосброс", "module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.", - "module.invisesp.description": "Обводит игроков, скрытых невидимостью.", + "module.invisesp.description": "Делает невидимых игроков видимыми (полупрозрачно).", + "setting.invisesp.opacity": "Прозрачность", + "module.itemphysic.description": "Даёт предметам на земле физику: лежат плашмя, крутятся в воздухе.", + "setting.itemphysic.reduce_size": "Уменьшить размер", + "setting.itemphysic.lay_flat": "Лежит плашмя на земле", + "setting.itemphysic.spin_in_air": "Крутится в воздухе", + "setting.itemphysic.no_bob": "Без покачивания", + "module.atmodawnfog.description": "Кинематографичная атмосфера: туман рассвета/заката, зависящий от времени суток и высоты.", + "setting.atmodawnfog.mode": "Режим", + "setting.atmodawnfog.density": "Плотность", + "setting.atmodawnfog.scatter_height": "Высота рассеивания", + "setting.atmodawnfog.softness": "Мягкость", + "setting.atmodawnfog.dawn_color": "Цвет зари", "module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.", "setting.autobed.place": "Ставить", "setting.autobed.explode": "Взрывать", @@ -1758,6 +1767,8 @@ "setting.autobed.text_color": "Цвет текста", "setting.autobed.slide_delay": "Задержка слайда", "setting.autobed.fade_time": "Время затухания", + "module.shulkerpreview.description": "Показывает содержимое шалкера при наведении в инвентаре.", + "setting.shulkerpreview.scale": "Масштаб", "module.rubberhand.description": "Позволяет атаковать и выполнять блочные действия модулей во время активного использования предмета.", "setting.rubberhand.actions": "Действия", "setting.rubberhand.actions.option.attack_while_shield": "Бить в щите", @@ -3003,5 +3014,44 @@ "setting.chinahat.brim_thickness": "Толщина полей", "setting.chinahat.head_top_offset": "Высота на голове", "setting.chinahat.head_forward_offset": "Смещение вперёд", - "setting.chinahat.head_right_offset": "Смещение вправо" + "setting.chinahat.head_right_offset": "Смещение вправо", + "module.animations.description": "Плавные анимации интерфейса.", + "setting.animations.hotbar_raise": "Подъём хотбара", + "setting.animations.inventory": "Открытие инвентаря", + "module.nofrienddamage.description": "Блокирует удары по игрокам, добавленным в друзья.", + "module.lockslot.description": "Запрещает выбрасывать предметы из заблокированных слотов хотбара (защита от случайного Q).", + "setting.lockslot.slots": "Заблокированные слоты", + "setting.lockslot.only_players_nearby": "Только при игроках рядом", + "setting.lockslot.slots.option.slot1": "Слот 1", + "setting.lockslot.slots.option.slot2": "Слот 2", + "setting.lockslot.slots.option.slot3": "Слот 3", + "setting.lockslot.slots.option.slot4": "Слот 4", + "setting.lockslot.slots.option.slot5": "Слот 5", + "setting.lockslot.slots.option.slot6": "Слот 6", + "setting.lockslot.slots.option.slot7": "Слот 7", + "setting.lockslot.slots.option.slot8": "Слот 8", + "setting.lockslot.slots.option.slot9": "Слот 9", + "module.noserverpack.description": "Блокирует принудительную загрузку серверного ресурспака: автоматически отклоняет запросы и может убрать уже применённые паки.", + "setting.noserverpack.action": "Действие", + "setting.noserverpack.action.option.Decline": "Отклонять", + "setting.noserverpack.action.option.Remove": "Убирать", + "setting.noserverpack.action.option.Ask": "Спрашивать", + "module.tapemouse.description": "Автоматически нажимает выбранную кнопку мыши с заданным интервалом, например для AFK-ферм.", + "setting.tapemouse.button": "Кнопка", + "setting.tapemouse.button.option.Attack": "Атака", + "setting.tapemouse.button.option.Use": "Использование", + "setting.tapemouse.interval": "Интервал", + "setting.tapemouse.only_hold": "Только при зажатии", + "module.autorespawn.description": "Автоматически возрождает после смерти по истечении заданной задержки.", + "setting.autorespawn.delay": "Задержка", + "module.deathcoords.description": "Отправляет в чат координаты вашей последней смерти.", + "setting.deathcoords.show_dimension": "Показывать измерение", + "setting.holdmyitems.sway_intensity": "Интенсивность покачивания", + "setting.holdmyitems.ease_strength": "Плавность equip", + "setting.holdmyitems.offhand": "Применять к оффхенду", + "setting.holdmyitems.walk_bob": "Покачивание при ходьбе", + "setting.holdmyitems.bob_strength": "Сила покачивания", + "setting.holdmyitems.use_drift": "Дрейф при использовании", + "setting.holdmyitems.drift_strength": "Сила дрейфа", + "setting.holdmyitems.join_appear": "Плавное появление при входе" } diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag b/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag new file mode 100644 index 0000000..b0b85f8 --- /dev/null +++ b/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag @@ -0,0 +1,156 @@ +#version 330 core + +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +out vec4 color; + +uniform sampler2D u_Src; +uniform sampler2D u_Mask; + +layout (std140) uniform HandNoise { + vec4 u_Color; // rgb, alpha + vec4 u_Glow; // rgb, alpha + vec4 u_Params0; // edgeWidth, quality, octaves, time + vec4 u_Params1; // resX, resY, scale, speed + vec4 u_Params2; // contrast, breathe, glowStrength, density +}; + +in vec2 v_TexCoord; + +float handNoiseHash21(vec2 p) { + p = fract(p * vec2(127.31, 311.7)); + p += dot(p, p + 41.31); + return fract(p.x * p.y); +} + +float handNoiseValue(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + float a = handNoiseHash21(i); + float b = handNoiseHash21(i + vec2(1.0, 0.0)); + float c = handNoiseHash21(i + vec2(0.0, 1.0)); + float d = handNoiseHash21(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float handNoiseFbm(vec2 p, int octaves) { + float v = 0.0; + float a = 0.55; + mat2 r = mat2(0.80, 0.60, -0.60, 0.80); + int o = clamp(octaves, 1, 3); + for (int i = 0; i < 3; ++i) { + if (i >= o) break; + v += a * handNoiseValue(p); + p = r * p * 2.07 + vec2(23.3, 41.7); + a *= 0.5; + } + return v; +} + +float handMaskAt(vec2 uv) { + vec4 mask = texture(u_Mask, uv); + return max(mask.a, max(mask.r, max(mask.g, mask.b))); +} + +float handNoiseEdgeDistance(vec2 oneTexel, int quality, float edgeWidth, out float hitAmount) { + float best = 1.0e5; + float hit = 0.0; + int rings = clamp(quality, 1, 4); + int dirs = quality <= 1 ? 8 : 12; + float radiusPx = max(edgeWidth, 1.0); + + for (int r = 1; r <= 4; ++r) { + if (r > rings) continue; + float rr = float(r) / float(rings); + float distPx = radiusPx * rr; + for (int i = 0; i < 12; ++i) { + if (i >= dirs) continue; + float angle = (float(i) + float(r) * 0.37) * 6.28318530718 / float(dirs); + vec2 dir = vec2(cos(angle), sin(angle)); + float a = handMaskAt(v_TexCoord + dir * oneTexel * distPx); + if (a > 0.02) { + best = min(best, rr); + hit = max(hit, a); + } + } + } + + hitAmount = hit; + return best; +} + +float handNoiseField(vec2 fragCoord, vec2 resolution, float time, int octaves) { + float scale = max(u_Params1.z, 0.05); + float speed = max(u_Params1.w, 0.0); + float contrast = max(u_Params2.x, 0.05); + float breathe = clamp(u_Params2.y, 0.0, 1.0); + float density = max(u_Params2.w, 0.0); + + vec2 uv = fragCoord / max(resolution, vec2(1.0)); + vec2 aspect = vec2(resolution.x / max(resolution.y, 1.0), 1.0); + vec2 p = (uv - 0.5) * aspect * scale; + + float t = time * speed; + vec2 warp = vec2( + handNoiseFbm(p * 1.3 + vec2(0.0, t * 0.32), octaves), + handNoiseFbm(p * 1.3 + vec2(5.2, -t * 0.26), octaves) + ) - 0.5; + float n = handNoiseFbm(p * 1.9 + warp * 1.35 + vec2(t * 0.21, -t * 0.14), octaves); + + float pulse = 0.5 + 0.5 * sin(t * 1.6 + warp.x * 2.4); + n = clamp(n * (1.0 + breathe * (pulse - 0.5) * 0.9), 0.0, 1.0); + return pow(clamp(n * (0.85 + density * 0.3), 0.0, 1.0), 1.0 / contrast); +} + +void main() { + vec4 base = texture(u_Src, v_TexCoord); + float mask = handMaskAt(v_TexCoord); + + vec2 resolution = max(u_Params1.xy, vec2(1.0)); + vec2 oneTexel = 1.0 / resolution; + float edgeWidth = max(u_Params0.x, 0.0); + int quality = int(u_Params0.y + 0.5); + int octaves = clamp(int(u_Params0.z + 0.5), 1, 3); + float time = u_Params0.w; + float glowStrength = max(u_Params2.z, 0.0); + + if (mask > 0.01) { + float n = handNoiseField(gl_FragCoord.xy, resolution, time, octaves); + + float nL = handMaskAt(v_TexCoord + vec2(-oneTexel.x, 0.0)); + float nR = handMaskAt(v_TexCoord + vec2(oneTexel.x, 0.0)); + float nU = handMaskAt(v_TexCoord + vec2(0.0, -oneTexel.y)); + float nD = handMaskAt(v_TexCoord + vec2(0.0, oneTexel.y)); + float innerEdge = clamp(1.0 - min(min(nL, nR), min(nU, nD)), 0.0, 1.0); + innerEdge = pow(innerEdge, 0.72); + + float alpha = clamp(u_Color.a * mask, 0.0, 1.0); + vec3 shade = u_Color.rgb * (0.55 + 0.75 * n); + shade = mix(shade, u_Glow.rgb, clamp((n - 0.68) * 1.4, 0.0, 0.35)); + vec3 outCol = mix(base.rgb, shade, alpha); + outCol += u_Glow.rgb * innerEdge * u_Glow.a * glowStrength * (0.18 + 0.30 * n); + color = vec4(clamp(outCol, 0.0, 1.0), 1.0); + return; + } + + float hitAmount = 0.0; + float d = handNoiseEdgeDistance(oneTexel, quality, edgeWidth, hitAmount); + if (hitAmount > 0.01) { + float n = handNoiseField(gl_FragCoord.xy, resolution, time, octaves); + float falloff = clamp(1.0 - d, 0.0, 1.0); + falloff = falloff * falloff * (3.0 - 2.0 * falloff); + float glowA = pow(falloff, 1.3) * u_Glow.a * glowStrength * (0.30 + 0.40 * n); + vec3 outCol = mix(base.rgb, u_Glow.rgb, glowA * 0.5); + outCol += u_Glow.rgb * glowA; + color = vec4(clamp(outCol, 0.0, 1.0), 1.0); + return; + } + + color = vec4(base.rgb, 1.0); +} diff --git a/combatant-client-26.2/src/main/resources/lovisual.mixins.json b/combatant-client-26.2/src/main/resources/lovisual.mixins.json index 3d22f0f..fc3af4b 100644 --- a/combatant-client-26.2/src/main/resources/lovisual.mixins.json +++ b/combatant-client-26.2/src/main/resources/lovisual.mixins.json @@ -90,11 +90,13 @@ "LightmapRenderStateExtractorMixin", "LivingEntityRendererMixin", "LivingEntityRenderStateMixin", + "LocalPlayerDropMixin", "LocalPlayerMixin", "LocatorBarMixin", "MinecraftMixin", "MouseMixin", "MultiPlayerGameModeMixin", + "MultiPlayerGameModeThrowMixin", "NarratorManagerMixin", "OptionsNarratorMixin", "ParticleManagerMixin", @@ -139,6 +141,7 @@ "accessors.InputAccessor", "accessors.ItemCooldownEntryAccessor", "accessors.ItemCooldownManagerAccessor", + "accessors.KeyMappingAccessor", "accessors.LevelExtractorAccessor", "accessors.LevelRendererAccessor", "accessors.LivingEntityRenderStateAccessor", diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java new file mode 100644 index 0000000..77ab7f4 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoServerPackTest.java @@ -0,0 +1,52 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class NoServerPackTest { + + @Test + void declinesInDeclineModeWhenEnabled() { + assertTrue(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.DECLINE.id)); + } + + @Test + void declinesInRemoveModeWhenEnabled() { + assertTrue(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.REMOVE.id)); + } + + @Test + void neverDeclinesInAskMode() { + assertFalse(NoServerPack.shouldDeclinePush(true, NoServerPack.Action.ASK.id)); + assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.ASK.id)); + } + + @Test + void disabledModuleNeverDeclines() { + assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.DECLINE.id)); + assertFalse(NoServerPack.shouldDeclinePush(false, NoServerPack.Action.REMOVE.id)); + } + + @Test + void unknownModeFallsBackToDecline() { + assertEquals(NoServerPack.Action.DECLINE, NoServerPack.Action.byId("bogus")); + assertEquals(NoServerPack.Action.DECLINE, NoServerPack.Action.byId(null)); + } + + @Test + void modeIdsResolveRoundTrip() { + for (NoServerPack.Action action : NoServerPack.Action.values()) { + assertEquals(action, NoServerPack.Action.byId(action.id)); + } + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java new file mode 100644 index 0000000..1afc3a0 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoRespawnTest.java @@ -0,0 +1,81 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class AutoRespawnTest { + + @Test + void deathAloneDoesNotTriggerImmediately() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, 500L)); + } + + @Test + void triggersOnceAfterDelay() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, 500L)); + assertFalse(gate.shouldRequestRespawn(true, 1200L, 500L)); + assertTrue(gate.shouldRequestRespawn(true, 1500L, 500L)); + } + + @Test + void requestIsSentOnlyOncePerDeath() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L)); + gate.onRequestSent(); + + // Player is still locally dead until the server responds. + assertFalse(gate.shouldRequestRespawn(true, 1100L, 0L)); + assertFalse(gate.shouldRequestRespawn(true, 5000L, 0L)); + } + + @Test + void comingBackToLifeRearmsTheGate() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L)); + gate.onRequestSent(); + assertFalse(gate.shouldRequestRespawn(true, 2000L, 0L)); + + assertFalse(gate.shouldRequestRespawn(false, 3000L, 0L)); + assertFalse(gate.shouldRequestRespawn(true, 3100L, 500L)); + assertTrue(gate.shouldRequestRespawn(true, 3600L, 500L)); + } + + @Test + void totemReviveWithoutRequestStartsFreshDeath() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, 2000L)); + assertFalse(gate.shouldRequestRespawn(false, 1200L, 2000L)); + + assertFalse(gate.shouldRequestRespawn(true, 5000L, 2000L)); + assertTrue(gate.shouldRequestRespawn(true, 7000L, 2000L)); + } + + @Test + void negativeDelayTreatedAsZero() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, -5L)); + assertTrue(gate.shouldRequestRespawn(true, 1001L, -5L)); + } + + @Test + void missingPlayerResetsState() { + AutoRespawn.RespawnGate gate = new AutoRespawn.RespawnGate(); + assertFalse(gate.shouldRequestRespawn(true, 1000L, 0L)); + gate.onRequestSent(); + + gate.reset(); + assertFalse(gate.shouldRequestRespawn(true, 2000L, 0L)); + assertTrue(gate.shouldRequestRespawn(true, 2001L, 0L)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java new file mode 100644 index 0000000..af321a7 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java @@ -0,0 +1,66 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class DeathCoordsTest { + + @Test + void detectsAliveToDeadTransition() { + DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector(); + assertFalse(detector.onTick(false)); + assertTrue(detector.onTick(true)); + assertFalse(detector.onTick(true)); + assertFalse(detector.onTick(true)); + } + + @Test + void secondDeathIsDetectedAgainAfterRevive() { + DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector(); + detector.onTick(true); + assertFalse(detector.onTick(false)); + assertTrue(detector.onTick(true)); + } + + @Test + void resetClearsDeathState() { + DeathCoords.DeathDetector detector = new DeathCoords.DeathDetector(); + detector.onTick(true); + detector.reset(); + assertTrue(detector.onTick(true)); + } + + @Test + void messageIncludesDimensionWhenEnabled() { + String message = DeathCoords.formatDeathMessage(12.25, 64.0, -789.5, "minecraft:overworld", true); + assertEquals("You died at 12.25, 64.00, -789.50 (minecraft:overworld)", message); + } + + @Test + void messageOmitsDimensionWhenDisabled() { + String message = DeathCoords.formatDeathMessage(0.5, 1.5, 2.5, "minecraft:the_nether", false); + assertEquals("You died at 0.50, 1.50, 2.50", message); + } + + @Test + void messageFallsBackWithoutDimensionName() { + assertEquals( + "You died at 1.00, 2.00, 3.00", + DeathCoords.formatDeathMessage(1, 2, 3, "", true) + ); + assertEquals( + "You died at 1.00, 2.00, 3.00", + DeathCoords.formatDeathMessage(1, 2, 3, null, true) + ); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java new file mode 100644 index 0000000..72da510 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/TapeMouseTest.java @@ -0,0 +1,65 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TapeMouseTest { + + @Test + void firstCallIsDueImmediately() { + TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer(); + assertTrue(pacer.isDue(1000L, 1000L)); + } + + @Test + void clicksAreSpacedByInterval() { + TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer(); + assertTrue(pacer.isDue(1000L, 500L)); + pacer.mark(1000L); + + assertFalse(pacer.isDue(1499L, 500L)); + assertTrue(pacer.isDue(1500L, 500L)); + assertTrue(pacer.isDue(4000L, 500L)); + } + + @Test + void changingIntervalIsRespected() { + TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer(); + pacer.mark(1000L); + + assertFalse(pacer.isDue(2499L, 2000L)); + assertTrue(pacer.isDue(3000L, 2000L)); + pacer.mark(3000L); + + assertTrue(pacer.isDue(3100L, 100L)); + } + + @Test + void resetAllowsImmediateClickAgain() { + TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer(); + pacer.mark(1000L); + assertFalse(pacer.isDue(1100L, 5000L)); + + pacer.reset(); + assertTrue(pacer.isDue(1100L, 5000L)); + } + + @Test + void nonPositiveIntervalStillPacesEveryCall() { + TapeMouse.ClickPacer pacer = new TapeMouse.ClickPacer(); + assertTrue(pacer.isDue(0L, 0L)); + pacer.mark(0L); + assertFalse(pacer.isDue(0L, 0L)); + assertTrue(pacer.isDue(1L, 0L)); + assertTrue(pacer.isDue(1L, -100L)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java new file mode 100644 index 0000000..4d0cf7e --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java @@ -0,0 +1,59 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Чистая математика AtmoDawnFog: окно времени суток и высотное затухание. + */ +final class AtmoDawnFogTest { + + private static final float EPS = 1e-4f; + + @Test + void dawnPeaksAtSunrise() { + assertEquals(1.0f, AtmoDawnFog.strengthFor(0.0f, "Dawn", 64.0f, 76.0f), EPS); + assertTrue(AtmoDawnFog.strengthFor(0.999f, "Dawn", 64.0f, 76.0f) > 0.98f); + } + + @Test + void dawnSilentAtNoonAndDusk() { + assertEquals(0.0f, AtmoDawnFog.strengthFor(0.25f, "Dawn", 64.0f, 76.0f), EPS); + assertEquals(0.0f, AtmoDawnFog.strengthFor(0.5f, "Dawn", 64.0f, 76.0f), EPS); + } + + @Test + void duskPeaksAtSunset() { + assertEquals(1.0f, AtmoDawnFog.strengthFor(0.5f, "Dusk", 64.0f, 76.0f), EPS); + assertEquals(0.0f, AtmoDawnFog.strengthFor(0.0f, "Dusk", 64.0f, 76.0f), EPS); + } + + @Test + void customModeIgnoresTime() { + assertEquals(1.0f, AtmoDawnFog.strengthFor(0.3f, "Custom", 64.0f, 76.0f), EPS); + assertEquals(1.0f, AtmoDawnFog.strengthFor(0.7f, "Custom", 64.0f, 76.0f), EPS); + } + + @Test + void altitudeFadesAboveScatterHeight() { + float below = AtmoDawnFog.strengthFor(0.0f, "Dawn", 76.0f, 76.0f); + float above = AtmoDawnFog.strengthFor(0.0f, "Dawn", 76.0f + 16.0f, 76.0f); + assertEquals(1.0f, below, EPS); + assertEquals(0.5f, above, EPS); + } + + @Test + void altitudeClampsAtZero() { + assertEquals(0.0f, AtmoDawnFog.strengthFor(0.0f, "Custom", 76.0f + 1000.0f, 76.0f), EPS); + assertTrue(AtmoDawnFog.strengthFor(0.0f, "Custom", 40.0f, 76.0f) <= 1.0f); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java new file mode 100644 index 0000000..d216742 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java @@ -0,0 +1,78 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import dev.loki.lovisual.render.engine.uniform.impl.PostFXUniforms; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Пресеты PostFX: Cinematic должен быть визуально заметным, + * Vanilla — нейтральным (это причина «модуль ничего не делал»). + */ +final class PostFxPresetTest { + + private static float[] preset(String name) { + float[] out = new float[PostFXUniforms.FLOAT_COUNT]; + PostFX.applyPresetTo(name, out); + return out; + } + + @Test + void vanillaPresetIsNeutral() { + float[] vanilla = preset("Vanilla"); + assertEquals(0.0f, vanilla[0], 1e-6); // exposure + assertEquals(1.0f, vanilla[1], 1e-6); // gamma + assertEquals(1.0f, vanilla[2], 1e-6); // contrast + assertEquals(1.0f, vanilla[3], 1e-6); // saturation + assertEquals(0.0f, vanilla[7], 1e-6); // LUT strength + assertEquals(0.0f, vanilla[15], 1e-6); // tone map off + } + + @Test + void cinematicPresetIsVisible() { + float[] cinematic = preset("Cinematic"); + assertNotEquals(1.0f, cinematic[2], 1e-4); // contrast изменён + assertTrue(cinematic[3] > 1.05f); // насыщенность выше + assertTrue(cinematic[7] > 0.2f); // тёплый LUT заметен + assertEquals(1.0f, cinematic[15], 1e-6); // filmic tone map + assertTrue(cinematic[19] > 0.0f); // холодные тени + assertTrue(cinematic[27] > 0.0f); // тёплые света + assertTrue(cinematic[8] > 0.1f); // виньетка заметна + } + + @Test + void cinematicDiffersFromVanillaOnGrade() { + float[] cinematic = preset("Cinematic"); + float[] vanilla = preset("Vanilla"); + assertNotEquals(vanilla[2], cinematic[2]); + assertNotEquals(vanilla[3], cinematic[3]); + assertNotEquals(vanilla[7], cinematic[7]); + } + + @Test + void unknownPresetFallsBackToSoft() { + float[] unknown = preset("Mystery"); + float[] soft = preset("Soft"); + for (int i = 0; i < 16; i++) { + assertEquals(soft[i], unknown[i], 1e-6); + } + } + + @Test + void nullPresetFallsBackToCinematic() { + float[] byNull = preset(null); + float[] cinematic = preset("Cinematic"); + for (int i = 0; i < 16; i++) { + assertEquals(cinematic[i], byNull[i], 1e-6); + } + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java new file mode 100644 index 0000000..4d0c80d --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java @@ -0,0 +1,76 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Математика ease-out-back для HoldMyItems. + */ +final class EaseCurvesTest { + + private static final float EPS = 1e-4f; + private static final float DEFAULT_C1 = 1.70158f; + + @Test + void easeOutBackStartsAtZeroAndEndsAtOne() { + assertEquals(0.0f, EaseCurves.easeOutBack(0.0f, DEFAULT_C1), EPS); + assertEquals(1.0f, EaseCurves.easeOutBack(1.0f, DEFAULT_C1), EPS); + } + + @Test + void easeOutBackOvershootsPastOne() { + float max = 0.0f; + for (int i = 0; i <= 100; i++) { + max = Math.max(max, EaseCurves.easeOutBack(i / 100.0f, DEFAULT_C1)); + } + assertTrue(max > 1.0f && max < 1.2f, "overshoot должен быть заметным, но умеренным: " + max); + } + + @Test + void shapeKeepsEndpoints() { + assertEquals(0.0f, EaseCurves.shapeEquipProgress(0.0f, DEFAULT_C1, 1.0f), EPS); + assertEquals(1.0f, EaseCurves.shapeEquipProgress(1.0f, DEFAULT_C1, 1.0f), EPS); + } + + @Test + void zeroIntensityFallsBackToLinear() { + for (int i = 0; i <= 10; i++) { + float x = i / 10.0f; + assertEquals(x, EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 0.0f), EPS); + } + } + + @Test + void fullIntensityMatchesCurve() { + float x = 0.7f; + assertEquals( + EaseCurves.easeOutBack(x, DEFAULT_C1), + EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 1.0f), + EPS + ); + } + + @Test + void intensityClamps() { + float x = 0.5f; + assertEquals( + EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 0.0f), + EaseCurves.shapeEquipProgress(x, DEFAULT_C1, -5.0f), + EPS + ); + assertEquals( + EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 1.0f), + EaseCurves.shapeEquipProgress(x, DEFAULT_C1, 5.0f), + EPS + ); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java new file mode 100644 index 0000000..73e5dc6 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java @@ -0,0 +1,72 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Совместная работа HoldMyItems (кривая) и ViewModel (занижение): + * конечная позиция определяется занижением, кривая добавляет перелёт. + */ +final class EquipProgressComposerTest { + + private static final float EPS = 1e-4f; + private static final float C1 = 1.6f; + + @Test + void bothDisabledIsIdentity() { + assertEquals(0.42f, EquipProgressComposer.compose(0.42f, false, 0, 0, false, 0), EPS); + } + + @Test + void loweringAloneScalesEndpoints() { + assertEquals(0.0f, EquipProgressComposer.compose(0.0f, false, 0, 0, true, 0.5f), EPS); + assertEquals(0.5f, EquipProgressComposer.compose(1.0f, false, 0, 0, true, 0.5f), EPS); + } + + @Test + void curveAloneKeepsEndpoints() { + assertEquals(0.0f, EquipProgressComposer.compose(0.0f, true, C1, 1.0f, false, 0), EPS); + assertEquals(1.0f, EquipProgressComposer.compose(1.0f, true, C1, 1.0f, false, 0), EPS); + } + + @Test + void togetherRestingPointIsLowered() { + //equip завершён (progress=1): финальная позиция = занижение, а не 1 + float rest = EquipProgressComposer.compose(1.0f, true, C1, 1.0f, true, 0.5f); + assertEquals(0.5f, rest, EPS); + } + + @Test + void togetherOvershootExceedsRestingPoint() { + float max = 0.0f; + for (int i = 0; i <= 100; i++) { + max = Math.max(max, EquipProgressComposer.compose(i / 100.0f, true, C1, 1.0f, true, 0.5f)); + } + assertTrue(max > 0.5f, "перелёт кривой должен поднимать предмет выше финальной позиции: " + max); + assertTrue(max < 0.5f * 1.2f, "перелёт умеренный: " + max); + } + + @Test + void curveDisabledButLoweringOn() { + assertEquals(0.25f, EquipProgressComposer.compose(0.5f, false, 0, 0, true, 0.5f), EPS); + } + + @Test + void loweringDisabledButCurveOn() { + float x = 0.7f; + assertEquals( + EaseCurves.shapeEquipProgress(x, C1, 1.0f), + EquipProgressComposer.compose(x, true, C1, 1.0f, false, 0), + EPS + ); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java new file mode 100644 index 0000000..27d089b --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java @@ -0,0 +1,162 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Pure-logic tests for the single-update-point animator. No Minecraft types involved. + */ +final class SmoothValueTest { + + private static final float EPS = 1.0e-4f; + + @Test + void reachesTargetWhenMovingUp() { + SmoothValue value = new SmoothValue(1.5f); + value.setTarget(true); + + for (int i = 0; i < 200; i++) { + value.update(0.5f); + } + + assertEquals(1.0f, value.value(), EPS); + assertTrue(value.isSettled()); + } + + @Test + void reachesTargetWhenMovingDown() { + SmoothValue value = new SmoothValue(1.5f, 1.0f); + value.setInstant(true); + value.setTarget(false); + + for (int i = 0; i < 200; i++) { + value.update(0.5f); + } + + assertEquals(0.0f, value.value(), EPS); + assertTrue(value.isSettled()); + } + + @Test + void movesUpMonotonically() { + SmoothValue value = new SmoothValue(0.8f); + value.setTarget(true); + + float previous = value.value(); + for (int i = 0; i < 100; i++) { + value.update(1.0f / 3.0f); + assertTrue(value.value() >= previous, "value must never fall while rising"); + assertTrue(value.value() <= 1.0f, "value must stay within 0..1"); + previous = value.value(); + } + assertEquals(1.0f, value.value(), EPS); + } + + @Test + void movesDownMonotonically() { + SmoothValue value = new SmoothValue(0.8f); + value.setInstant(true); + value.setTarget(false); + + float previous = value.value(); + for (int i = 0; i < 100; i++) { + value.update(1.0f / 3.0f); + assertTrue(value.value() <= previous, "value must never rise while falling"); + assertTrue(value.value() >= 0.0f, "value must stay within 0..1"); + previous = value.value(); + } + assertEquals(0.0f, value.value(), EPS); + } + + @Test + void setInstantTeleportsValueAndTarget() { + SmoothValue value = new SmoothValue(1.0f); + assertEquals(0.0f, value.value(), EPS); + + value.setInstant(true); + assertEquals(1.0f, value.value(), EPS); + assertEquals(1.0f, value.target(), EPS); + assertTrue(value.isSettled()); + + value.setInstant(false); + assertEquals(0.0f, value.value(), EPS); + assertEquals(0.0f, value.target(), EPS); + assertTrue(value.isSettled()); + } + + @Test + void setTargetDoesNotMoveValue() { + SmoothValue value = new SmoothValue(2.0f); + value.setTarget(true); + assertEquals(0.0f, value.value(), EPS, "setTarget must not advance the value"); + assertEquals(1.0f, value.target(), EPS); + } + + @Test + void frameRateIndependent() { + // One long step must land on the same point as many small steps of equal total time. + float totalTicks = 2.0f; + float speed = 1.1f; + + SmoothValue coarse = new SmoothValue(speed); + coarse.setTarget(true); + coarse.update(totalTicks); + + SmoothValue fine = new SmoothValue(speed); + fine.setTarget(true); + int steps = 240; + for (int i = 0; i < steps; i++) { + fine.update(totalTicks / steps); + } + + assertEquals(coarse.value(), fine.value(), 5.0e-3f); + assertTrue(coarse.value() > 0.5f && coarse.value() < 1.0f); + } + + @Test + void zeroAndBrokenDeltasAreIgnored() { + SmoothValue value = new SmoothValue(1.0f); + value.setTarget(true); + + value.update(0.0f); + value.update(-1.0f); + value.update(Float.NaN); + assertEquals(0.0f, value.value(), EPS); + assertFalse(value.isSettled()); + + value.update(1.0f); + assertTrue(value.value() > 0.0f); + } + + @Test + void targetFlipsMidFlightReverseDirection() { + SmoothValue value = new SmoothValue(1.0f); + value.setTarget(true); + value.update(1.0f); + assertTrue(value.value() > 0.0f); + + value.setTarget(false); + float peak = value.value(); + value.update(1.0f); + assertTrue(value.value() < peak, "falling must start immediately after the flip"); + } + + @Test + void rejectsBrokenSpeed() { + assertThrows(IllegalArgumentException.class, () -> new SmoothValue(0.0f)); + assertThrows(IllegalArgumentException.class, () -> new SmoothValue(-1.0f)); + assertThrows(IllegalArgumentException.class, () -> new SmoothValue(Float.NaN)); + assertThrows(IllegalArgumentException.class, () -> new SmoothValue(Float.POSITIVE_INFINITY)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java new file mode 100644 index 0000000..8fbb253 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java @@ -0,0 +1,97 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Фаза и амплитуда покачивания при ходьбе. */ +final class BobMathTest { + + @Test + void amplitudeIsZeroWhenIdle() { + assertEquals(0.0f, BobMath.amplitude(0.0f), 1.0e-6f); + } + + @Test + void amplitudeGrowsWithSpeed() { + float slow = BobMath.amplitude(0.25f); + float fast = BobMath.amplitude(0.9f); + assertTrue(slow > 0.0f && slow < fast, "амплитуда должна расти со скоростью"); + assertTrue(slow < 1.0f && fast <= 1.0f); + } + + @Test + void amplitudeClampedToOne() { + assertEquals(1.0f, BobMath.amplitude(2.5f), 1.0e-6f); + } + + @Test + void brokenSpeedGivesZeroAmplitude() { + assertEquals(0.0f, BobMath.amplitude(Float.NaN), 1.0e-6f); + assertEquals(0.0f, BobMath.amplitude(Float.NEGATIVE_INFINITY), 1.0e-6f); + assertEquals(0.0f, BobMath.amplitude(-0.5f), 1.0e-6f); + } + + @Test + void lateralStartsAtZeroAndIsBounded() { + assertEquals(0.0f, BobMath.lateral(0.0f), 1.0e-6f); + for (float phase = 0.0f; phase < 40.0f; phase += 0.125f) { + float value = BobMath.lateral(phase); + assertTrue(value >= -1.0f && value <= 1.0f, "латераль вне диапазона в фазе " + phase); + } + } + + @Test + void lateralIsAntisymmetric() { + for (float phase = -3.0f; phase <= 3.0f; phase += 0.25f) { + assertEquals(-BobMath.lateral(phase), BobMath.lateral(-phase), 1.0e-5f); + } + } + + @Test + void lateralRepeatsAfterFullCycle() { + float cycle = (float) (2.0 * Math.PI / BobMath.STEP_RATE); + assertEquals(BobMath.lateral(0.3f), BobMath.lateral(0.3f + cycle), 1.0e-4f); + } + + @Test + void lateralReachesPeakAtExpectedPhase() { + assertEquals(1.0f, BobMath.lateral(BobMath.lateralPeakPhase()), 1.0e-4f); + } + + @Test + void verticalIsNeverPositive() { + for (float phase = 0.0f; phase < 40.0f; phase += 0.125f) { + assertTrue(BobMath.vertical(phase) <= 0.0f, "вертикаль должна тянуть вниз"); + assertTrue(BobMath.vertical(phase) >= -1.0f); + } + } + + @Test + void verticalDipsTwicePerCycle() { + float cycle = (float) (2.0 * Math.PI / BobMath.STEP_RATE); + int dips = 0; + float previous = 0.0f; + for (float t = 0.0f; t <= cycle; t += 0.01f) { + float value = BobMath.vertical(t); + if (previous > -0.999f && value <= -0.999f) { + dips++; + } + previous = value; + } + assertEquals(2, dips, "за цикл должно быть два проседания (два шага)"); + } + + @Test + void stepRateMatchesVanillaLimbFrequency() { + assertEquals(0.6662f, BobMath.STEP_RATE, 1.0e-6f); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java new file mode 100644 index 0000000..0fe8523 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandAnimStateTest.java @@ -0,0 +1,149 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Покадровое состояние: одна правка цели за кадр, реакции на смену мира/игрока. */ +final class HandAnimStateTest { + + private static final Object PLAYER_A = new Object(); + private static final Object PLAYER_B = new Object(); + private static final Object LEVEL_1 = new Object(); + private static final Object LEVEL_2 = new Object(); + + private static final float DT = 0.5f; + + @Test + void firstFrameDoesNotProduceSwayImpulse() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 30.0f, -15.0f, false, false, DT); + assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f); + state.beginFrame(PLAYER_A, LEVEL_1, 30.0f, -15.0f, false, false, DT); + assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f); + } + + @Test + void swayReactsToMouseMotion() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + for (int i = 0; i < 8; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 6.0f * (i + 1), 0.0f, false, false, DT); + } + assertTrue(state.sway().yawLag() > 0.0f, "вращение камеры должно давать отставание"); + } + + @Test + void yawWrapMatchesEquivalentTurn() { + HandAnimState acrossBoundary = new HandAnimState(); + acrossBoundary.beginFrame(PLAYER_A, LEVEL_1, 175.0f, 0.0f, false, false, DT); + acrossBoundary.beginFrame(PLAYER_A, LEVEL_1, -175.0f, 0.0f, false, false, DT); + + HandAnimState plain = new HandAnimState(); + plain.beginFrame(PLAYER_A, LEVEL_1, -5.0f, 0.0f, false, false, DT); + plain.beginFrame(PLAYER_A, LEVEL_1, 5.0f, 0.0f, false, false, DT); + + assertEquals(plain.sway().yawLag(), acrossBoundary.sway().yawLag(), 1.0e-4f, + "переход через границу мира равен обычному повороту на те же 10 градусов"); + } + + @Test + void teleportResetsSwayInsteadOfImpulse() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + for (int i = 0; i < 8; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 6.0f * (i + 1), 0.0f, false, false, DT); + } + assertTrue(state.sway().yawLag() > 0.5f); + state.beginFrame(PLAYER_A, LEVEL_1, 180.0f, 0.0f, false, false, DT); + assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f); + } + + @Test + void driftIsPerHand() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + for (int i = 0; i < 20; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + } + assertTrue(state.drift(true) > 0.9f, "дрейф в главной руке должен подняться к единице"); + assertEquals(0.0f, state.drift(false), 1.0e-6f, "вторая рука не должна дёргаться"); + } + + @Test + void driftSettlesBackWhenUsageStops() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + for (int i = 0; i < 30; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + } + for (int i = 0; i < 120; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + } + assertEquals(0.0f, state.drift(true), 1.0e-3f, "после окончания использования дрейф должен уйти в ноль"); + } + + @Test + void joinProgressRisesThenStays() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + float previous = 0.0f; + for (int i = 0; i < 200; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + float value = state.joinProgress(); + assertTrue(value >= previous - 1.0e-6f, "прогресс появления не должен откатываться"); + previous = value; + } + assertEquals(1.0f, state.joinProgress(), 1.0e-3f); + } + + @Test + void playerOrLevelChangeRestartsJoin() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + for (int i = 0; i < 200; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + } + assertEquals(1.0f, state.joinProgress(), 1.0e-3f); + + state.beginFrame(PLAYER_B, LEVEL_1, 0.0f, 0.0f, false, false, DT); + assertTrue(state.joinProgress() < 0.1f, "смена игрока (респавн) перезапускает появление"); + state.beginFrame(PLAYER_B, LEVEL_2, 0.0f, 0.0f, false, false, DT); + assertTrue(state.joinProgress() < 0.1f, "смена мира тоже перезапускает появление"); + } + + @Test + void nullPlayerResetsEverything() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + for (int i = 0; i < 30; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + } + state.beginFrame(null, null, 0.0f, 0.0f, false, false, DT); + assertEquals(0.0f, state.drift(true), 1.0e-6f); + assertEquals(0.0f, state.joinProgress(), 1.0e-6f); + assertTrue(state.sway().isSettled()); + } + + @Test + void resetReturnsToFreshState() { + HandAnimState state = new HandAnimState(); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + for (int i = 0; i < 30; i++) { + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, true, false, DT); + } + state.reset(); + assertEquals(0.0f, state.drift(true), 1.0e-6f); + assertEquals(0.0f, state.joinProgress(), 1.0e-6f); + state.beginFrame(PLAYER_A, LEVEL_1, 0.0f, 0.0f, false, false, DT); + assertEquals(0.0f, state.sway().yawLag(), 1.0e-6f); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java new file mode 100644 index 0000000..fde3417 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java @@ -0,0 +1,130 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Сборка позы руки из эффектов. */ +final class HandPoseMathTest { + + private static final float EPS = 1.0e-6f; + + @Test + void swayZeroStrengthIsZero() { + assertEquals(HandPoseMath.ZERO, HandPoseMath.sway(6.0f, 4.0f, 0.0f)); + } + + @Test + void swayStrengthScalesOutput() { + HandPoseMath.Pose unit = HandPoseMath.sway(3.0f, 2.0f, 1.0f); + HandPoseMath.Pose doubled = HandPoseMath.sway(3.0f, 2.0f, 2.0f); + assertEquals(unit.dx() * 2.0f, doubled.dx(), EPS); + assertEquals(unit.ry() * 2.0f, doubled.ry(), EPS); + } + + @Test + void swaySignsFollowCameraTurn() { + // Поворот вправо (yaw+): предмет отстаёт влево на экране (+X) и доворачивается назад (ry-). + HandPoseMath.Pose yaw = HandPoseMath.sway(2.0f, 0.0f, 1.0f); + assertTrue(yaw.dx() > 0.0f); + assertTrue(yaw.ry() < 0.0f); + assertTrue(yaw.rz() > 0.0f); + // Взгляд вниз (pitch+): предмет отстаёт вверх (+Y) и задирается (rx-). + HandPoseMath.Pose pitch = HandPoseMath.sway(0.0f, 2.0f, 1.0f); + assertTrue(pitch.dy() > 0.0f); + assertTrue(pitch.rx() < 0.0f); + assertEquals(0.0f, pitch.dx(), EPS); + } + + @Test + void swayMirrorsForOppositeLag() { + HandPoseMath.Pose positive = HandPoseMath.sway(2.0f, 1.0f, 1.0f); + HandPoseMath.Pose negative = HandPoseMath.sway(-2.0f, -1.0f, 1.0f); + assertEquals(-positive.dx(), negative.dx(), EPS); + assertEquals(-positive.dy(), negative.dy(), EPS); + assertEquals(-positive.rx(), negative.rx(), EPS); + assertEquals(-positive.ry(), negative.ry(), EPS); + assertEquals(-positive.rz(), negative.rz(), EPS); + } + + @Test + void bobZeroAmplitudeIsZero() { + assertEquals(HandPoseMath.ZERO, HandPoseMath.bob(3.0f, 0.0f, 1.0f)); + assertEquals(HandPoseMath.ZERO, HandPoseMath.bob(3.0f, 0.8f, 0.0f)); + } + + @Test + void bobIsBoundedAndScales() { + for (float phase = 0.0f; phase < 20.0f; phase += 0.1f) { + HandPoseMath.Pose pose = HandPoseMath.bob(phase, 1.0f, 1.0f); + assertTrue(Math.abs(pose.dx()) <= 0.045f + EPS); + assertTrue(Math.abs(pose.dy()) <= 0.035f + EPS); + assertTrue(Math.abs(pose.rz()) <= 2.5f + EPS); + assertTrue(pose.dy() <= 0.0f, "вертикаль боба не должна поднимать предмет"); + } + HandPoseMath.Pose half = HandPoseMath.bob(0.7f, 0.5f, 1.0f); + HandPoseMath.Pose full = HandPoseMath.bob(0.7f, 1.0f, 1.0f); + assertEquals(full.dx() * 0.5f, half.dx(), EPS); + } + + @Test + void driftPullsEachArmTowardCenter() { + HandPoseMath.Pose right = HandPoseMath.drift(1.0f, 1.0f, true); + assertTrue(right.dx() > 0.0f, "правая рука тянется влево на экране (+X)"); + HandPoseMath.Pose left = HandPoseMath.drift(1.0f, 1.0f, false); + assertTrue(left.dx() < 0.0f, "левая рука тянется вправо на экране (-X)"); + assertEquals(-right.dx(), left.dx(), EPS); + assertEquals(-right.ry(), left.ry(), EPS); + assertEquals(-right.rz(), left.rz(), EPS); + } + + @Test + void driftZeroProgressAndStrengthGiveZero() { + assertEquals(HandPoseMath.ZERO, HandPoseMath.drift(0.0f, 1.0f, true)); + assertEquals(HandPoseMath.ZERO, HandPoseMath.drift(1.0f, 0.0f, false)); + } + + @Test + void joinDropAtFullProgressIsZero() { + assertEquals(HandPoseMath.ZERO, HandPoseMath.joinDrop(1.0f)); + } + + @Test + void joinDropEasesOutMonotonically() { + float previousDrop = Float.POSITIVE_INFINITY; + for (float p = 0.0f; p <= 1.0f; p += 0.05f) { + float drop = -HandPoseMath.joinDrop(p).dy(); + assertTrue(drop >= 0.0f && drop <= 0.85f + EPS); + assertTrue(drop <= previousDrop, "подъём должен замедляться к концу"); + previousDrop = drop; + } + assertEquals(0.85f, -HandPoseMath.joinDrop(0.0f).dy(), EPS); + } + + @Test + void addCombinesPosesAndZeroIsIdentity() { + HandPoseMath.Pose a = HandPoseMath.sway(2.0f, 1.0f, 1.0f); + HandPoseMath.Pose b = HandPoseMath.bob(0.5f, 1.0f, 1.0f); + HandPoseMath.Pose sum = HandPoseMath.add(a, b); + assertEquals(a.dx() + b.dx(), sum.dx(), EPS); + assertEquals(a.rx() + b.rx(), sum.rx(), EPS); + assertEquals(a, HandPoseMath.add(a, HandPoseMath.ZERO)); + assertEquals(a, HandPoseMath.add(HandPoseMath.ZERO, a)); + } + + @Test + void significanceThresholdWorks() { + assertFalse(HandPoseMath.ZERO.isSignificant(1.0e-4f)); + assertTrue(HandPoseMath.sway(1.0f, 0.0f, 1.0f).isSignificant(1.0e-4f)); + assertFalse(HandPoseMath.drift(1.0f, 1.0f, true).isSignificant(20.0f)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java new file mode 100644 index 0000000..1a75f51 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/SwayStateTest.java @@ -0,0 +1,121 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.animation.hand; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Математика отставания предмета от поворота камеры. */ +final class SwayStateTest { + + private static final float DT = 0.5f; + + @Test + void withoutInputStaysZero() { + SwayState state = new SwayState(); + for (int i = 0; i < 60; i++) { + state.update(0.0f, 0.0f, DT); + } + assertEquals(0.0f, state.yawLag(), 1.0e-6f); + assertEquals(0.0f, state.pitchLag(), 1.0e-6f); + assertTrue(state.isSettled()); + } + + @Test + void yawImpulseProducesPositiveLag() { + SwayState state = new SwayState(); + state.update(4.0f, 0.0f, DT); + assertTrue(state.yawLag() > 0.0f, "движение мыши вправо должно давать положительное отставание"); + assertEquals(0.0f, state.pitchLag(), 1.0e-6f); + } + + @Test + void lagDecaysBackToZeroWhenMouseStill() { + SwayState state = new SwayState(); + state.update(4.0f, -2.0f, DT); + for (int i = 0; i < 400; i++) { + state.update(0.0f, 0.0f, DT); + } + assertTrue(state.isSettled(), "без ввода отставание должно полностью затухнуть"); + } + + @Test + void lagIsClamped() { + SwayState state = new SwayState(); + for (int i = 0; i < 40; i++) { + state.update(180.0f, -120.0f, DT); + } + assertTrue(state.yawLag() <= SwayState.MAX_YAW_LAG_DEG + 1.0e-4f); + assertTrue(state.pitchLag() >= -SwayState.MAX_PITCH_LAG_DEG - 1.0e-4f); + } + + @Test + void mirroredInputsGiveMirroredLag() { + SwayState left = new SwayState(); + SwayState right = new SwayState(); + for (int i = 0; i < 10; i++) { + left.update(-3.0f, 1.5f, DT); + right.update(3.0f, -1.5f, DT); + } + assertEquals(left.yawLag(), -right.yawLag(), 1.0e-5f); + assertEquals(left.pitchLag(), -right.pitchLag(), 1.0e-5f); + } + + @Test + void oppositeInputPullsBackThroughZero() { + SwayState state = new SwayState(); + for (int i = 0; i < 10; i++) { + state.update(3.0f, 0.0f, DT); + } + float peak = state.yawLag(); + for (int i = 0; i < 10; i++) { + state.update(-3.0f, 0.0f, DT); + } + assertTrue(state.yawLag() < peak, "обратное движение должно гасить отставание"); + } + + @Test + void nonPositiveDeltaIgnored() { + SwayState state = new SwayState(); + state.update(2.0f, 1.0f, DT); + float yaw = state.yawLag(); + state.update(2.0f, 1.0f, 0.0f); + state.update(2.0f, 1.0f, -1.0f); + assertEquals(yaw, state.yawLag(), 1.0e-6f); + assertFalse(state.isSettled()); + } + + @Test + void brokenInputsAreSafe() { + SwayState state = new SwayState(); + state.update(Float.NaN, Float.POSITIVE_INFINITY, DT); + state.update(1.0f, 1.0f, Float.NaN); + assertEquals(0.0f, state.yawLag(), 1.0e-6f); + assertEquals(0.0f, state.pitchLag(), 1.0e-6f); + } + + @Test + void wrapDegreesKeepsResultInHalfCircle() { + assertEquals(-20.0f, SwayState.wrapDegrees(340.0f), 1.0e-4f); + assertEquals(20.0f, SwayState.wrapDegrees(-340.0f), 1.0e-4f); + assertEquals(-180.0f, SwayState.wrapDegrees(180.0f), 1.0e-4f); + assertEquals(0.0f, SwayState.wrapDegrees(360.0f), 1.0e-4f); + assertEquals(5.0f, SwayState.wrapDegrees(365.0f), 1.0e-4f); + } + + @Test + void resetClearsEverything() { + SwayState state = new SwayState(); + state.update(4.0f, 2.0f, DT); + state.reset(); + assertTrue(state.isSettled()); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java new file mode 100644 index 0000000..1cc493a --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java @@ -0,0 +1,86 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.protection; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class NotifyThrottleTest { + + @Test + void firstCallIsAllowed() { + NotifyThrottle throttle = new NotifyThrottle(1000L); + assertTrue(throttle.tryAcquire(5_000L)); + } + + @Test + void secondCallInsideIntervalIsSuppressed() { + NotifyThrottle throttle = new NotifyThrottle(1_000L); + assertTrue(throttle.tryAcquire(5_000L)); + assertFalse(throttle.tryAcquire(5_999L)); + } + + @Test + void callExactlyAtIntervalBoundaryIsAllowed() { + NotifyThrottle throttle = new NotifyThrottle(1_000L); + assertTrue(throttle.tryAcquire(5_000L)); + assertTrue(throttle.tryAcquire(6_000L)); + } + + @Test + void callAfterIntervalIsAllowed() { + NotifyThrottle throttle = new NotifyThrottle(1_500L); + assertTrue(throttle.tryAcquire(10_000L)); + assertFalse(throttle.tryAcquire(11_499L)); + assertTrue(throttle.tryAcquire(11_500L)); + assertTrue(throttle.tryAcquire(13_001L)); + } + + @Test + void remainingMsIsMonotonicAndClampedToZero() { + NotifyThrottle throttle = new NotifyThrottle(1_000L); + assertEquals(0L, throttle.remainingMs(1_000L), "never fired"); + throttle.tryAcquire(2_000L); + assertEquals(1_000L, throttle.remainingMs(2_000L)); + assertEquals(500L, throttle.remainingMs(2_500L)); + assertEquals(0L, throttle.remainingMs(3_000L)); + assertEquals(0L, throttle.remainingMs(10_000L)); + } + + @Test + void backwardsClockDoesNotLockOutCaller() { + NotifyThrottle throttle = new NotifyThrottle(1_000L); + throttle.tryAcquire(5_000L); + assertTrue(throttle.tryAcquire(1_000L), "moving time back should allow an emission"); + } + + @Test + void nonPositiveIntervalAlwaysAllows() { + NotifyThrottle zero = new NotifyThrottle(0L); + assertTrue(zero.tryAcquire(100L)); + assertTrue(zero.tryAcquire(100L)); + assertTrue(zero.tryAcquire(100L)); + + NotifyThrottle negative = new NotifyThrottle(-5L); + assertTrue(negative.tryAcquire(1L)); + assertTrue(negative.tryAcquire(2L)); + } + + @Test + void resetRestoresInitialState() { + NotifyThrottle throttle = new NotifyThrottle(60_000L); + assertTrue(throttle.tryAcquire(1_000L)); + assertFalse(throttle.tryAcquire(2_000L)); + throttle.reset(); + assertEquals(0L, throttle.remainingMs(2_000L)); + assertTrue(throttle.tryAcquire(2_000L)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java new file mode 100644 index 0000000..2959caa --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java @@ -0,0 +1,101 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.protection; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class SlotLockPolicyTest { + + private static Map states(int... lockedZeroBasedSlots) { + Map out = SlotLockPolicy.defaultSlotStates(); + for (int slot : lockedZeroBasedSlots) { + out.put(SlotLockPolicy.slotKey(slot), true); + } + return out; + } + + @Test + void defaultsUnlockAllNineSlots() { + Map defaults = SlotLockPolicy.defaultSlotStates(); + assertEquals(9, defaults.size()); + for (int slot = 0; slot < 9; slot++) { + assertFalse(SlotLockPolicy.isSlotLocked(defaults, slot), "slot " + slot + " must be unlocked"); + } + } + + @Test + void slotKeyIsOneBased() { + assertEquals("slot1", SlotLockPolicy.slotKey(0)); + assertEquals("slot5", SlotLockPolicy.slotKey(4)); + assertEquals("slot9", SlotLockPolicy.slotKey(8)); + } + + @Test + void lockedSlotIsBlockedWhenNearbyCheckDisabled() { + assertTrue(SlotLockPolicy.isDropBlocked(states(4), 4, false, false)); + assertTrue(SlotLockPolicy.isDropBlocked(states(4), 4, true, false)); + } + + @Test + void lockedSlotRespectsOnlyPlayersNearby() { + assertTrue(SlotLockPolicy.isDropBlocked(states(0), 0, true, true)); + assertFalse(SlotLockPolicy.isDropBlocked(states(0), 0, false, true)); + } + + @Test + void unlockedSlotIsNeverBlocked() { + assertFalse(SlotLockPolicy.isDropBlocked(states(1, 2), 0, true, false)); + assertFalse(SlotLockPolicy.isDropBlocked(states(1, 2), 0, false, false)); + } + + @Test + void hotbarEdgesAreCovered() { + Map edgeStates = states(0, 8); + assertTrue(SlotLockPolicy.isDropBlocked(edgeStates, 0, false, false)); + assertTrue(SlotLockPolicy.isDropBlocked(edgeStates, 8, false, false)); + assertFalse(SlotLockPolicy.isDropBlocked(edgeStates, 4, false, false)); + } + + @Test + void outOfRangeSlotsAreNeverBlocked() { + Map all = states(0, 1, 2, 3, 4, 5, 6, 7, 8); + assertFalse(SlotLockPolicy.isDropBlocked(all, -1, true, false)); + assertFalse(SlotLockPolicy.isDropBlocked(all, 9, true, false)); + assertFalse(SlotLockPolicy.isSlotLocked(all, -1)); + assertFalse(SlotLockPolicy.isSlotLocked(all, 9)); + } + + @Test + void missingOrNullStatesAreNeverBlocked() { + assertFalse(SlotLockPolicy.isDropBlocked(null, 0, true, false)); + assertFalse(SlotLockPolicy.isDropBlocked(new HashMap<>(), 0, true, false)); + assertFalse(SlotLockPolicy.isSlotLocked(null, 0)); + } + + @Test + void unknownOrNonBooleanEntriesAreIgnored() { + Map odd = new HashMap<>(); + odd.put("slot", true); + odd.put("slot10", true); + odd.put("slot0", true); + assertFalse(SlotLockPolicy.isSlotLocked(odd, 0)); + assertFalse(SlotLockPolicy.isDropBlocked(odd, 0, true, false)); + } + + @Test + void nearbyCheckDoesNotAffectUnlockedSlots() { + assertFalse(SlotLockPolicy.isDropBlocked(states(8), 3, true, true)); + } +}