chore(history): squash 16 commit(s) from 2026-09-15
- fix(stability): FireworkParticles NPE, mesh trim, tab-list warn spam, TargetHud mob icons - fix(trollface_mask): маска не рендерилась на себе, всех/друзей игнорировал всех кроме прицела - docs(todo): план Фазы 10 — заход по референсам из Downloads (trollface/rain/hiteffect/hmi) - docs(todo): вывод по сравнению Rain — уже паритет с обоими референсами, mokriyzhir отдельная идея на будущее - fix(wet_surface): кривая нормаль/мёртвый код ряби + новый модуль WetWorld (SSR top из 3 референсов) - docs(todo): отмечен WetWorld + фикс wet_surface шейдера как выполненные - feat(holdmyitems): grip-хват для мечей/инструментов (портировано из Delta HMI) - docs(todo): отмечен HoldMyItems grip как выполненный - feat(hiteffect): режим Impact Ring — кольцевая волна + вертикальное свечение при ударе - docs(todo): отмечена Фаза 10 полностью выполненной - feat(gps): новый модуль Gps — компас-стрелка + маркер к точке (идея Soup) - feat(hitbubbles): новый модуль HitBubbles — отдельно от HitEffect.BUBBLES - feat(ambientparticles): новый модуль — атмосферные мошки / светлячки со шлейфом - docs(todo): отмечены Gps/HitBubbles/AmbientParticles как выполненные (Фаза 7) - feat(endcrystal): новый модуль — тонировка модели + осколки при разрушении - docs(todo): отмечен EndCrystal как выполненный
This commit is contained in:
parent
e06ebc26ff
commit
4a73a70dbf
39 changed files with 2250 additions and 201 deletions
34
TODO.md
34
TODO.md
|
|
@ -48,18 +48,34 @@
|
||||||
контейнера с настройкой цвета/видимости, БЕЗ увеличения хитбокса (увеличение —
|
контейнера с настройкой цвета/видимости, БЕЗ увеличения хитбокса (увеличение —
|
||||||
чит, удалён в Фазе 3.5). Референс: `visuals/Hitboxes.java`
|
чит, удалён в Фазе 3.5). Референс: `visuals/Hitboxes.java`
|
||||||
- [x] **Watermark** (HUD, draggable) — обещан в README, в коде отсутствует
|
- [x] **Watermark** (HUD, draggable) — обещан в README, в коде отсутствует
|
||||||
- [ ] **Gps** (world) — луч/маркер направления к точке (у Soup ~400 стр.); связка
|
- [x] **Gps** (world) — ЗАВЕРШЕНО 2026-09-15: компас-стрелка + маркер к точке
|
||||||
с Xaero-waypoints и координатами. Референс: `world/Gps.java`
|
(`visuals/world/Gps.java`, `util/world/GpsBearing.java` чистая математика + тесты,
|
||||||
|
команда `%gps <x> <y> <z> | clear`). Референс: `world/Gps.java` — идея взята
|
||||||
|
(стрелка+маркер+дистанция+авто-удаление), код не копировался. Xaero-связка НЕ
|
||||||
|
сделана (была опциональной идеей, не в скоупе запроса). Коммит fb0b288.
|
||||||
- [x] **Light** (world) — кастомный цвет освещения (у Soup 41 стр.). Референс: `world/Light.java`
|
- [x] **Light** (world) — кастомный цвет освещения (у Soup 41 стр.). Референс: `world/Light.java`
|
||||||
- [ ] **HitBubbles** (particles) — пузыри при ударах по игрокам (у Soup 323 стр.).
|
- [x] **HitBubbles** (particles) — ЗАВЕРШЕНО 2026-09-15: отдельный модуль
|
||||||
Референс: `particles/HitBubbles.java`
|
`visuals/effects/HitBubbles.java` (не режим HitEffect.BUBBLES — независимое включение,
|
||||||
- [ ] **AmbientParticles + FireFlyParticle** (particles) — атмосферные частицы мира /
|
трёхфазная анимация появление/жизнь/исчезновение через новый `ThreeStageProgress`).
|
||||||
светлячки (у Soup 775 + 241 стр.). Референс: `particles/AmbientParticles.java`,
|
Заодно вынесены общие хелперы из HitEffect (DRY): `AttackHitPoint`,
|
||||||
`particles/types/FireFlyParticle.java`
|
`OrientedQuadRenderer`, `AnimatedColorModeOptions`. Референс: `particles/HitBubbles.java`
|
||||||
|
(идея взята, код не копировался; интерполяции/множественные текстуры Soup не портированы).
|
||||||
|
Коммит 5f2fc70.
|
||||||
|
- [x] **AmbientParticles + FireFlyParticle** (particles) — ЗАВЕРШЕНО 2026-09-15: модуль
|
||||||
|
`visuals/particles/AmbientParticles.java`, два режима — Ambient (плывущие мошки) и
|
||||||
|
Firefly (блуждание + кольцевой буфер шлейфа вместо Trail-объектов референса). 19
|
||||||
|
визуальных стилей референса (Stars/Hearts/Bloom/.../Pyramid, asset-тяжёлые) НЕ
|
||||||
|
портированы — фича-крип чит-клиента вне направления проекта; цвет через уже
|
||||||
|
существующий AnimatedRenderColors вместо своей Sync/Custom-системы. Коммит 28ec166.
|
||||||
- [x] **NameProtect** (misc) — подмена своего ника и ников друзей в тексте чата/скорборда
|
- [x] **NameProtect** (misc) — подмена своего ника и ников друзей в тексте чата/скорборда
|
||||||
(у Soup 42 стр.). Референс: `other/NameProtect.java`
|
(у Soup 42 стр.). Референс: `other/NameProtect.java`
|
||||||
- [ ] **EndCrystal** (visuals) — улучшенный рендер энд-кристаллов (у Soup 217 стр.).
|
- [x] **EndCrystal** (visuals) — ЗАВЕРШЕНО 2026-09-15: `visuals/scene/EndCrystal.java` +
|
||||||
Референс: `other/EndCrystal.java`
|
`mixins/render/entity/EndCrystalRendererMixin.java` (WrapOperation на ModelPart.render,
|
||||||
|
различает 2 вызова glass + 1 cube счётчиком-ordinal — тонировка/скрытие частей) +
|
||||||
|
inject в `ClientLevelMixin.removeEntity` (осколки-вспышка на месте разрушения, т.к.
|
||||||
|
ваниль убирает сущность мгновенно). Референс: `other/EndCrystal.java` — идея взята
|
||||||
|
(кастомный цвет, "доиграть" разрушение), Sync/Custom×4-цвета+Wave/Vertex упрощены до
|
||||||
|
общего AnimatedRenderColors. Коммит 50db8e3.
|
||||||
- [ ] **Trinket 2.0** — апгрейд нашего CosmeticDisplay до уровня Soup Trinket:
|
- [ ] **Trinket 2.0** — апгрейд нашего CosmeticDisplay до уровня Soup Trinket:
|
||||||
иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл
|
иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл
|
||||||
(цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы
|
(цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы
|
||||||
|
|
|
||||||
|
|
@ -409,3 +409,85 @@
|
||||||
(N = проходов за кадр).
|
(N = проходов за кадр).
|
||||||
- [ ] `./gradlew build` после каждого пункта; `./gradlew test` зелёный.
|
- [ ] `./gradlew build` после каждого пункта; `./gradlew test` зелёный.
|
||||||
- [ ] Порядок: 9.1 (по одному пункту на коммит) → 9.2.
|
- [ ] Порядок: 9.1 (по одному пункту на коммит) → 9.2.
|
||||||
|
|
||||||
|
## Фаза 10: Заход по референсам из Downloads (2026-09-15)
|
||||||
|
> Пользователь скинул 5 архивов, распакованы в `/tmp/lv_refs/`. Порядок выполнения —
|
||||||
|
> сверху вниз, по одному коммиту на пункт, `./gradlew build`+`test` после каждого.
|
||||||
|
|
||||||
|
- [x] **TrollfaceMask — почини "не работает"** (`/tmp/lv_refs/trollface/TrollfaceMask.java`,
|
||||||
|
референс Moonward 1.21.4). Наш модуль: `trollface/TrollfaceMask.java`.
|
||||||
|
Причина бага: биллборд ориентировался к камере (headPos→camPos); от первого лица
|
||||||
|
камера почти совпадает по вертикали с головой → view≈(0,1,0) → right=view×up
|
||||||
|
вырождается в 0 → плоскость ложится горизонтально, не видна. Режимы «Друзей»/«Всех»
|
||||||
|
брали только `TargetManager.getTarget()` (сущность под прицелом) — маска рисовалась
|
||||||
|
максимум на одном игроке. Фикс: ориентация по `getViewVector()` цели вместо камеры,
|
||||||
|
перебор `mc.level.players()` с фильтром по режиму (себя/friend via PlayerRelations/все).
|
||||||
|
Коммит e7e0ff2.
|
||||||
|
- [x] **Rain — сравнение с двумя референсами (2026-09-15, вывод: у нас уже паритет/лучше).**
|
||||||
|
Референс 1 `/tmp/lv_refs/rainfull/` (Cataclysm client) — наш `Rain.java` (894 строки) уже
|
||||||
|
является портом этого самого класса (Drop/Splash/Droplet/Mist, presetDensity/Wind/Mass,
|
||||||
|
drawStreaks/Ripples/Crowns/Droplets/Mist — 1:1 совпадающие константы и формулы), ПЛЮС у нас
|
||||||
|
уже есть их wet-surface screen-space постфх (`WetSurfaceUniforms` + шейдер
|
||||||
|
`assets/lovisual/shaders/wet_surface.frag`, pipeline `WET_SURFACE`, реализован как
|
||||||
|
`PostProcessPass` в самом Rain.java) — то, что в референсе вынесено в отдельный
|
||||||
|
`WetSurfaceRenderer`. Улучшать нечего, разница только в деталях реализации пайплайна.
|
||||||
|
Референс 2 `/tmp/lv_refs/mokriyzhir/` (`WetWorld.java`, пакет `ez.redux...`) — это ОТДЕЛЬНЫЙ
|
||||||
|
самостоятельный модуль "Wet World" из чужого чит-клиента (Category.VISUALS), НЕ часть дождя:
|
||||||
|
мочит поверхности всего мира по реальной погоде сервера (`world.getRainGradient`), с настройками
|
||||||
|
Quality (Low/Medium/High/Ultra), Reflection Strength, Wet Darkening, Ripples+Speed, Rain Only,
|
||||||
|
учитывает fog (интеграция с их модулем Fog: цвет/дистанция/яркость подмешивается в шейдер).
|
||||||
|
У нас такого отдельного всегда-активного "мир мокрый по факту дождя" модуля нет — наш
|
||||||
|
wet-surface жёстко завязан на модуль Rain (работает только когда сам Rain включён). Если
|
||||||
|
захочется — можно вынести wet-surface-pass Rain.java в отдельный модуль WetWorld с привязкой
|
||||||
|
к `mc.level.isRaining()`+`getRainLevel(tickDelta)` вместо `onlyWhenRaining` галки, плюс quality-
|
||||||
|
пресет и fog-интеграцию. НЕ делаю сейчас — не запрошено явно, чисто идея на будущее.
|
||||||
|
- [x] **WetWorld — сделан отдельным модулем (2026-09-15), шейдер Rain пофикшен.**
|
||||||
|
Третий референс `/tmp/lv_refs/wetworld2/` (`ru.white...WetWorld` + `WetWorldPipeline` +
|
||||||
|
`wet_world.vsh/fsh`) оказался технически лучшим из трёх: настоящий SSR с бинарным
|
||||||
|
рефайнментом пересечения, dithering шага луча, реконструкция нормали по соседям в буфере
|
||||||
|
глубины (не screen-space derivative), fbm-небо с sun specular, fog-интеграция.
|
||||||
|
Проверка нашего `wet_surface.frag` («может кривые») подтвердила баги:
|
||||||
|
1) `normalFromDepth` считался через `dFdx/dFdy(viewPos)` — рвётся на границах глубины,
|
||||||
|
шумная/блочная нормаль -> дрожащий край лужи;
|
||||||
|
2) рябь была МЁРТВЫМ КОДОМ: `vec2 perturb = vec2(dFdx(ripple), dFdy(ripple)) * 0.0;` —
|
||||||
|
умножение на 0 полностью гасило эффект, а сама рябь добавлялась как сырое значение
|
||||||
|
волны вместо её наклона.
|
||||||
|
Исправлено (коммит 0986ce6): нормаль — по 4 соседям depth-буфера (как в лучшем референсе),
|
||||||
|
рябь — аналитический градиент (разность высоты в соседних точках), добавлен bin-рефайнмент
|
||||||
|
SSR-пересечения + dithering, небо дополнено fbm-облаками/sun specular, затемнение вынесено
|
||||||
|
в отдельный uniform-параметр (было зашито константой 0.82). Новый модуль
|
||||||
|
`visuals/weather/WetWorld.java`: мочит мир по факту `mc.level.getRainLevel()`, независимо
|
||||||
|
от партиклов Rain и тумблера `wet_ground` — Quality (Low/Medium/High/Ultra
|
||||||
|
-> шаги/дистанция/bin-рефайнмент SSR), Reflection Strength, Darkening, Ripples+Speed,
|
||||||
|
Rain Only. Переиспользует общий `WET_SURFACE` pipeline/`WetSurfaceUniforms` (при
|
||||||
|
одновременном включении с Rain.wet_ground эффект стекается — задокументировано в коде).
|
||||||
|
Fog-интеграция референса НЕ перенесена (нет доступа к чужому Fog-модулю/данным тумана в
|
||||||
|
нашем движке в этом заходе) — можно добавить отдельно, если понадобится.
|
||||||
|
- [x] **HitEffect — интегрирован ImpactRing (2026-09-15).**
|
||||||
|
Референс `/tmp/lv_refs/myasomaybe/ImpactRing/` — `JumpWaveRenderer`/`JumpGlowRenderer`
|
||||||
|
используют захват сцены + собственный низкоуровневый RHI-пайплайн с SSR-варпом
|
||||||
|
(кастомный шейдер сэмплит скопированный цветовой буфер по кольцу) — тяжеловесная техника
|
||||||
|
не в духе нашего движка. Взята идея визуала (кольцо + вертикальная стена свечения в точке
|
||||||
|
удара), реализована через уже существующий движок: обычные аддитивные квады через
|
||||||
|
MeshBuilder/Renderer3D (`WORLD_COLORED_ADDITIVE` + `WORLD_COLORED_ADDITIVE_DEPTH`), без
|
||||||
|
захвата сцены и отдельного шейдера — консистентно с уже существующими WaveEffect/HitBubble
|
||||||
|
в том же файле. Новый `EffectMode.IMPACT` в HitEffect: настройки impact_radius/
|
||||||
|
impact_duration/impact_glow_height, срабатывает как WAVE (по удару), рендер —
|
||||||
|
расширяющееся кольцо (сегменты, fade к центру) + полупрозрачная вертикальная стена по
|
||||||
|
контуру круга, затухающая вверх и во времени. Коммит 61c478e. `./gradlew build`+`test` ✓.
|
||||||
|
- [x] **HoldMyItems — сверка с HMI.zip (2026-09-15), портирован grip.**
|
||||||
|
Референс `/tmp/lv_refs/hmi/` (Delta HMI): обёртка обфусцированного чит-клиента над
|
||||||
|
СТОРОННИМ jar-модом `holdmyitems-5.1.1+26.2.jar` + свои `SwingAnimations` (десяток стилей
|
||||||
|
свинга, weapon-inspect a-la CS:GO karambit-spin, "Pretty Hands" режимы). Полный набор
|
||||||
|
НЕ портирован — фича-крип чит-клиента, конфликтует с KISS и с правилом «без стороннего
|
||||||
|
jar в сборке» (Delta буквально бандлит чужой мод и синхронизирует с ним настройки через
|
||||||
|
`OriginalHmiBridge`). Взята единственная самоценная и самодостаточная идея — **grip**:
|
||||||
|
лёгкий хват предметов с рукоятью (мечи/инструменты/трезубец/палки — см.
|
||||||
|
`heldAlongTheFist`) наискось в кулаке вместо строго вертикального ванильного equip.
|
||||||
|
Портировано (коммит b257fde): `HeldAlongFist` (чистый предикат по ItemTags),
|
||||||
|
`HandPoseMath.grip(...)` (чистая функция позы, тесты gripMirrorsBySide/
|
||||||
|
gripExtendPullsCloserAndAcrossFist), настройки `grip_enabled`+6 slider'ов в HoldMyItems
|
||||||
|
(дефолты как у референсного пресета "HMI"), проброс ItemStack через
|
||||||
|
HandDynamics.apply/ItemInHandRendererMixin. `./gradlew build`+`test` зелёные.
|
||||||
|
|
||||||
|
### Проверка после каждого пункта: `./gradlew build` ✓, `./gradlew test` ✓, TODO.md отмечен
|
||||||
|
|
|
||||||
|
|
@ -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.command.impl.world;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.features.command.core.CommandContext;
|
||||||
|
import dev.loki.lovisual.features.command.core.CommandInfo;
|
||||||
|
import dev.loki.lovisual.features.command.util.ClientCommand;
|
||||||
|
import dev.loki.lovisual.features.command.util.CommandOutput;
|
||||||
|
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||||
|
import dev.loki.lovisual.features.module.modules.visuals.world.Gps;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@CommandInfo(
|
||||||
|
id = "gps",
|
||||||
|
usage = "%gps <x> <y> <z> | %gps clear",
|
||||||
|
descriptionKey = "command.gps.description"
|
||||||
|
)
|
||||||
|
public final class GpsCommand implements ClientCommand {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean execute(CommandContext ctx) {
|
||||||
|
Gps gps = Modules.get(Gps.class);
|
||||||
|
if (gps == null) {
|
||||||
|
CommandOutput.error("Gps module is unavailable.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String first = ctx.arg(0);
|
||||||
|
if (first != null && "clear".equalsIgnoreCase(first.trim())) {
|
||||||
|
gps.clearTarget();
|
||||||
|
CommandOutput.success("Gps target cleared.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.args().size() < 3) {
|
||||||
|
CommandOutput.warning("Usage: " + metadata().usage());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer x = parseInt(ctx.arg(0));
|
||||||
|
Integer y = parseInt(ctx.arg(1));
|
||||||
|
Integer z = parseInt(ctx.arg(2));
|
||||||
|
if (x == null || y == null || z == null) {
|
||||||
|
CommandOutput.error("Invalid coordinates. Use integers.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
gps.setTarget(new BlockPos(x, y, z));
|
||||||
|
CommandOutput.success(String.format(Locale.ROOT, "Gps target set: %d, %d, %d", x, y, z));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(CommandContext ctx, int argIndex, String token) {
|
||||||
|
if (argIndex == 0) {
|
||||||
|
String lower = token == null ? "" : token.toLowerCase(Locale.ROOT);
|
||||||
|
return "clear".startsWith(lower) ? List.of("clear") : List.of();
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer parseInt(String value) {
|
||||||
|
if (value == null || value.isBlank()) return null;
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,13 +19,18 @@ import dev.loki.lovisual.util.player.health.PlayerSkinResolver;
|
||||||
import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot;
|
import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.entity.EntityType;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.Item;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.Items;
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.item.SpawnEggItem;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud.*;
|
import static dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud.*;
|
||||||
|
|
@ -127,6 +132,18 @@ public final class TargetHudFaceRenderer {
|
||||||
HudRenderUtil.scaleAlpha(element.uiStroke, alphaFactor)
|
HudRenderUtil.scaleAlpha(element.uiStroke, alphaFactor)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ItemStack mobIcon = resolveMobIcon(target);
|
||||||
|
if (mobIcon != null && !mobIcon.isEmpty()) {
|
||||||
|
float iconSize = size * 0.72f;
|
||||||
|
float itemScale = Math.max(0.35f, iconSize / 16.0f);
|
||||||
|
float ix = x + (size - iconSize) * 0.5f;
|
||||||
|
float iy = y + (size - iconSize) * 0.5f;
|
||||||
|
// Slight hurt tint via background already; keep icon full alpha
|
||||||
|
TargetHudEquipmentRenderer.drawItem(renderer, mobIcon, ix, iy, itemScale, 0, alphaFactor);
|
||||||
|
// Add tiny hurt overlay as text shadow alternative is already via tint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String name = target.getName().getString();
|
String name = target.getName().getString();
|
||||||
String letter = name.isEmpty() ? "?" : name.substring(0, 1).toUpperCase(Locale.ROOT);
|
String letter = name.isEmpty() ? "?" : name.substring(0, 1).toUpperCase(Locale.ROOT);
|
||||||
TextRenderer faceRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, textRenderer);
|
TextRenderer faceRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, textRenderer);
|
||||||
|
|
@ -150,6 +167,29 @@ public final class TargetHudFaceRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ItemStack resolveMobIcon(LivingEntity target) {
|
||||||
|
if (target == null) return null;
|
||||||
|
try {
|
||||||
|
EntityType<?> type = target.getType();
|
||||||
|
Optional<Holder<Item>> eggHolder = SpawnEggItem.byId(type);
|
||||||
|
if (eggHolder.isPresent()) {
|
||||||
|
return new ItemStack(eggHolder.get().value());
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
// Fallback skulls / heads for bosses and special mobs without spawn eggs
|
||||||
|
try {
|
||||||
|
String id = net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(target.getType()).toString();
|
||||||
|
if (id.contains("ender_dragon")) return new ItemStack(Items.DRAGON_HEAD);
|
||||||
|
if (id.contains("wither")) return new ItemStack(Items.WITHER_SKELETON_SKULL);
|
||||||
|
if (id.contains("iron_golem")) return new ItemStack(Items.IRON_BLOCK);
|
||||||
|
if (id.contains("snow_golem")) return new ItemStack(Items.SNOW_BLOCK);
|
||||||
|
if (id.contains("giant")) return new ItemStack(Items.ZOMBIE_HEAD);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static void drawTotemPopBadge(TargetHud element, Renderer2D renderer, TextRenderer textRenderer, TotemPopSnapshot snapshot, float faceX, float faceY, float faceSize, float faceRadius, float scaleFactor, float alphaFactor) {
|
public static void drawTotemPopBadge(TargetHud element, Renderer2D renderer, TextRenderer textRenderer, TotemPopSnapshot snapshot, float faceX, float faceY, float faceSize, float faceRadius, float scaleFactor, float alphaFactor) {
|
||||||
if (snapshot == null || snapshot.count() <= 0 || element.totemPopAnimation <= 0.001f) {
|
if (snapshot == null || snapshot.count() <= 0 || element.totemPopAnimation <= 0.001f) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* 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.effects;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||||
|
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||||
|
import dev.loki.lovisual.mixininterface.entity.ILocalPlayer;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.render.helpers.particle.OrientedQuadRenderer;
|
||||||
|
import dev.loki.lovisual.util.animation.ThreeStageProgress;
|
||||||
|
import dev.loki.lovisual.util.combat.hit.AttackHitPoint;
|
||||||
|
import dev.loki.lovisual.util.time.GameClock;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пузыри-разлёт в точке попадания по цели — отдельный от {@link HitEffect}
|
||||||
|
* модуль (референс: Soup HitBubbles, идея переиспользована, код не копировался).
|
||||||
|
* В отличие от режима {@code EffectMode.BUBBLES} в HitEffect (простой fade-out
|
||||||
|
* за lifeTime), тут трёхфазная анимация появление/жизнь/исчезновение
|
||||||
|
* ({@link ThreeStageProgress}) и независимое от HitEffect включение —
|
||||||
|
* оба модуля можно использовать одновременно или по отдельности.
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "hitbubbles",
|
||||||
|
displayName = "HitBubbles",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Пузыри в точке попадания с трёхфазной анимацией появления/жизни/исчезновения"
|
||||||
|
)
|
||||||
|
public final class HitBubbles extends Module {
|
||||||
|
|
||||||
|
private static final float CHARGED_THRESHOLD = 0.9f;
|
||||||
|
|
||||||
|
private final ModeValue colorMode = modeSetting("hitBubblesColorMode", "color_mode", "Static", AnimatedColorModeOptions.OPTIONS);
|
||||||
|
private final NumberValue<Integer> colorSpeed = num("hitBubblesColorSpeed", "color_speed", 18, 2, 54);
|
||||||
|
private final RGBAColorValue colorValue = color("hitBubblesColor", "color", "#FF55FFFF");
|
||||||
|
private final RGBAColorValue colorValue2 = visibleWhen(color("hitBubblesColor2", "color2", "#FFFF5555"), this::usesSecondaryColor);
|
||||||
|
private final NumberValue<Float> bubbleScale = num("hitBubblesScale2", "scale", 1.0f, 0.4f, 2.0f);
|
||||||
|
private final NumberValue<Float> rotateSpeed = num("hitBubblesRotateSpeed", "rotate_speed", 2.0f, 0.5f, 5.0f);
|
||||||
|
private final NumberValue<Integer> appearMs = num("hitBubblesAppearMs", "appear_duration_ms", 150, 20, 1500);
|
||||||
|
private final NumberValue<Integer> existMs = num("hitBubblesExistMs", "exist_duration_ms", 300, 20, 2000);
|
||||||
|
private final NumberValue<Integer> disappearMs = num("hitBubblesDisappearMs", "disappear_duration_ms", 300, 20, 2000);
|
||||||
|
private final BooleanValue onlyPlayers = bool("hitBubblesOnlyPlayers2", "only_players", true);
|
||||||
|
private final BooleanValue depthTest = bool("hitBubblesDepthTest2", "depth_test", true);
|
||||||
|
|
||||||
|
private final List<Bubble> bubbles = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
bubbles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleHit(Entity target) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (!isEnabled() || mc.player == null || mc.level == null) return;
|
||||||
|
if (!(target instanceof LivingEntity)) return;
|
||||||
|
if (onlyPlayers.get() && !(target instanceof Player)) return;
|
||||||
|
|
||||||
|
ILocalPlayer access = (ILocalPlayer) mc.player;
|
||||||
|
float yaw = access.lovisual$getLastYaw();
|
||||||
|
float pitch = access.lovisual$getLastPitch();
|
||||||
|
|
||||||
|
Vec3 point = AttackHitPoint.resolve(target, yaw, pitch);
|
||||||
|
if (point == null) point = target.position();
|
||||||
|
|
||||||
|
bubbles.add(new Bubble(point, -yaw, pitch, GameClock.millis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.END_MAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (!isEnabled() || mc.player == null || mc.level == null || bubbles.isEmpty()) return;
|
||||||
|
|
||||||
|
long totalMs = totalDurationMs();
|
||||||
|
bubbles.removeIf(b -> GameClock.millis() - b.startMs > totalMs);
|
||||||
|
if (bubbles.isEmpty()) return;
|
||||||
|
|
||||||
|
boolean useDepth = depthTest.get();
|
||||||
|
RenderPipeline pipeline = useDepth
|
||||||
|
? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE
|
||||||
|
: LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE;
|
||||||
|
Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN;
|
||||||
|
|
||||||
|
MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BUBBLE, depthMode);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
long now = GameClock.millis();
|
||||||
|
for (Bubble b : bubbles) {
|
||||||
|
long elapsed = now - b.startMs;
|
||||||
|
float visibility = ThreeStageProgress.visibility(elapsed, appearMs.get(), existMs.get(), disappearMs.get());
|
||||||
|
if (visibility <= 0.01f) continue;
|
||||||
|
|
||||||
|
b.rotationDeg -= b.angularVelocity * rotateSpeed.get();
|
||||||
|
float scale = visibility * 2f * bubbleScale.get();
|
||||||
|
|
||||||
|
int c1 = applyOpacity(getColorArgb(270), visibility);
|
||||||
|
int c2 = applyOpacity(getColorArgb(0), visibility);
|
||||||
|
int c3 = applyOpacity(getColorArgb(180), visibility);
|
||||||
|
int c4 = applyOpacity(getColorArgb(90), visibility);
|
||||||
|
|
||||||
|
OrientedQuadRenderer.addOrientedQuad(mesh, b.pos, b.yaw, b.pitch, b.rotationDeg, scale, c1, c2, c3, c4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long totalDurationMs() {
|
||||||
|
return (long) appearMs.get() + existMs.get() + disappearMs.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean usesSecondaryColor() {
|
||||||
|
return AnimatedRenderColors.usesSecondary(AnimatedColorModeOptions.parse(colorMode.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getColorArgb(int phase) {
|
||||||
|
return AnimatedRenderColors.resolve(
|
||||||
|
AnimatedColorModeOptions.parse(colorMode.get()),
|
||||||
|
colorSpeed.get(),
|
||||||
|
phase,
|
||||||
|
colorValue.getArgb(),
|
||||||
|
colorValue2.getArgb()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int applyOpacity(int argb, float opacity) {
|
||||||
|
float clamped = Math.min(1f, Math.max(0f, opacity));
|
||||||
|
int a = (argb >>> 24) & 0xFF;
|
||||||
|
int na = Mth.clamp((int) (a * clamped), 0, 255);
|
||||||
|
return (na << 24) | (argb & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Bubble {
|
||||||
|
private final Vec3 pos;
|
||||||
|
private final float yaw;
|
||||||
|
private final float pitch;
|
||||||
|
private final long startMs;
|
||||||
|
private final float angularVelocity;
|
||||||
|
private float rotationDeg;
|
||||||
|
|
||||||
|
private Bubble(Vec3 pos, float yaw, float pitch, long startMs) {
|
||||||
|
this.pos = pos;
|
||||||
|
this.yaw = yaw;
|
||||||
|
this.pitch = pitch;
|
||||||
|
this.startMs = startMs;
|
||||||
|
this.angularVelocity = 0.5f + (float) Math.random() * 1.5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,10 +22,7 @@ import net.minecraft.util.Mth;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.entity.projectile.ProjectileUtil;
|
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraft.world.phys.AABB;
|
|
||||||
import net.minecraft.world.phys.EntityHitResult;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||||
import org.joml.Quaternionf;
|
import org.joml.Quaternionf;
|
||||||
|
|
@ -39,13 +36,16 @@ import dev.loki.lovisual.mixininterface.entity.ILocalPlayer;
|
||||||
import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown;
|
import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown;
|
||||||
import dev.loki.lovisual.render.engine.RenderState;
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions;
|
||||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.render.helpers.particle.OrientedQuadRenderer;
|
||||||
import dev.loki.lovisual.render.helpers.player.Particle3D;
|
import dev.loki.lovisual.render.helpers.player.Particle3D;
|
||||||
import dev.loki.lovisual.render.helpers.util.ParticleTextureMode;
|
import dev.loki.lovisual.render.helpers.util.ParticleTextureMode;
|
||||||
|
import dev.loki.lovisual.util.combat.hit.AttackHitPoint;
|
||||||
import dev.loki.lovisual.util.time.Timer;
|
import dev.loki.lovisual.util.time.Timer;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
|
@ -72,6 +72,10 @@ public class HitEffect extends Module {
|
||||||
private static final String SETTING_COLOR2 = "color2";
|
private static final String SETTING_COLOR2 = "color2";
|
||||||
private static final String SETTING_WAVE_RADIUS = "wave_radius";
|
private static final String SETTING_WAVE_RADIUS = "wave_radius";
|
||||||
private static final String SETTING_WAVE_DURATION = "wave_duration";
|
private static final String SETTING_WAVE_DURATION = "wave_duration";
|
||||||
|
private static final String SETTING_IMPACT_RADIUS = "impact_radius";
|
||||||
|
private static final String SETTING_IMPACT_DURATION = "impact_duration";
|
||||||
|
private static final String SETTING_IMPACT_GLOW_HEIGHT = "impact_glow_height";
|
||||||
|
private static final int IMPACT_RING_SEGMENTS = 24;
|
||||||
private static final String SETTING_HIT_PARTICLES = "hit_particles";
|
private static final String SETTING_HIT_PARTICLES = "hit_particles";
|
||||||
private static final String SETTING_HIT_PARTICLE_COUNT = "hit_particle_count";
|
private static final String SETTING_HIT_PARTICLE_COUNT = "hit_particle_count";
|
||||||
private static final String SETTING_HIT_PARTICLE_SIZE = "hit_particle_size";
|
private static final String SETTING_HIT_PARTICLE_SIZE = "hit_particle_size";
|
||||||
|
|
@ -122,6 +126,12 @@ public class HitEffect extends Module {
|
||||||
visibleWhen(num("hitBubblesWaveRadius", SETTING_WAVE_RADIUS, 12, 3, 32), this::isWaveMode);
|
visibleWhen(num("hitBubblesWaveRadius", SETTING_WAVE_RADIUS, 12, 3, 32), this::isWaveMode);
|
||||||
private final NumberValue<Integer> waveDurationMs =
|
private final NumberValue<Integer> waveDurationMs =
|
||||||
visibleWhen(num("hitBubblesWaveDuration", SETTING_WAVE_DURATION, 1500, 200, 6000), this::isWaveMode);
|
visibleWhen(num("hitBubblesWaveDuration", SETTING_WAVE_DURATION, 1500, 200, 6000), this::isWaveMode);
|
||||||
|
private final NumberValue<Float> impactRadius =
|
||||||
|
visibleWhen(num("hitBubblesImpactRadius", SETTING_IMPACT_RADIUS, 1.4f, 0.4f, 4.0f), this::isImpactMode);
|
||||||
|
private final NumberValue<Integer> impactDurationMs =
|
||||||
|
visibleWhen(num("hitBubblesImpactDuration", SETTING_IMPACT_DURATION, 450, 150, 1500), this::isImpactMode);
|
||||||
|
private final NumberValue<Float> impactGlowHeight =
|
||||||
|
visibleWhen(num("hitBubblesImpactGlowHeight", SETTING_IMPACT_GLOW_HEIGHT, 0.8f, 0.2f, 2.5f), this::isImpactMode);
|
||||||
private final BooleanValue hitParticlesEnabled =
|
private final BooleanValue hitParticlesEnabled =
|
||||||
bool("hitEffectParticlesEnabled", SETTING_HIT_PARTICLES, false);
|
bool("hitEffectParticlesEnabled", SETTING_HIT_PARTICLES, false);
|
||||||
private final NumberValue<Integer> hitParticleCount =
|
private final NumberValue<Integer> hitParticleCount =
|
||||||
|
|
@ -138,6 +148,7 @@ public class HitEffect extends Module {
|
||||||
private final List<HitBubble> bubbles = new ArrayList<>();
|
private final List<HitBubble> bubbles = new ArrayList<>();
|
||||||
private final List<Star> stars = new ArrayList<>();
|
private final List<Star> stars = new ArrayList<>();
|
||||||
private final List<WaveEffect> waves = new ArrayList<>();
|
private final List<WaveEffect> waves = new ArrayList<>();
|
||||||
|
private final List<ImpactRing> impacts = new ArrayList<>();
|
||||||
private final List<Particle3D> hitParticles = new ArrayList<>();
|
private final List<Particle3D> hitParticles = new ArrayList<>();
|
||||||
private final List<Particle3D>[] hitParticleBuckets = createBuckets();
|
private final List<Particle3D>[] hitParticleBuckets = createBuckets();
|
||||||
private int lastHitParticleTick = Integer.MIN_VALUE;
|
private int lastHitParticleTick = Integer.MIN_VALUE;
|
||||||
|
|
@ -165,44 +176,6 @@ public class HitEffect extends Module {
|
||||||
return (na << 24) | (argb & 0x00FFFFFF);
|
return (na << 24) | (argb & 0x00FFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOrientedQuad(MeshBuilder mesh, Vec3 center, float yaw, float pitch,
|
|
||||||
float spinDeg, float scale, int c1, int c2, int c3, int c4) {
|
|
||||||
Quaternionf rot = new Quaternionf()
|
|
||||||
.rotateY((float) Math.toRadians(yaw))
|
|
||||||
.rotateX((float) Math.toRadians(pitch));
|
|
||||||
|
|
||||||
Vector3f forward = new Vector3f(0, 0, 1).rotate(rot);
|
|
||||||
Vector3f right = new Vector3f(1, 0, 0).rotate(rot);
|
|
||||||
Vector3f up = new Vector3f(0, 1, 0).rotate(rot);
|
|
||||||
|
|
||||||
Quaternionf spin = new Quaternionf().fromAxisAngleRad(forward, (float) Math.toRadians(spinDeg));
|
|
||||||
right.rotate(spin);
|
|
||||||
up.rotate(spin);
|
|
||||||
|
|
||||||
right.mul(scale / 2f);
|
|
||||||
up.mul(scale / 2f);
|
|
||||||
|
|
||||||
double p1x = center.x - right.x() - up.x();
|
|
||||||
double p1y = center.y - right.y() - up.y();
|
|
||||||
double p1z = center.z - right.z() - up.z();
|
|
||||||
double p2x = center.x + right.x() - up.x();
|
|
||||||
double p2y = center.y + right.y() - up.y();
|
|
||||||
double p2z = center.z + right.z() - up.z();
|
|
||||||
double p3x = center.x + right.x() + up.x();
|
|
||||||
double p3y = center.y + right.y() + up.y();
|
|
||||||
double p3z = center.z + right.z() + up.z();
|
|
||||||
double p4x = center.x - right.x() + up.x();
|
|
||||||
double p4y = center.y - right.y() + up.y();
|
|
||||||
double p4z = center.z - right.z() + up.z();
|
|
||||||
|
|
||||||
mesh.ensureQuadCapacity();
|
|
||||||
int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(c1)).next();
|
|
||||||
int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(c2)).next();
|
|
||||||
int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(c3)).next();
|
|
||||||
int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(c4)).next();
|
|
||||||
mesh.quad(i1, i2, i3, i4);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz,
|
private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz,
|
||||||
float size, Quaternionf camRot, int argb) {
|
float size, Quaternionf camRot, int argb) {
|
||||||
Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
|
Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
|
||||||
|
|
@ -274,6 +247,7 @@ public class HitEffect extends Module {
|
||||||
bubbles.clear();
|
bubbles.clear();
|
||||||
stars.clear();
|
stars.clear();
|
||||||
waves.clear();
|
waves.clear();
|
||||||
|
impacts.clear();
|
||||||
hitParticles.clear();
|
hitParticles.clear();
|
||||||
lastHitParticleTick = Integer.MIN_VALUE;
|
lastHitParticleTick = Integer.MIN_VALUE;
|
||||||
spawnedHitParticlesThisTick = 0;
|
spawnedHitParticlesThisTick = 0;
|
||||||
|
|
@ -289,13 +263,14 @@ public class HitEffect extends Module {
|
||||||
Vec3 pos = target.position();
|
Vec3 pos = target.position();
|
||||||
BlockPos basePos = BlockPos.containing(pos.x, pos.y - 0.1, pos.z);
|
BlockPos basePos = BlockPos.containing(pos.x, pos.y - 0.1, pos.z);
|
||||||
addWave(basePos);
|
addWave(basePos);
|
||||||
|
} else if (effectMode.get() == EffectMode.IMPACT) {
|
||||||
|
impacts.add(new ImpactRing(target.position(), GameClock.millis()));
|
||||||
} else {
|
} else {
|
||||||
ILocalPlayer access = (ILocalPlayer) mc.player;
|
ILocalPlayer access = (ILocalPlayer) mc.player;
|
||||||
float yaw = access.lovisual$getLastYaw();
|
float yaw = access.lovisual$getLastYaw();
|
||||||
float pitch = access.lovisual$getLastPitch();
|
float pitch = access.lovisual$getLastPitch();
|
||||||
|
|
||||||
double distance = mc.player.distanceTo(target) + 1.0;
|
Vec3 point = AttackHitPoint.resolve(target, yaw, pitch);
|
||||||
Vec3 point = getRtxPoint(target, yaw, pitch, distance);
|
|
||||||
if (point == null) point = target.position();
|
if (point == null) point = target.position();
|
||||||
|
|
||||||
bubbles.add(new HitBubble(point, -yaw, pitch, new Timer()));
|
bubbles.add(new HitBubble(point, -yaw, pitch, new Timer()));
|
||||||
|
|
@ -343,10 +318,28 @@ public class HitEffect extends Module {
|
||||||
renderWaveEffect(renderer);
|
renderWaveEffect(renderer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (effectMode.get() == EffectMode.IMPACT) {
|
||||||
|
renderImpactEffect(renderer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
renderBubbleEffect(renderer, tickDelta);
|
renderBubbleEffect(renderer, tickDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderImpactEffect(Renderer3D renderer) {
|
||||||
|
if (impacts.isEmpty()) return;
|
||||||
|
|
||||||
|
Iterator<ImpactRing> iterator = impacts.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
ImpactRing impact = iterator.next();
|
||||||
|
if (impact.isExpired()) {
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
impact.render(renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void renderBubbleEffect(Renderer3D renderer, float tickDelta) {
|
private void renderBubbleEffect(Renderer3D renderer, float tickDelta) {
|
||||||
boolean useDepth = depthTest.get();
|
boolean useDepth = depthTest.get();
|
||||||
RenderPipeline pipeline = useDepth
|
RenderPipeline pipeline = useDepth
|
||||||
|
|
@ -371,7 +364,7 @@ public class HitEffect extends Module {
|
||||||
int c3 = applyOpacity(getColorArgb(180), 1f - factor);
|
int c3 = applyOpacity(getColorArgb(180), 1f - factor);
|
||||||
int c4 = applyOpacity(getColorArgb(90), 1f - factor);
|
int c4 = applyOpacity(getColorArgb(90), 1f - factor);
|
||||||
|
|
||||||
addOrientedQuad(mesh, b.pos, b.yaw, b.pitch, angle, scale, c1, c2, c3, c4);
|
OrientedQuadRenderer.addOrientedQuad(mesh, b.pos, b.yaw, b.pitch, angle, scale, c1, c2, c3, c4);
|
||||||
}
|
}
|
||||||
|
|
||||||
bubbles.removeIf(b -> b.life.passedMs(lifeTime.get() * 50L));
|
bubbles.removeIf(b -> b.life.passedMs(lifeTime.get() * 50L));
|
||||||
|
|
@ -553,6 +546,10 @@ public class HitEffect extends Module {
|
||||||
return effectMode.get() == EffectMode.WAVE;
|
return effectMode.get() == EffectMode.WAVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isImpactMode() {
|
||||||
|
return effectMode.get() == EffectMode.IMPACT;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean usesSecondaryColor() {
|
private boolean usesSecondaryColor() {
|
||||||
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
||||||
}
|
}
|
||||||
|
|
@ -568,16 +565,7 @@ public class HitEffect extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
private AnimatedRenderColors.Mode animatedColorMode() {
|
private AnimatedRenderColors.Mode animatedColorMode() {
|
||||||
return switch (colorMode.get()) {
|
return AnimatedColorModeOptions.parse(colorMode.get());
|
||||||
case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW;
|
|
||||||
case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
|
|
||||||
case "Sky" -> AnimatedRenderColors.Mode.SKY;
|
|
||||||
case "Fade" -> AnimatedRenderColors.Mode.FADE;
|
|
||||||
case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
|
|
||||||
case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS;
|
|
||||||
case "Theme" -> AnimatedRenderColors.Mode.THEME;
|
|
||||||
default -> AnimatedRenderColors.Mode.STATIC;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCritical(Player attacker, float cooldown) {
|
private boolean isCritical(Player attacker, float cooldown) {
|
||||||
|
|
@ -607,40 +595,10 @@ public class HitEffect extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vec3 getRtxPoint(Entity target, float yaw, float pitch, double distance) {
|
|
||||||
Minecraft mc = Minecraft.getInstance();
|
|
||||||
if (mc.player == null) return null;
|
|
||||||
|
|
||||||
Vec3 start = mc.player.position().add(0, mc.player.getEyeHeight(mc.player.getPose()), 0);
|
|
||||||
Vec3 rot = getRotationVector(pitch, yaw);
|
|
||||||
Vec3 end = start.add(rot.scale(distance));
|
|
||||||
AABB box = mc.player.getBoundingBox().expandTowards(rot.scale(distance)).inflate(1.0, 1.0, 1.0);
|
|
||||||
|
|
||||||
EntityHitResult hit = ProjectileUtil.getEntityHitResult(
|
|
||||||
mc.player,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
box,
|
|
||||||
e -> !e.isSpectator() && e.isPickable() && e == target,
|
|
||||||
distance * distance
|
|
||||||
);
|
|
||||||
|
|
||||||
return hit != null ? hit.getLocation() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vec3 getRotationVector(float pitch, float yaw) {
|
|
||||||
float yawRad = -yaw * ((float) Math.PI / 180F);
|
|
||||||
float pitchRad = pitch * ((float) Math.PI / 180F);
|
|
||||||
float cosYaw = Mth.cos(yawRad);
|
|
||||||
float sinYaw = Mth.sin(yawRad);
|
|
||||||
float cosPitch = Mth.cos(pitchRad);
|
|
||||||
float sinPitch = Mth.sin(pitchRad);
|
|
||||||
return new Vec3(sinYaw * cosPitch, -sinPitch, cosYaw * cosPitch);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum EffectMode {
|
public enum EffectMode {
|
||||||
BUBBLES,
|
BUBBLES,
|
||||||
WAVE
|
WAVE,
|
||||||
|
IMPACT
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ParticleTexture(Identifier texture, int index) {
|
private record ParticleTexture(Identifier texture, int index) {
|
||||||
|
|
@ -772,4 +730,96 @@ public class HitEffect extends Module {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кольцевая волна + вертикальная "стена" свечения в точке удара — реализация
|
||||||
|
* идеи ImpactRing (JumpWaveRenderer/JumpGlowRenderer, экранный SSR-варп в
|
||||||
|
* референсе): у нас проще, обычными аддитивными квадами через MeshBuilder,
|
||||||
|
* без захвата сцены — консистентно с остальными эффектами файла.
|
||||||
|
*/
|
||||||
|
private class ImpactRing {
|
||||||
|
private final Vec3 center;
|
||||||
|
private final long startTime;
|
||||||
|
|
||||||
|
private ImpactRing(Vec3 center, long startTime) {
|
||||||
|
this.center = center;
|
||||||
|
this.startTime = startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExpired() {
|
||||||
|
return GameClock.millis() - startTime > Math.max(1L, impactDurationMs.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render(Renderer3D renderer) {
|
||||||
|
long durationMs = Math.max(1L, impactDurationMs.get());
|
||||||
|
long elapsed = GameClock.millis() - startTime;
|
||||||
|
float progress = Mth.clamp((float) elapsed / durationMs, 0f, 1f);
|
||||||
|
float fade = (float) Math.pow(1.0 - progress, 1.4);
|
||||||
|
if (fade <= 0.01f) return;
|
||||||
|
|
||||||
|
float maxRadius = Math.max(0.1f, impactRadius.get());
|
||||||
|
float radius = maxRadius * (0.15f + 0.85f * progress);
|
||||||
|
int color = getColorArgb((int) (progress * 360));
|
||||||
|
|
||||||
|
MeshBuilder ring = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE, Renderer3D.DepthMode.MAIN);
|
||||||
|
if (ring != null) {
|
||||||
|
renderRing(ring, radius, fade, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshBuilder glow = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH);
|
||||||
|
if (glow != null) {
|
||||||
|
renderGlowWall(glow, radius, fade * 0.6f, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderRing(MeshBuilder mesh, float radius, float fade, int color) {
|
||||||
|
float inner = radius * 0.72f;
|
||||||
|
for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) {
|
||||||
|
double a0 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * i;
|
||||||
|
double a1 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * (i + 1);
|
||||||
|
|
||||||
|
float ix0 = (float) (Math.cos(a0) * inner);
|
||||||
|
float iz0 = (float) (Math.sin(a0) * inner);
|
||||||
|
float ix1 = (float) (Math.cos(a1) * inner);
|
||||||
|
float iz1 = (float) (Math.sin(a1) * inner);
|
||||||
|
float ox0 = (float) (Math.cos(a0) * radius);
|
||||||
|
float oz0 = (float) (Math.sin(a0) * radius);
|
||||||
|
float ox1 = (float) (Math.cos(a1) * radius);
|
||||||
|
float oz1 = (float) (Math.sin(a1) * radius);
|
||||||
|
|
||||||
|
int edgeColor = applyOpacity(color, fade);
|
||||||
|
int fadeColor = applyOpacity(color, 0f);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(center.x + ix0, center.y + 0.02, center.z + iz0).color(new RenderColor(fadeColor)).next();
|
||||||
|
int i2 = mesh.vec3(center.x + ix1, center.y + 0.02, center.z + iz1).color(new RenderColor(fadeColor)).next();
|
||||||
|
int i3 = mesh.vec3(center.x + ox1, center.y + 0.02, center.z + oz1).color(new RenderColor(edgeColor)).next();
|
||||||
|
int i4 = mesh.vec3(center.x + ox0, center.y + 0.02, center.z + oz0).color(new RenderColor(edgeColor)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderGlowWall(MeshBuilder mesh, float radius, float fade, int color) {
|
||||||
|
float height = impactGlowHeight.get() * (1.0f - fade * 0.3f);
|
||||||
|
for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) {
|
||||||
|
double a0 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * i;
|
||||||
|
double a1 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * (i + 1);
|
||||||
|
|
||||||
|
float x0 = (float) (Math.cos(a0) * radius);
|
||||||
|
float z0 = (float) (Math.sin(a0) * radius);
|
||||||
|
float x1 = (float) (Math.cos(a1) * radius);
|
||||||
|
float z1 = (float) (Math.sin(a1) * radius);
|
||||||
|
|
||||||
|
int baseColor = applyOpacity(color, fade);
|
||||||
|
int topColor = applyOpacity(color, 0f);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(center.x + x0, center.y, center.z + z0).color(new RenderColor(baseColor)).next();
|
||||||
|
int i2 = mesh.vec3(center.x + x1, center.y, center.z + z1).color(new RenderColor(baseColor)).next();
|
||||||
|
int i3 = mesh.vec3(center.x + x1, center.y + height, center.z + z1).color(new RenderColor(topColor)).next();
|
||||||
|
int i4 = mesh.vec3(center.x + x0, center.y + height, center.z + z0).color(new RenderColor(topColor)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,15 @@ public final class HoldMyItems extends Module {
|
||||||
public final NumberValue<Float> driftStrength = visibleWhen(num("drift_strength", 1.0f, 0.0f, 2.0f), useDrift::get);
|
public final NumberValue<Float> driftStrength = visibleWhen(num("drift_strength", 1.0f, 0.0f, 2.0f), useDrift::get);
|
||||||
public final BooleanValue joinAppear = bool("join_appear", true);
|
public final BooleanValue joinAppear = bool("join_appear", true);
|
||||||
|
|
||||||
|
public final BooleanValue gripEnabled = bool("grip_enabled", true);
|
||||||
|
public final NumberValue<Float> gripExtend = visibleWhen(num("grip_extend", 0.3f, -0.3f, 1.2f), gripEnabled::get);
|
||||||
|
public final NumberValue<Float> gripSlide = visibleWhen(num("grip_slide", 0.1f, -0.2f, 0.4f), gripEnabled::get);
|
||||||
|
public final NumberValue<Float> gripOut = visibleWhen(num("grip_out", 0.05f, -0.25f, 0.25f), gripEnabled::get);
|
||||||
|
public final NumberValue<Float> gripForward = visibleWhen(num("grip_forward", 0.14f, -0.2f, 0.5f), gripEnabled::get);
|
||||||
|
public final NumberValue<Float> gripTilt = visibleWhen(num("grip_tilt", 0.0f, -90.0f, 90.0f), gripEnabled::get);
|
||||||
|
public final NumberValue<Float> gripTurn = visibleWhen(num("grip_turn", 0.0f, -90.0f, 90.0f), gripEnabled::get);
|
||||||
|
public final NumberValue<Float> gripRoll = visibleWhen(num("grip_roll", 0.0f, -180.0f, 180.0f), gripEnabled::get);
|
||||||
|
|
||||||
public HandAnimState handAnim() {
|
public HandAnimState handAnim() {
|
||||||
return handAnim;
|
return handAnim;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package dev.loki.lovisual.features.module.modules.visuals.optimization;
|
package dev.loki.lovisual.features.module.modules.visuals.optimization;
|
||||||
|
|
||||||
|
import net.minecraft.core.particles.ParticleTypes;
|
||||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
import dev.loki.lovisual.events.impl.render.ParticleSpawnEvent;
|
import dev.loki.lovisual.events.impl.render.ParticleSpawnEvent;
|
||||||
|
|
@ -65,6 +66,11 @@ public final class FpsOptimizer extends Module {
|
||||||
@EventHandler
|
@EventHandler
|
||||||
private void onParticleSpawn(ParticleSpawnEvent event) {
|
private void onParticleSpawn(ParticleSpawnEvent event) {
|
||||||
if (!isEnabled() || !disableParticles.get()) return;
|
if (!isEnabled() || !disableParticles.get()) return;
|
||||||
|
// Firework particles are created by FireworkParticles$Starter and assume ParticleEngine.createParticle never returns null.
|
||||||
|
// Cancelling them returns null and causes NPE at Starter.createParticle -> setTrail().
|
||||||
|
// Guard is also present in FireworkParticlesStarterMixin, but exempt here to preserve fireworks even with optimizer enabled.
|
||||||
|
var type = event.options().getType();
|
||||||
|
if (type == ParticleTypes.FIREWORK || type == ParticleTypes.FLASH) return;
|
||||||
event.cancel();
|
event.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
/*
|
||||||
|
* 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.particles;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||||
|
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Атмосферные частицы мира: мягко плывущие вверх светящиеся мошки ("Ambient")
|
||||||
|
* или странствующие светлячки со шлейфом ("Firefly", идея — Soup
|
||||||
|
* particles/types/FireFlyParticle.java, переписана без trail-объектов на
|
||||||
|
* кольцевой буфер позиций). Референс AmbientParticles.java (775 строк, 19
|
||||||
|
* визуальных стилей Stars/Hearts/Bloom/.../Pyramid) не портирован целиком —
|
||||||
|
* это asset-тяжёлый фича-крип чит-клиента; взята только атмосферная идея
|
||||||
|
* (мошки + светлячки), под наши текстуры и правила.
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "ambientparticles",
|
||||||
|
displayName = "AmbientParticles",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Атмосферные частицы мира: плывущие мошки или странствующие светлячки со шлейфом"
|
||||||
|
)
|
||||||
|
public final class AmbientParticles extends Module {
|
||||||
|
|
||||||
|
private static final int TRAIL_CAPACITY = 60;
|
||||||
|
private static final float MOTE_LIFE_MS = 6000f;
|
||||||
|
|
||||||
|
private final EnumValue<ParticleMode> mode = enumMode("mode", ParticleMode.AMBIENT, ParticleMode.values());
|
||||||
|
private final NumberValue<Integer> count = num("count", 60, 10, 200);
|
||||||
|
private final NumberValue<Float> scale = num("scale", 1.0f, 0.3f, 3.0f);
|
||||||
|
private final NumberValue<Float> spawnRadius = num("spawn_radius", 25.0f, 10.0f, 40.0f);
|
||||||
|
private final NumberValue<Float> spawnHeight = num("spawn_height", 6.0f, 2.0f, 20.0f);
|
||||||
|
private final NumberValue<Integer> trailLength = visibleWhen(num("trail_length", 20, 5, 60), this::isFirefly);
|
||||||
|
|
||||||
|
private final ModeValue colorMode = modeSetting("colorMode", "color_mode", "Static", AnimatedColorModeOptions.OPTIONS);
|
||||||
|
private final NumberValue<Integer> colorSpeed = num("color_speed", 18, 2, 54);
|
||||||
|
private final RGBAColorValue colorValue = color("color", "#FFFFE6A0");
|
||||||
|
private final RGBAColorValue colorValue2 = visibleWhen(color("color2", "#FF89C4FF"), this::usesSecondaryColor);
|
||||||
|
|
||||||
|
private final List<Mote> motes = new ArrayList<>();
|
||||||
|
private final List<Firefly> fireflies = new ArrayList<>();
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
private boolean isFirefly() {
|
||||||
|
return mode.get() == ParticleMode.FIREFLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean usesSecondaryColor() {
|
||||||
|
return AnimatedRenderColors.usesSecondary(AnimatedColorModeOptions.parse(colorMode.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveColorArgb(int phase) {
|
||||||
|
return AnimatedRenderColors.resolve(
|
||||||
|
AnimatedColorModeOptions.parse(colorMode.get()), colorSpeed.get(), phase,
|
||||||
|
colorValue.getArgb(), colorValue2.getArgb());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
motes.clear();
|
||||||
|
fireflies.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
motes.clear();
|
||||||
|
fireflies.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.END_MAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (!isEnabled() || mc.player == null || mc.level == null) return;
|
||||||
|
|
||||||
|
Vec3 center = mc.player.position();
|
||||||
|
if (isFirefly()) {
|
||||||
|
fireflies.removeIf(f -> f.age <= 0);
|
||||||
|
spawnFireflies(center);
|
||||||
|
for (Firefly f : fireflies) f.update(random);
|
||||||
|
renderFireflies(renderer);
|
||||||
|
} else {
|
||||||
|
motes.removeIf(m -> System.currentTimeMillis() - m.spawnMs > MOTE_LIFE_MS);
|
||||||
|
spawnMotes(center);
|
||||||
|
for (Mote m : motes) m.update();
|
||||||
|
renderMotes(renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnMotes(Vec3 center) {
|
||||||
|
int target = count.get();
|
||||||
|
while (motes.size() < target) {
|
||||||
|
double angle = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double dist = Math.sqrt(random.nextDouble()) * spawnRadius.get();
|
||||||
|
double x = center.x + Math.cos(angle) * dist;
|
||||||
|
double z = center.z + Math.sin(angle) * dist;
|
||||||
|
double y = center.y + random.nextDouble() * spawnHeight.get();
|
||||||
|
motes.add(new Mote(x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnFireflies(Vec3 center) {
|
||||||
|
int target = count.get() / 2;
|
||||||
|
while (fireflies.size() < target) {
|
||||||
|
double angle = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double dist = Math.sqrt(random.nextDouble()) * spawnRadius.get();
|
||||||
|
double x = center.x + Math.cos(angle) * dist;
|
||||||
|
double z = center.z + Math.sin(angle) * dist;
|
||||||
|
double y = center.y + random.nextDouble() * spawnHeight.get();
|
||||||
|
fireflies.add(new Firefly(x, y, z, trailLength.get(), random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMotes(Renderer3D renderer) {
|
||||||
|
if (motes.isEmpty()) return;
|
||||||
|
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, TextureStorage.FIRE_FLY, Renderer3D.DepthMode.MAIN);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
Quaternionf camRot = RenderState.cameraRotation;
|
||||||
|
for (Mote m : motes) {
|
||||||
|
float progress = (float) ((System.currentTimeMillis() - m.spawnMs) / MOTE_LIFE_MS);
|
||||||
|
float fade = (float) Math.sin(Math.min(1.0, progress) * Math.PI);
|
||||||
|
if (fade <= 0.01f) continue;
|
||||||
|
int argb = applyOpacity(resolveColorArgb((int) (m.x * 10)), fade);
|
||||||
|
addBillboard(mesh, m.x, m.y, m.z, scale.get() * 0.35f, camRot, argb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderFireflies(Renderer3D renderer) {
|
||||||
|
if (fireflies.isEmpty()) return;
|
||||||
|
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, TextureStorage.FIRE_FLY, Renderer3D.DepthMode.MAIN);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
Quaternionf camRot = RenderState.cameraRotation;
|
||||||
|
for (Firefly f : fireflies) {
|
||||||
|
float bodyFade = Mth.clamp(f.age / (float) f.maxAge, 0f, 1f);
|
||||||
|
int size = f.trailSize();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
float trailFade = (i + 1f) / size;
|
||||||
|
float alpha = bodyFade * trailFade;
|
||||||
|
if (alpha <= 0.02f) continue;
|
||||||
|
int argb = applyOpacity(resolveColorArgb(f.hashCode() % 360), alpha);
|
||||||
|
double[] p = f.trailPoint(i);
|
||||||
|
addBillboard(mesh, p[0], p[1], p[2], scale.get() * (0.12f + 0.08f * trailFade), camRot, argb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addBillboard(MeshBuilder mesh, double cx, double cy, double cz,
|
||||||
|
float size, Quaternionf camRot, int argb) {
|
||||||
|
Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
|
||||||
|
Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(cx - right.x() - up.x(), cy - right.y() - up.y(), cz - right.z() - up.z()).vec2(0, 1).color(new RenderColor(argb)).next();
|
||||||
|
int i2 = mesh.vec3(cx + right.x() - up.x(), cy + right.y() - up.y(), cz + right.z() - up.z()).vec2(1, 1).color(new RenderColor(argb)).next();
|
||||||
|
int i3 = mesh.vec3(cx + right.x() + up.x(), cy + right.y() + up.y(), cz + right.z() + up.z()).vec2(1, 0).color(new RenderColor(argb)).next();
|
||||||
|
int i4 = mesh.vec3(cx - right.x() + up.x(), cy - right.y() + up.y(), cz - right.z() + up.z()).vec2(0, 0).color(new RenderColor(argb)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int applyOpacity(int argb, float opacity) {
|
||||||
|
int a = (argb >>> 24) & 0xFF;
|
||||||
|
int na = Mth.clamp((int) (a * Mth.clamp(opacity, 0f, 1f)), 0, 255);
|
||||||
|
return (na << 24) | (argb & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ParticleMode implements EnumValue.IdProvider {
|
||||||
|
AMBIENT("ambient"),
|
||||||
|
FIREFLY("firefly");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
ParticleMode(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Мошка: плывёт вверх, слегка виляя, живёт {@link #MOTE_LIFE_MS} и гаснет. */
|
||||||
|
private static final class Mote {
|
||||||
|
double x, y, z;
|
||||||
|
final long spawnMs;
|
||||||
|
final double phase;
|
||||||
|
|
||||||
|
Mote(double x, double y, double z) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.spawnMs = System.currentTimeMillis();
|
||||||
|
this.phase = Math.random() * Math.PI * 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
double t = (System.currentTimeMillis() - spawnMs) / 1000.0;
|
||||||
|
y += 0.004;
|
||||||
|
x += Math.sin(t * 0.6 + phase) * 0.003;
|
||||||
|
z += Math.cos(t * 0.5 + phase) * 0.003;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Светлячок: случайное блуждание с затуханием скорости и кольцевым буфером шлейфа. */
|
||||||
|
private static final class Firefly {
|
||||||
|
double x, y, z;
|
||||||
|
double vx, vy, vz;
|
||||||
|
int age;
|
||||||
|
final int maxAge;
|
||||||
|
private final double[] trailX;
|
||||||
|
private final double[] trailY;
|
||||||
|
private final double[] trailZ;
|
||||||
|
private int trailCount;
|
||||||
|
private int trailHead;
|
||||||
|
|
||||||
|
Firefly(double x, double y, double z, int trailLength, Random random) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.vx = (random.nextDouble() * 2.0 - 1.0) * 0.02;
|
||||||
|
this.vy = (random.nextDouble() * 2.0 - 1.0) * 0.01;
|
||||||
|
this.vz = (random.nextDouble() * 2.0 - 1.0) * 0.02;
|
||||||
|
this.age = 100 + random.nextInt(200);
|
||||||
|
this.maxAge = this.age;
|
||||||
|
int cap = Mth.clamp(trailLength, 1, TRAIL_CAPACITY);
|
||||||
|
this.trailX = new double[cap];
|
||||||
|
this.trailY = new double[cap];
|
||||||
|
this.trailZ = new double[cap];
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Random random) {
|
||||||
|
age--;
|
||||||
|
x += vx;
|
||||||
|
y += vy;
|
||||||
|
z += vz;
|
||||||
|
vx += (random.nextDouble() * 2.0 - 1.0) * 0.004;
|
||||||
|
vy += (random.nextDouble() * 2.0 - 1.0) * 0.002;
|
||||||
|
vz += (random.nextDouble() * 2.0 - 1.0) * 0.004;
|
||||||
|
vx *= 0.97;
|
||||||
|
vy *= 0.97;
|
||||||
|
vz *= 0.97;
|
||||||
|
|
||||||
|
trailX[trailHead] = x;
|
||||||
|
trailY[trailHead] = y;
|
||||||
|
trailZ[trailHead] = z;
|
||||||
|
trailHead = (trailHead + 1) % trailX.length;
|
||||||
|
if (trailCount < trailX.length) trailCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int trailSize() {
|
||||||
|
return trailCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] trailPoint(int ageIndex) {
|
||||||
|
int idx = Math.floorMod(trailHead - 1 - ageIndex, trailX.length);
|
||||||
|
return new double[]{trailX[idx], trailY[idx], trailZ[idx]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.module.modules.visuals.scene;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.util.time.GameClock;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кастомизация рендера энд-кристаллов: цвет стекла/куба (см.
|
||||||
|
* {@code EndCrystalRendererMixin}) и осколки-вспышка на месте разрушения
|
||||||
|
* (кристалл исчезает из мира мгновенно — ваниль не даёт доиграть анимацию).
|
||||||
|
* Референс: Soup other/EndCrystal.java — идея взята (тонировка модели,
|
||||||
|
* персистентный эффект поломки), код не копировался. Гранулярное разбиение
|
||||||
|
* на "Outer Glass"/"Inner Glass" референса условно: ванильная модель зовёт
|
||||||
|
* один и тот же {@code ModelPart glass} дважды подряд (внешний и вложенный
|
||||||
|
* повёрнутый куб) — мы различаем эти два вызова по порядку в миксине.
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "endcrystal",
|
||||||
|
displayName = "EndCrystal",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Цвет модели энд-кристалла и эффект осколков при разрушении"
|
||||||
|
)
|
||||||
|
public final class EndCrystal extends Module {
|
||||||
|
|
||||||
|
private static final int SHARD_COUNT = 14;
|
||||||
|
private static final float SHARD_SPEED = 0.16f;
|
||||||
|
|
||||||
|
private final ModeValue colorMode = modeSetting("colorMode", "color_mode", "Sync", "Sync", "Custom");
|
||||||
|
private final NumberValue<Integer> colorSpeed = num("color_speed", 18, 2, 54);
|
||||||
|
private final RGBAColorValue colorValue = visibleWhen(color("color", "#FF7FE0D8"), this::isCustomColor);
|
||||||
|
private final RGBAColorValue colorValue2 = visibleWhen(color("color2", "#FFB57FE0"), () -> isCustomColor() && usesSecondaryColor());
|
||||||
|
private final ModeValue colorAnimation = visibleWhen(
|
||||||
|
modeSetting("colorAnimation", "color_animation", "Static", AnimatedColorModeOptions.OPTIONS), this::isCustomColor);
|
||||||
|
|
||||||
|
public final BooleanValue outerGlassVisible = bool("outer_glass", true);
|
||||||
|
public final BooleanValue innerGlassVisible = bool("inner_glass", true);
|
||||||
|
public final BooleanValue cubeVisible = bool("cube", true);
|
||||||
|
|
||||||
|
private final BooleanValue shatterEnabled = bool("shatter_enabled", true);
|
||||||
|
private final NumberValue<Integer> shatterDurationMs = visibleWhen(num("shatter_duration_ms", 600, 200, 2000), shatterEnabled::get);
|
||||||
|
|
||||||
|
private final List<Shard> shards = new ArrayList<>();
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
private boolean isCustomColor() {
|
||||||
|
return "Custom".equals(colorMode.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean usesSecondaryColor() {
|
||||||
|
return AnimatedRenderColors.usesSecondary(AnimatedColorModeOptions.parse(colorAnimation.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return тонировка модели (ARGB), либо -1 (без тонировки — ванильный белый), если Sync. */
|
||||||
|
public int resolveTintArgb() {
|
||||||
|
if (!isCustomColor()) return -1;
|
||||||
|
return AnimatedRenderColors.resolve(
|
||||||
|
AnimatedColorModeOptions.parse(colorAnimation.get()), colorSpeed.get(), 0,
|
||||||
|
colorValue.getArgb(), colorValue2.getArgb());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Вызывается миксином при разрушении кристалла (RemovalReason#shouldDestroy). */
|
||||||
|
public void onCrystalDestroyed(Vec3 pos) {
|
||||||
|
if (!isEnabled() || !shatterEnabled.get()) return;
|
||||||
|
|
||||||
|
int color = resolveTintArgb();
|
||||||
|
if (color == -1) color = 0xFF9CF0E8;
|
||||||
|
|
||||||
|
long now = GameClock.millis();
|
||||||
|
for (int i = 0; i < SHARD_COUNT; i++) {
|
||||||
|
double yaw = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double pitch = random.nextDouble() * Math.PI - Math.PI / 2.0;
|
||||||
|
double speed = SHARD_SPEED * (0.5 + random.nextDouble());
|
||||||
|
Vec3 velocity = new Vec3(
|
||||||
|
Math.cos(yaw) * Math.cos(pitch),
|
||||||
|
Math.sin(pitch),
|
||||||
|
Math.sin(yaw) * Math.cos(pitch)
|
||||||
|
).scale(speed);
|
||||||
|
shards.add(new Shard(pos, velocity, now, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
shards.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.END_MAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (!isEnabled() || shards.isEmpty()) return;
|
||||||
|
|
||||||
|
long durationMs = Math.max(1, shatterDurationMs.get());
|
||||||
|
long now = GameClock.millis();
|
||||||
|
shards.removeIf(s -> now - s.startMs > durationMs);
|
||||||
|
if (shards.isEmpty()) return;
|
||||||
|
|
||||||
|
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, TextureStorage.PARTICLE_STARS, Renderer3D.DepthMode.MAIN);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
Quaternionf camRot = RenderState.cameraRotation;
|
||||||
|
for (Shard s : shards) {
|
||||||
|
float progress = Mth.clamp((now - s.startMs) / (float) durationMs, 0f, 1f);
|
||||||
|
float fade = 1.0f - progress;
|
||||||
|
if (fade <= 0.02f) continue;
|
||||||
|
|
||||||
|
double dt = (now - s.startMs) / 1000.0;
|
||||||
|
double x = s.origin.x + s.velocity.x * dt;
|
||||||
|
double y = s.origin.y + s.velocity.y * dt - 0.35 * dt * dt;
|
||||||
|
double z = s.origin.z + s.velocity.z * dt;
|
||||||
|
|
||||||
|
float size = 0.22f * fade;
|
||||||
|
int argb = applyOpacity(s.colorArgb, fade);
|
||||||
|
addBillboard(mesh, x, y, z, size, camRot, argb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addBillboard(MeshBuilder mesh, double cx, double cy, double cz,
|
||||||
|
float size, Quaternionf camRot, int argb) {
|
||||||
|
Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
|
||||||
|
Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(cx - right.x() - up.x(), cy - right.y() - up.y(), cz - right.z() - up.z()).vec2(0, 1).color(new RenderColor(argb)).next();
|
||||||
|
int i2 = mesh.vec3(cx + right.x() - up.x(), cy + right.y() - up.y(), cz + right.z() - up.z()).vec2(1, 1).color(new RenderColor(argb)).next();
|
||||||
|
int i3 = mesh.vec3(cx + right.x() + up.x(), cy + right.y() + up.y(), cz + right.z() + up.z()).vec2(1, 0).color(new RenderColor(argb)).next();
|
||||||
|
int i4 = mesh.vec3(cx - right.x() + up.x(), cy - right.y() + up.y(), cz - right.z() + up.z()).vec2(0, 0).color(new RenderColor(argb)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int applyOpacity(int argb, float opacity) {
|
||||||
|
int a = (argb >>> 24) & 0xFF;
|
||||||
|
int na = Mth.clamp((int) (a * Mth.clamp(opacity, 0f, 1f)), 0, 255);
|
||||||
|
return (na << 24) | (argb & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Shard(Vec3 origin, Vec3 velocity, long startMs, int colorArgb) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,14 +12,16 @@ import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
import dev.loki.lovisual.features.module.core.Module;
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.relations.PlayerRelations;
|
||||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
import dev.loki.lovisual.util.target.TargetManager;
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.player.AbstractClientPlayer;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
@ModuleInfo(
|
@ModuleInfo(
|
||||||
|
|
@ -64,68 +66,6 @@ public class TrollfaceMask extends Module {
|
||||||
if (mc.player == null || mc.level == null) return;
|
if (mc.player == null || mc.level == null) return;
|
||||||
|
|
||||||
String targetMode = target.get();
|
String targetMode = target.get();
|
||||||
boolean selfMode = "Себя".equals(targetMode);
|
|
||||||
|
|
||||||
LivingEntity currentTarget = TargetManager.getTarget();
|
|
||||||
boolean hasCrosshairTarget = currentTarget != null && currentTarget.isAlive();
|
|
||||||
|
|
||||||
if (!selfMode && !hasCrosshairTarget) return;
|
|
||||||
|
|
||||||
LivingEntity renderTarget;
|
|
||||||
boolean shouldRender;
|
|
||||||
|
|
||||||
switch (targetMode) {
|
|
||||||
case "Себя" -> {
|
|
||||||
renderTarget = mc.player;
|
|
||||||
shouldRender = true;
|
|
||||||
}
|
|
||||||
case "Друзей" -> {
|
|
||||||
if (!hasCrosshairTarget) { shouldRender = false; renderTarget = null; break; }
|
|
||||||
renderTarget = currentTarget;
|
|
||||||
shouldRender = true;
|
|
||||||
}
|
|
||||||
case "Всех" -> {
|
|
||||||
if (!hasCrosshairTarget) { shouldRender = false; renderTarget = null; break; }
|
|
||||||
renderTarget = currentTarget;
|
|
||||||
shouldRender = true;
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
renderTarget = mc.player;
|
|
||||||
shouldRender = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!shouldRender || renderTarget == null) return;
|
|
||||||
|
|
||||||
Vec3 camPos = mc.gameRenderer.mainCamera().position();
|
|
||||||
double headX = renderTarget.getX();
|
|
||||||
double headY = renderTarget.getY() + renderTarget.getBbHeight() * 0.72;
|
|
||||||
double headZ = renderTarget.getZ();
|
|
||||||
|
|
||||||
Vec3 headPos = new Vec3(headX, headY, headZ);
|
|
||||||
Vec3 viewDir = camPos.subtract(headPos).normalize();
|
|
||||||
|
|
||||||
if (viewDir.lengthSqr() < 1.0E-8) return;
|
|
||||||
|
|
||||||
Vec3 upVec = new Vec3(0, 1, 0);
|
|
||||||
Vec3 right = viewDir.cross(upVec).normalize();
|
|
||||||
if (right.lengthSqr() < 1.0E-8) {
|
|
||||||
right = new Vec3(1, 0, 0);
|
|
||||||
}
|
|
||||||
Vec3 actualUp = right.cross(viewDir).normalize();
|
|
||||||
|
|
||||||
float s = size.get();
|
|
||||||
float alpha = this.alpha.get();
|
|
||||||
int color = 0xFFFFFFFF;
|
|
||||||
int r = (color >> 16) & 0xFF;
|
|
||||||
int g = (color >> 8) & 0xFF;
|
|
||||||
int b = color & 0xFF;
|
|
||||||
int a = (int) (alpha * 255);
|
|
||||||
|
|
||||||
Vec3 p1 = headPos.add(right.scale(-s)).add(actualUp.scale(-s * 0.75));
|
|
||||||
Vec3 p2 = headPos.add(right.scale(s)).add(actualUp.scale(-s * 0.75));
|
|
||||||
Vec3 p3 = headPos.add(right.scale(s)).add(actualUp.scale(s * 0.75));
|
|
||||||
Vec3 p4 = headPos.add(right.scale(-s)).add(actualUp.scale(s * 0.75));
|
|
||||||
|
|
||||||
int textureIndex = maskTypeIndex(maskType.get());
|
int textureIndex = maskTypeIndex(maskType.get());
|
||||||
Identifier texId = switch (textureIndex) {
|
Identifier texId = switch (textureIndex) {
|
||||||
case 0 -> TextureStorage.TROLLFACE_1;
|
case 0 -> TextureStorage.TROLLFACE_1;
|
||||||
|
|
@ -137,14 +77,66 @@ public class TrollfaceMask extends Module {
|
||||||
default -> TextureStorage.TROLLFACE_1;
|
default -> TextureStorage.TROLLFACE_1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (Player player : mc.level.players()) {
|
||||||
|
if (!shouldMask(player, mc.player, targetMode)) continue;
|
||||||
|
renderMask(renderer, player, tickDelta, texId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldMask(Player candidate, Player self, String targetMode) {
|
||||||
|
if (!candidate.isAlive()) return false;
|
||||||
|
return switch (targetMode) {
|
||||||
|
case "Себя" -> candidate == self;
|
||||||
|
case "Друзей" -> candidate != self
|
||||||
|
&& candidate instanceof AbstractClientPlayer
|
||||||
|
&& PlayerRelations.get().isFriend(candidate.getGameProfile().name());
|
||||||
|
case "Всех" -> true;
|
||||||
|
default -> candidate == self;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Маска рисуется в плоскости лица цели (yaw/pitch головы), а не биллбордом к камере:
|
||||||
|
// биллборд к камере вырождается для себя от первого лица (камера почти совпадает с головой
|
||||||
|
// по вертикали -> вектор view~(0,1,0) -> right=view×up=0 -> плоскость ложится горизонтально
|
||||||
|
// и не видна). Ориентация по взгляду цели работает одинаково для себя/друзей/всех.
|
||||||
|
private void renderMask(Renderer3D renderer, LivingEntity renderTarget, float tickDelta, Identifier texId) {
|
||||||
|
double headX = renderTarget.getX();
|
||||||
|
double headY = renderTarget.getY() + renderTarget.getBbHeight() * 0.72;
|
||||||
|
double headZ = renderTarget.getZ();
|
||||||
|
Vec3 headPos = new Vec3(headX, headY, headZ);
|
||||||
|
|
||||||
|
Vec3 forward = renderTarget.getViewVector(tickDelta);
|
||||||
|
if (forward.lengthSqr() < 1.0E-8) return;
|
||||||
|
forward = forward.normalize();
|
||||||
|
|
||||||
|
Vec3 worldUp = new Vec3(0, 1, 0);
|
||||||
|
Vec3 right = forward.cross(worldUp).normalize();
|
||||||
|
if (right.lengthSqr() < 1.0E-8) {
|
||||||
|
right = new Vec3(1, 0, 0);
|
||||||
|
}
|
||||||
|
Vec3 actualUp = right.cross(forward).normalize();
|
||||||
|
|
||||||
|
// Маска висит чуть впереди лица вдоль направления взгляда, иначе z-fight с головой.
|
||||||
|
headPos = headPos.add(forward.scale(0.35));
|
||||||
|
|
||||||
|
float s = size.get();
|
||||||
|
float alpha = this.alpha.get();
|
||||||
|
int r = 255, g = 255, b = 255;
|
||||||
|
int a = (int) (alpha * 255);
|
||||||
|
|
||||||
|
Vec3 p1 = headPos.add(right.scale(-s)).add(actualUp.scale(-s * 0.75));
|
||||||
|
Vec3 p2 = headPos.add(right.scale(s)).add(actualUp.scale(-s * 0.75));
|
||||||
|
Vec3 p3 = headPos.add(right.scale(s)).add(actualUp.scale(s * 0.75));
|
||||||
|
Vec3 p4 = headPos.add(right.scale(-s)).add(actualUp.scale(s * 0.75));
|
||||||
|
|
||||||
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED, texId, Renderer3D.DepthMode.NONE);
|
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED, texId, Renderer3D.DepthMode.NONE);
|
||||||
if (mesh == null) return;
|
if (mesh == null) return;
|
||||||
|
|
||||||
mesh.ensureQuadCapacity();
|
mesh.ensureQuadCapacity();
|
||||||
int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 1).color(r, g, b, a).next();
|
int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(1, 1).color(r, g, b, a).next();
|
||||||
int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 1).color(r, g, b, a).next();
|
int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(0, 1).color(r, g, b, a).next();
|
||||||
int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 0).color(r, g, b, a).next();
|
int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(0, 0).color(r, g, b, a).next();
|
||||||
int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 0).color(r, g, b, a).next();
|
int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(1, 0).color(r, g, b, a).next();
|
||||||
mesh.quad(i1, i2, i3, i4);
|
mesh.quad(i1, i2, i3, i4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -448,6 +448,14 @@ public final class Rain extends Module implements PostProcessPass {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int binarySteps() {
|
||||||
|
return switch (reflectionQuality.get()) {
|
||||||
|
case HIGH -> 6;
|
||||||
|
case BALANCED -> 4;
|
||||||
|
case PERFORMANCE -> 3;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private float rainAmount() {
|
private float rainAmount() {
|
||||||
return Mth.clamp(0.55f + density.get() * presetDensity() * 0.28f, 0.55f, 1.45f);
|
return Mth.clamp(0.55f + density.get() * presetDensity() * 0.28f, 0.55f, 1.45f);
|
||||||
}
|
}
|
||||||
|
|
@ -524,7 +532,9 @@ public final class Rain extends Module implements PostProcessPass {
|
||||||
reflectionDistance.get(),
|
reflectionDistance.get(),
|
||||||
reflectionSteps(),
|
reflectionSteps(),
|
||||||
rainAmount(),
|
rainAmount(),
|
||||||
puddleRipples.get()
|
puddleRipples.get(),
|
||||||
|
binarySteps(),
|
||||||
|
0.18f
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* 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.weather;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.impl.world.liquid.WetSurfaceUniforms;
|
||||||
|
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Мочит все подходящие (смотрящие вверх) поверхности мира по факту реальной погоды сервера —
|
||||||
|
* в отличие от {@link Rain}, не зависит от партиклов дождя и её собственного тумблера
|
||||||
|
* {@code wet_ground}: работает как отдельный лёгкий "always-on if raining" эффект. Использует
|
||||||
|
* тот же SSR-шейдер (wet_surface.frag) и тот же {@link WetSurfaceUniforms}, что и Rain — если
|
||||||
|
* включены оба модуля одновременно, эффект отражений применится дважды подряд (стек), поэтому
|
||||||
|
* рекомендуется держать выключенным {@code Rain.wet_ground}, когда включён WetWorld.
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "wet_world",
|
||||||
|
displayName = "Wet World",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Мокрые поверхности мира с отражениями по факту дождя на сервере"
|
||||||
|
)
|
||||||
|
public final class WetWorld extends Module implements PostProcessPass {
|
||||||
|
|
||||||
|
private final Matrix4f projection = new Matrix4f();
|
||||||
|
private final Matrix4f invView = new Matrix4f();
|
||||||
|
private boolean depthSupported = true;
|
||||||
|
private float clock;
|
||||||
|
private long lastFrameNanos;
|
||||||
|
|
||||||
|
{
|
||||||
|
PostProcessManager.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final EnumValue<Quality> quality = enumMode("quality", Quality.HIGH, Quality.values());
|
||||||
|
private final NumberValue<Float> reflectionStrength = num("reflection_strength", 0.75f, 0.1f, 1.5f);
|
||||||
|
private final NumberValue<Float> darkening = num("darkening", 0.30f, 0.0f, 0.8f);
|
||||||
|
private final BooleanValue ripples = bool("ripples", true);
|
||||||
|
private final NumberValue<Float> rippleSpeed = visibleWhen(num("ripple_speed", 1.0f, 0.1f, 3.0f), ripples::get);
|
||||||
|
private final BooleanValue rainOnly = bool("rain_only", true);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
depthSupported = true;
|
||||||
|
lastFrameNanos = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int reflectionSteps() {
|
||||||
|
return switch (quality.get()) {
|
||||||
|
case LOW -> 16;
|
||||||
|
case MEDIUM -> 28;
|
||||||
|
case HIGH -> 44;
|
||||||
|
case ULTRA -> 64;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int binarySteps() {
|
||||||
|
return switch (quality.get()) {
|
||||||
|
case LOW -> 2;
|
||||||
|
case MEDIUM -> 3;
|
||||||
|
case HIGH -> 5;
|
||||||
|
case ULTRA -> 7;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float maxDistance() {
|
||||||
|
return switch (quality.get()) {
|
||||||
|
case LOW -> 24.0f;
|
||||||
|
case MEDIUM -> 32.0f;
|
||||||
|
case HIGH -> 48.0f;
|
||||||
|
case ULTRA -> 64.0f;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float rainGradient(Minecraft mc) {
|
||||||
|
try {
|
||||||
|
return mc.level.getRainLevel(1.0f);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return mc.level.isRaining() ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (!isEnabled() || !depthSupported || mc.player == null || mc.level == null) return false;
|
||||||
|
return !rainOnly.get() || rainGradient(mc) > 0.01f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Phase getPhase() {
|
||||||
|
return Phase.PRE_HAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) {
|
||||||
|
if (!isActive() || context == null || src == null || dst == null) return false;
|
||||||
|
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (lastFrameNanos == 0L) lastFrameNanos = now;
|
||||||
|
clock += (float) Mth.clamp((now - lastFrameNanos) / 1_000_000_000.0, 0.0, 0.05);
|
||||||
|
lastFrameNanos = now;
|
||||||
|
|
||||||
|
GpuTextureView mainDepth = context.mainDepth();
|
||||||
|
boolean hasDepth = mainDepth != null;
|
||||||
|
if (!hasDepth) mainDepth = src;
|
||||||
|
|
||||||
|
Matrix4f proj = LoVisualWorldMatrices.renderProjectionMatrix();
|
||||||
|
if (proj == null) proj = RenderState.worldProjection;
|
||||||
|
projection.set(proj);
|
||||||
|
invView.set(new Matrix4f().rotation(RenderState.cameraRotation));
|
||||||
|
|
||||||
|
float rain = rainGradient(mc);
|
||||||
|
float effectiveStrength = reflectionStrength.get() * (rainOnly.get() ? Mth.clamp(rain, 0.0f, 1.0f) : 1.0f);
|
||||||
|
|
||||||
|
WetSurfaceUniforms.update(
|
||||||
|
projection,
|
||||||
|
invView,
|
||||||
|
(float) RenderState.cameraPos.x, (float) RenderState.cameraPos.y, (float) RenderState.cameraPos.z,
|
||||||
|
context.width() > 0 ? 1.0f / context.width() : 0.0f,
|
||||||
|
context.height() > 0 ? 1.0f / context.height() : 0.0f,
|
||||||
|
clock,
|
||||||
|
hasDepth,
|
||||||
|
1.0f,
|
||||||
|
0.95f,
|
||||||
|
effectiveStrength,
|
||||||
|
ripples.get() ? 1.0f : 0.0f,
|
||||||
|
maxDistance(),
|
||||||
|
reflectionSteps(),
|
||||||
|
Mth.clamp(0.6f + rain * 0.4f, 0.6f, 1.0f),
|
||||||
|
ripples.get(),
|
||||||
|
binarySteps(),
|
||||||
|
darkening.get()
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
FullScreenRenderer.ensureInit();
|
||||||
|
FullScreenRenderer.begin("LoVisual WetWorld Pass")
|
||||||
|
.attachment(dst)
|
||||||
|
.pipeline(LoVisualRenderPipelines.WET_SURFACE)
|
||||||
|
.uniform("WetSurface", WetSurfaceUniforms.get())
|
||||||
|
.sampler("u_Texture", src, PostProcessManager.getSampler())
|
||||||
|
.sampler("u_MainDepth", mainDepth, PostProcessManager.getSampler())
|
||||||
|
.end();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
depthSupported = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Quality implements EnumValue.IdProvider {
|
||||||
|
LOW("low"),
|
||||||
|
MEDIUM("medium"),
|
||||||
|
HIGH("high"),
|
||||||
|
ULTRA("ultra");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
Quality(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* 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.world;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.phase.HudPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||||
|
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||||
|
import dev.loki.lovisual.render.helpers.clip.ScreenProjection;
|
||||||
|
import dev.loki.lovisual.util.world.GpsBearing;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Компас-стрелка к заданной точке мира (луч/маркер направления, референс —
|
||||||
|
* идея Soup Gps): всегда видимая стрелка на экране, доворачивающаяся к цели
|
||||||
|
* по мере поворота камеры, плюс маркер прямо на месте, когда точка попадает
|
||||||
|
* в кадр. Цель ставится командой {@code %gps <x> <y> <z>} / {@code %gps clear}
|
||||||
|
* (см. {@link dev.loki.lovisual.features.command.impl.world.GpsCommand}).
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "gps",
|
||||||
|
displayName = "Gps",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Компас-стрелка и маркер к заданной точке мира"
|
||||||
|
)
|
||||||
|
public final class Gps extends Module {
|
||||||
|
|
||||||
|
private static final float ARROW_LENGTH_RATIO = 1.6f;
|
||||||
|
private static final int SCREEN_MARGIN = 24;
|
||||||
|
|
||||||
|
private volatile BlockPos target;
|
||||||
|
|
||||||
|
private final NumberValue<Float> arrowSize = num("arrow_size", 16.0f, 8.0f, 32.0f);
|
||||||
|
private final NumberValue<Float> markerSize = num("marker_size", 16.0f, 8.0f, 32.0f);
|
||||||
|
private final BooleanValue showDistance = bool("show_distance", true);
|
||||||
|
private final BooleanValue showLocationIcon = bool("show_location_icon", true);
|
||||||
|
private final RGBAColorValue arrowColor = color("color", "#FF55C8FF");
|
||||||
|
private final BooleanValue autoDelete = bool("auto_delete", false);
|
||||||
|
private final NumberValue<Float> autoDeleteDistance = visibleWhen(num("auto_delete_distance", 5.0f, 1.0f, 10.0f), autoDelete::get);
|
||||||
|
|
||||||
|
public void setTarget(BlockPos pos) {
|
||||||
|
this.target = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearTarget() {
|
||||||
|
this.target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockPos getTarget() {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
BlockPos current = target;
|
||||||
|
if (current == null || !autoDelete.get()) return;
|
||||||
|
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.player == null) return;
|
||||||
|
|
||||||
|
double dx = (current.getX() + 0.5) - mc.player.getX();
|
||||||
|
double dz = (current.getZ() + 0.5) - mc.player.getZ();
|
||||||
|
if (GpsBearing.horizontalDistance(dx, dz) <= autoDeleteDistance.get()) {
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HudPhase getHudPhase() {
|
||||||
|
return HudPhase.FIRST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) {
|
||||||
|
BlockPos current = target;
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (!isEnabled() || current == null || mc.player == null || mc.level == null || mc.getWindow() == null) return;
|
||||||
|
|
||||||
|
double dx = (current.getX() + 0.5) - mc.player.getX();
|
||||||
|
double dz = (current.getZ() + 0.5) - mc.player.getZ();
|
||||||
|
float bearing = GpsBearing.bearingDegrees(dx, dz);
|
||||||
|
float playerYaw = mc.player.getViewYRot(tickDelta);
|
||||||
|
float rotation = GpsBearing.arrowRotationDegrees(bearing, playerYaw);
|
||||||
|
double distance = GpsBearing.horizontalDistance(dx, dz);
|
||||||
|
int argb = arrowColor.getArgb();
|
||||||
|
|
||||||
|
float centerX = mc.getWindow().getGuiScaledWidth() / 2.0f;
|
||||||
|
float anchorY = mc.getWindow().getGuiScaledHeight() / 4.0f;
|
||||||
|
drawArrow(renderer, centerX, anchorY, rotation, argb);
|
||||||
|
|
||||||
|
if (showDistance.get()) {
|
||||||
|
drawDistanceLabel(textRenderer, centerX, anchorY + arrowSize.get() + 4.0f, distance, argb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLocationIcon.get()) {
|
||||||
|
drawLocationIconIfVisible(renderer, textRenderer, current, tickDelta, distance, argb, mc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawArrow(Renderer2D renderer, float anchorX, float anchorY, float rotationDeg, int argb) {
|
||||||
|
float size = arrowSize.get();
|
||||||
|
float half = size / 2.0f;
|
||||||
|
float length = size * ARROW_LENGTH_RATIO / 2.0f;
|
||||||
|
|
||||||
|
double rad = Math.toRadians(rotationDeg);
|
||||||
|
double cos = Math.cos(rad);
|
||||||
|
double sin = Math.sin(rad);
|
||||||
|
|
||||||
|
// Локальные вершины треугольника (нос вверх, назад по X): поворачиваем
|
||||||
|
// вокруг якоря на rotationDeg и переносим в экранные координаты.
|
||||||
|
double[] points = new double[]{
|
||||||
|
rotate(0, -length, cos, sin, anchorX, anchorY, true),
|
||||||
|
rotate(0, -length, cos, sin, anchorX, anchorY, false),
|
||||||
|
rotate(-half, half, cos, sin, anchorX, anchorY, true),
|
||||||
|
rotate(-half, half, cos, sin, anchorX, anchorY, false),
|
||||||
|
rotate(half, half, cos, sin, anchorX, anchorY, true),
|
||||||
|
rotate(half, half, cos, sin, anchorX, anchorY, false),
|
||||||
|
};
|
||||||
|
renderer.polygon(points, 3, argb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double rotate(double lx, double ly, double cos, double sin, double anchorX, double anchorY, boolean wantX) {
|
||||||
|
double rx = lx * cos - ly * sin;
|
||||||
|
double ry = lx * sin + ly * cos;
|
||||||
|
return wantX ? anchorX + rx : anchorY + ry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawDistanceLabel(TextRenderer textRenderer, float centerX, float y, double distance, int argb) {
|
||||||
|
String text = String.format(Locale.ROOT, "%.1fm", distance);
|
||||||
|
boolean wasBuilding = textRenderer.isBuilding();
|
||||||
|
if (!wasBuilding) textRenderer.begin();
|
||||||
|
double width = textRenderer.getWidth(text, true);
|
||||||
|
textRenderer.render(text, centerX - width / 2.0, y, new RenderColor(argb), true);
|
||||||
|
if (!wasBuilding) textRenderer.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLocationIconIfVisible(Renderer2D renderer, TextRenderer textRenderer, BlockPos pos,
|
||||||
|
float tickDelta, double distance, int argb, Minecraft mc) {
|
||||||
|
Vec3 world = new Vec3(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
|
||||||
|
Vec3 screen = ScreenProjection.worldToScreen(world, tickDelta);
|
||||||
|
if (screen == null || screen.z < 0.0 || screen.z > 1.0) return;
|
||||||
|
|
||||||
|
int width = mc.getWindow().getGuiScaledWidth();
|
||||||
|
int height = mc.getWindow().getGuiScaledHeight();
|
||||||
|
if (screen.x < -SCREEN_MARGIN || screen.x > width + SCREEN_MARGIN
|
||||||
|
|| screen.y < -SCREEN_MARGIN || screen.y > height + SCREEN_MARGIN) return;
|
||||||
|
|
||||||
|
float radius = markerSize.get() / 2.0f;
|
||||||
|
renderer.circle(screen.x, screen.y, radius, argb);
|
||||||
|
|
||||||
|
if (showDistance.get()) {
|
||||||
|
drawDistanceLabel(textRenderer, (float) screen.x, (float) screen.y + radius + 4.0f, distance, argb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,10 +16,12 @@ import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.Items;
|
import net.minecraft.world.item.Items;
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.slf4j.Logger;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.Unique;
|
import org.spongepowered.asm.mixin.Unique;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue;
|
import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue;
|
||||||
import dev.loki.lovisual.events.Events;
|
import dev.loki.lovisual.events.Events;
|
||||||
|
|
@ -150,6 +152,14 @@ public abstract class ClientPacketListenerMixin {
|
||||||
StaffTracker.INSTANCE.onEntityAppear(player.getUUID(), player.getGameProfile().name());
|
StaffTracker.INSTANCE.onEntityAppear(player.getUUID(), player.getGameProfile().name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Redirect(method = "handlePlayerInfoUpdate", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"))
|
||||||
|
private void lovisual$silenceUnknownPlayerWarn(Logger instance, String format, Object arg1, Object arg2) {
|
||||||
|
// Серверы HolyWorld/Funtime шлют UPDATE_DISPLAY_NAME для фейковых записей таба без ADD_PLAYER.
|
||||||
|
// Ванилла флудит WARN "Ignoring player info update for unknown player" десятки раз в тик.
|
||||||
|
// Глушим спам -> debug, чтобы лог остался читаемым. Полный update ниже всё равно трекается в lovisual$onPlayerList.
|
||||||
|
instance.debug(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
@Inject(method = "handlePlayerInfoUpdate", at = @At("TAIL"))
|
@Inject(method = "handlePlayerInfoUpdate", at = @At("TAIL"))
|
||||||
private void lovisual$onPlayerList(ClientboundPlayerInfoUpdatePacket packet, CallbackInfo ci) {
|
private void lovisual$onPlayerList(ClientboundPlayerInfoUpdatePacket packet, CallbackInfo ci) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import dev.loki.lovisual.features.module.modules.combat.damage.NoFriendDamage;
|
||||||
import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns;
|
import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns;
|
||||||
import dev.loki.lovisual.features.module.modules.misc.sound.HitSounds;
|
import dev.loki.lovisual.features.module.modules.misc.sound.HitSounds;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam;
|
import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam;
|
||||||
|
import dev.loki.lovisual.features.module.modules.visuals.effects.HitBubbles;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.effects.HitEffect;
|
import dev.loki.lovisual.features.module.modules.visuals.effects.HitEffect;
|
||||||
import dev.loki.lovisual.mixins.accessors.world.WorldAccessor;
|
import dev.loki.lovisual.mixins.accessors.world.WorldAccessor;
|
||||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||||
|
|
@ -167,6 +168,11 @@ public class MultiPlayerGameModeMixin {
|
||||||
hitEffect.handleHit(target);
|
hitEffect.handleHit(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HitBubbles hitBubbles = Modules.get(HitBubbles.class);
|
||||||
|
if (hitBubbles != null) {
|
||||||
|
hitBubbles.handleHit(target);
|
||||||
|
}
|
||||||
|
|
||||||
TargetHud.notifyHit(target);
|
TargetHud.notifyHit(target);
|
||||||
TargetManager.onAttack(target);
|
TargetManager.onAttack(target);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.mixins.render.entity;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||||
|
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||||
|
import dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal;
|
||||||
|
import net.minecraft.client.model.geom.ModelPart;
|
||||||
|
import net.minecraft.client.renderer.entity.EndCrystalRenderer;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тонировка стекла/куба энд-кристалла и скрытие отдельных частей модели по
|
||||||
|
* настройкам {@link EndCrystal}. Ванильный {@code render(...)} зовёт
|
||||||
|
* {@code this.glass.render(...)} дважды подряд (внешний куб, затем повёрнутый
|
||||||
|
* вложенный) и {@code this.cube.render(...)} один раз — различаем эти три
|
||||||
|
* вызова счётчиком {@link #lovisual$glassCallOrdinal}, сбрасываемым в начале
|
||||||
|
* каждого рендера кристалла.
|
||||||
|
*/
|
||||||
|
@Mixin(EndCrystalRenderer.class)
|
||||||
|
public abstract class EndCrystalRendererMixin {
|
||||||
|
|
||||||
|
@Shadow
|
||||||
|
private ModelPart glass;
|
||||||
|
@Shadow
|
||||||
|
private ModelPart cube;
|
||||||
|
|
||||||
|
@Unique
|
||||||
|
private int lovisual$glassCallOrdinal;
|
||||||
|
|
||||||
|
@Inject(
|
||||||
|
method = "render(Lnet/minecraft/world/entity/boss/enderdragon/EndCrystal;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
|
||||||
|
at = @At("HEAD")
|
||||||
|
)
|
||||||
|
private void lovisual$resetOrdinal(Object entity, float entityYaw, float partialTicks, PoseStack poseStack,
|
||||||
|
Object buffer, int packedLight, CallbackInfo ci) {
|
||||||
|
lovisual$glassCallOrdinal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapOperation(
|
||||||
|
method = "render(Lnet/minecraft/world/entity/boss/enderdragon/EndCrystal;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
|
||||||
|
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/geom/ModelPart;render(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;II)V")
|
||||||
|
)
|
||||||
|
private void lovisual$tintOrHideModelPart(ModelPart instance, PoseStack poseStack, VertexConsumer vertexConsumer,
|
||||||
|
int light, int overlay, Operation<Void> original) {
|
||||||
|
EndCrystal module = Modules.get(EndCrystal.class);
|
||||||
|
if (module == null || !module.isEnabled()) {
|
||||||
|
original.call(instance, poseStack, vertexConsumer, light, overlay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean visible;
|
||||||
|
if (instance == this.cube) {
|
||||||
|
visible = module.cubeVisible.get();
|
||||||
|
} else {
|
||||||
|
lovisual$glassCallOrdinal++;
|
||||||
|
visible = lovisual$glassCallOrdinal == 1 ? module.outerGlassVisible.get() : module.innerGlassVisible.get();
|
||||||
|
}
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
|
int tint = module.resolveTintArgb();
|
||||||
|
if (tint == -1) {
|
||||||
|
original.call(instance, poseStack, vertexConsumer, light, overlay);
|
||||||
|
} else {
|
||||||
|
instance.render(poseStack, vertexConsumer, light, overlay, tint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,7 +63,7 @@ public abstract class ItemInHandRendererMixin {
|
||||||
HandAnimState handAnim = holdMyItems.handAnim();
|
HandAnimState handAnim = holdMyItems.handAnim();
|
||||||
if (handAnim == null) return;
|
if (handAnim == null) return;
|
||||||
|
|
||||||
lovisual$handPosePushed = HandDynamics.apply(holdMyItems, handAnim, player, tickDelta, hand, matrices);
|
lovisual$handPosePushed = HandDynamics.apply(holdMyItems, handAnim, player, tickDelta, hand, item, matrices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject(method = "submitArmWithItem", at = @At("RETURN"))
|
@Inject(method = "submitArmWithItem", at = @At("RETURN"))
|
||||||
|
|
|
||||||
|
|
@ -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.mixins.render.level;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vanilla bug: FireworkParticles$Starter.createParticle assumes ParticleEngine.createParticle never returns null.
|
||||||
|
* Our ParticleManagerMixin returns null when a ParticleSpawnEvent is cancelled (e.g. FpsOptimizer.disable_particles).
|
||||||
|
* This causes NPE: sparkParticle is null -> setTrail() crashes.
|
||||||
|
* Fix: guard every SparkParticle setter to be null-safe. Use Object for private inner type.
|
||||||
|
*/
|
||||||
|
@Mixin(targets = "net.minecraft.client.particle.FireworkParticles$Starter")
|
||||||
|
public abstract class FireworkParticlesStarterMixin {
|
||||||
|
|
||||||
|
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setTrail(Z)V"))
|
||||||
|
private void lovisual$guardSetTrail(Object instance, boolean trail, Operation<Void> original) {
|
||||||
|
if (instance == null) return;
|
||||||
|
original.call(instance, trail);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setTwinkle(Z)V"))
|
||||||
|
private void lovisual$guardSetTwinkle(Object instance, boolean twinkle, Operation<Void> original) {
|
||||||
|
if (instance == null) return;
|
||||||
|
original.call(instance, twinkle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setAlpha(F)V"))
|
||||||
|
private void lovisual$guardSetAlpha(Object instance, float alpha, Operation<Void> original) {
|
||||||
|
if (instance == null) return;
|
||||||
|
original.call(instance, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setColor(I)V"))
|
||||||
|
private void lovisual$guardSetColor(Object instance, int color, Operation<Void> original) {
|
||||||
|
if (instance == null) return;
|
||||||
|
original.call(instance, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setFadeColor(I)V"))
|
||||||
|
private void lovisual$guardSetFadeColor(Object instance, int fadeColor, Operation<Void> original) {
|
||||||
|
if (instance == null) return;
|
||||||
|
original.call(instance, fadeColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ import net.minecraft.client.multiplayer.ClientLevel;
|
||||||
import net.minecraft.core.particles.ItemParticleOption;
|
import net.minecraft.core.particles.ItemParticleOption;
|
||||||
import net.minecraft.core.particles.ParticleOptions;
|
import net.minecraft.core.particles.ParticleOptions;
|
||||||
import net.minecraft.core.particles.ParticleTypes;
|
import net.minecraft.core.particles.ParticleTypes;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.boss.enderdragon.EndCrystal;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
|
@ -21,6 +23,20 @@ import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender;
|
||||||
@Mixin(ClientLevel.class)
|
@Mixin(ClientLevel.class)
|
||||||
public abstract class ClientLevelMixin {
|
public abstract class ClientLevelMixin {
|
||||||
|
|
||||||
|
@Inject(method = "removeEntity", at = @At("HEAD"))
|
||||||
|
private void lovisual$onEndCrystalRemoved(int entityId, Entity.RemovalReason reason, CallbackInfo ci) {
|
||||||
|
if (!reason.shouldDestroy()) return;
|
||||||
|
|
||||||
|
dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal module =
|
||||||
|
Modules.get(dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal.class);
|
||||||
|
if (module == null || !module.isEnabled()) return;
|
||||||
|
|
||||||
|
Entity entity = ((ClientLevel) (Object) this).getEntity(entityId);
|
||||||
|
if (entity instanceof EndCrystal crystal) {
|
||||||
|
module.onCrystalDestroyed(crystal.position());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Inject(
|
@Inject(
|
||||||
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
|
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
|
||||||
at = @At("HEAD"),
|
at = @At("HEAD"),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.animation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Общий набор строковых опций {@link ModeValue}-настройки цвета и парсер в
|
||||||
|
* {@link AnimatedRenderColors.Mode} — вынесено, чтобы каждый модуль с
|
||||||
|
* анимированным цветом не дублировал один и тот же switch (см. HitEffect,
|
||||||
|
* HitBubbles).
|
||||||
|
*/
|
||||||
|
public enum AnimatedColorModeOptions {
|
||||||
|
;
|
||||||
|
|
||||||
|
public static final String[] OPTIONS = {
|
||||||
|
"Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static AnimatedRenderColors.Mode parse(String value) {
|
||||||
|
return switch (value == null ? "" : value) {
|
||||||
|
case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW;
|
||||||
|
case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
|
||||||
|
case "Sky" -> AnimatedRenderColors.Mode.SKY;
|
||||||
|
case "Fade" -> AnimatedRenderColors.Mode.FADE;
|
||||||
|
case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
|
||||||
|
case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS;
|
||||||
|
case "Theme" -> AnimatedRenderColors.Mode.THEME;
|
||||||
|
default -> AnimatedRenderColors.Mode.STATIC;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -580,8 +580,17 @@ public final class MeshBuilder implements AutoCloseable {
|
||||||
+ indexBytes + ", capacity=" + indices.capacity() + ", format=" + format);
|
+ indexBytes + ", capacity=" + indices.capacity() + ", format=" + format);
|
||||||
}
|
}
|
||||||
if (primitiveIndicesCount > 0 && indicesCount % primitiveIndicesCount != 0) {
|
if (primitiveIndicesCount > 0 && indicesCount % primitiveIndicesCount != 0) {
|
||||||
|
if (DEBUG) {
|
||||||
throw new IllegalStateException(owner + ": MeshBuilder incomplete primitive payload: indices=" + indicesCount
|
throw new IllegalStateException(owner + ": MeshBuilder incomplete primitive payload: indices=" + indicesCount
|
||||||
+ ", primitiveLength=" + primitiveIndicesCount + ", format=" + format);
|
+ ", primitiveLength=" + primitiveIndicesCount + ", format=" + format);
|
||||||
|
} else {
|
||||||
|
int trimmed = (indicesCount / primitiveIndicesCount) * primitiveIndicesCount;
|
||||||
|
// Silence spam: log once per frame via frame stats? Use logger warn with throttling.
|
||||||
|
// Trim incomplete tail to avoid Render Frame crash (e.g. 176 % 3 == 2 on TRIANGLES).
|
||||||
|
org.slf4j.LoggerFactory.getLogger(MeshBuilder.class).warn("{}: trimming incomplete primitive payload: indices={} -> {} (primitiveLength={}, format={})", owner, indicesCount, trimmed, primitiveIndicesCount, format);
|
||||||
|
indicesCount = trimmed;
|
||||||
|
if (indicesCount == 0) return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
debugValidateIndices(owner);
|
debugValidateIndices(owner);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ public enum WetSurfaceUniforms {
|
||||||
.putFloat().putFloat().putFloat().putFloat()
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
.putFloat().putFloat().putFloat().putFloat()
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
.putFloat().putFloat().putFloat().putFloat()
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
private static final Data DATA = new Data();
|
private static final Data DATA = new Data();
|
||||||
|
|
@ -34,7 +35,8 @@ public enum WetSurfaceUniforms {
|
||||||
float camX, float camY, float camZ,
|
float camX, float camY, float camZ,
|
||||||
float invW, float invH, float time, boolean mainDepthEnabled,
|
float invW, float invH, float time, boolean mainDepthEnabled,
|
||||||
float wetness, float puddleCoverage, float reflectionStrength, float rippleStrength,
|
float wetness, float puddleCoverage, float reflectionStrength, float rippleStrength,
|
||||||
float maxDistance, int reflectionSteps, float rainAmount, boolean ripplesEnabled) {
|
float maxDistance, int reflectionSteps, float rainAmount, boolean ripplesEnabled,
|
||||||
|
int binarySteps, float darkeningFactor) {
|
||||||
DATA.projection.set(projection);
|
DATA.projection.set(projection);
|
||||||
DATA.invView.set(invView);
|
DATA.invView.set(invView);
|
||||||
DATA.camX = camX;
|
DATA.camX = camX;
|
||||||
|
|
@ -52,6 +54,8 @@ public enum WetSurfaceUniforms {
|
||||||
DATA.reflectionSteps = reflectionSteps;
|
DATA.reflectionSteps = reflectionSteps;
|
||||||
DATA.rainAmount = rainAmount;
|
DATA.rainAmount = rainAmount;
|
||||||
DATA.ripplesEnabled = ripplesEnabled ? 1.0f : 0.0f;
|
DATA.ripplesEnabled = ripplesEnabled ? 1.0f : 0.0f;
|
||||||
|
DATA.binarySteps = binarySteps;
|
||||||
|
DATA.darkeningFactor = darkeningFactor;
|
||||||
LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA);
|
LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,6 +70,7 @@ public enum WetSurfaceUniforms {
|
||||||
private float invW, invH, time, mainDepthEnabled;
|
private float invW, invH, time, mainDepthEnabled;
|
||||||
private float wetness, puddleCoverage, reflectionStrength, rippleStrength;
|
private float wetness, puddleCoverage, reflectionStrength, rippleStrength;
|
||||||
private float maxDistance, reflectionSteps, rainAmount, ripplesEnabled;
|
private float maxDistance, reflectionSteps, rainAmount, ripplesEnabled;
|
||||||
|
private float binarySteps, darkeningFactor;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(java.nio.ByteBuffer buffer) {
|
public void write(java.nio.ByteBuffer buffer) {
|
||||||
|
|
@ -76,6 +81,7 @@ public enum WetSurfaceUniforms {
|
||||||
b.putFloat(invW).putFloat(invH).putFloat(time).putFloat(mainDepthEnabled);
|
b.putFloat(invW).putFloat(invH).putFloat(time).putFloat(mainDepthEnabled);
|
||||||
b.putFloat(wetness).putFloat(puddleCoverage).putFloat(reflectionStrength).putFloat(rippleStrength);
|
b.putFloat(wetness).putFloat(puddleCoverage).putFloat(reflectionStrength).putFloat(rippleStrength);
|
||||||
b.putFloat(maxDistance).putFloat(reflectionSteps).putFloat(rainAmount).putFloat(ripplesEnabled);
|
b.putFloat(maxDistance).putFloat(reflectionSteps).putFloat(rainAmount).putFloat(ripplesEnabled);
|
||||||
|
b.putFloat(binarySteps).putFloat(darkeningFactor).putFloat(0.0f).putFloat(0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,24 @@ public final class WorldPassCompiler {
|
||||||
stats.skippedEmptyCommand();
|
stats.skippedEmptyCommand();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
if (mesh.isBuilding()) mesh.end();
|
if (mesh.isBuilding()) mesh.end();
|
||||||
|
} catch (IllegalStateException | IndexOutOfBoundsException e) {
|
||||||
|
org.slf4j.LoggerFactory.getLogger(WorldPassCompiler.class).warn("Skipping invalid world mesh: {}", e.getMessage());
|
||||||
|
stats.skippedEmptyCommand();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mesh.getIndicesCount() <= 0) {
|
if (mesh.getIndicesCount() <= 0) {
|
||||||
stats.skippedEmptyCommand();
|
stats.skippedEmptyCommand();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
validateWorldMeshAnchor(ctx, mesh, command);
|
validateWorldMeshAnchor(ctx, mesh, command);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
org.slf4j.LoggerFactory.getLogger(WorldPassCompiler.class).warn("Skipping world mesh with invalid anchor: {}", e.getMessage());
|
||||||
|
stats.skippedEmptyCommand();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GpuMeshHandle uploaded = null;
|
GpuMeshHandle uploaded = null;
|
||||||
MeshBuilder drawMesh = mesh;
|
MeshBuilder drawMesh = mesh;
|
||||||
|
|
|
||||||
|
|
@ -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.render.helpers.particle;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Плоский квад, ориентированный по yaw/pitch/spin вокруг своей точки — общая
|
||||||
|
* геометрия для "пузырь"-эффектов (HitEffect.BUBBLES, HitBubbles), вынесена
|
||||||
|
* из HitEffect чтобы не дублировать между модулями.
|
||||||
|
*/
|
||||||
|
public enum OrientedQuadRenderer {
|
||||||
|
;
|
||||||
|
|
||||||
|
public static void addOrientedQuad(MeshBuilder mesh, Vec3 center, float yaw, float pitch,
|
||||||
|
float spinDeg, float scale, int c1, int c2, int c3, int c4) {
|
||||||
|
Quaternionf rot = new Quaternionf()
|
||||||
|
.rotateY((float) Math.toRadians(yaw))
|
||||||
|
.rotateX((float) Math.toRadians(pitch));
|
||||||
|
|
||||||
|
Vector3f forward = new Vector3f(0, 0, 1).rotate(rot);
|
||||||
|
Vector3f right = new Vector3f(1, 0, 0).rotate(rot);
|
||||||
|
Vector3f up = new Vector3f(0, 1, 0).rotate(rot);
|
||||||
|
|
||||||
|
Quaternionf spin = new Quaternionf().fromAxisAngleRad(forward, (float) Math.toRadians(spinDeg));
|
||||||
|
right.rotate(spin);
|
||||||
|
up.rotate(spin);
|
||||||
|
|
||||||
|
right.mul(scale / 2f);
|
||||||
|
up.mul(scale / 2f);
|
||||||
|
|
||||||
|
double p1x = center.x - right.x() - up.x();
|
||||||
|
double p1y = center.y - right.y() - up.y();
|
||||||
|
double p1z = center.z - right.z() - up.z();
|
||||||
|
double p2x = center.x + right.x() - up.x();
|
||||||
|
double p2y = center.y + right.y() - up.y();
|
||||||
|
double p2z = center.z + right.z() - up.z();
|
||||||
|
double p3x = center.x + right.x() + up.x();
|
||||||
|
double p3y = center.y + right.y() + up.y();
|
||||||
|
double p3z = center.z + right.z() + up.z();
|
||||||
|
double p4x = center.x - right.x() + up.x();
|
||||||
|
double p4y = center.y - right.y() + up.y();
|
||||||
|
double p4z = center.z - right.z() + up.z();
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(c1)).next();
|
||||||
|
int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(c2)).next();
|
||||||
|
int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(c3)).next();
|
||||||
|
int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(c4)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Чистая математика трёхфазной анимации (появление/жизнь/исчезновение) —
|
||||||
|
* идея из Soup HitBubbles (ThreeStageAnimation), без библиотеки именованных
|
||||||
|
* интерполяций референса: appear — ease-out (быстрый старт), disappear —
|
||||||
|
* ease-in (медленный старт затухания).
|
||||||
|
*/
|
||||||
|
public enum ThreeStageProgress {
|
||||||
|
;
|
||||||
|
|
||||||
|
public enum Stage { APPEAR, EXIST, DISAPPEAR, FINISHED }
|
||||||
|
|
||||||
|
public static Stage stage(double elapsedMs, double appearMs, double existMs, double disappearMs) {
|
||||||
|
if (elapsedMs < appearMs) return Stage.APPEAR;
|
||||||
|
if (elapsedMs < appearMs + existMs) return Stage.EXIST;
|
||||||
|
if (elapsedMs < appearMs + existMs + disappearMs) return Stage.DISAPPEAR;
|
||||||
|
return Stage.FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isFinished(double elapsedMs, double appearMs, double existMs, double disappearMs) {
|
||||||
|
return stage(elapsedMs, appearMs, existMs, disappearMs) == Stage.FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return общая "видимость" 0..1: растёт на appear, держится 1 на exist, падает на disappear. */
|
||||||
|
public static float visibility(double elapsedMs, double appearMs, double existMs, double disappearMs) {
|
||||||
|
Stage stage = stage(elapsedMs, appearMs, existMs, disappearMs);
|
||||||
|
return switch (stage) {
|
||||||
|
case APPEAR -> easeOut(clamp01(safeDiv(elapsedMs, appearMs)));
|
||||||
|
case EXIST -> 1.0f;
|
||||||
|
case DISAPPEAR -> 1.0f - easeIn(clamp01(safeDiv(elapsedMs - appearMs - existMs, disappearMs)));
|
||||||
|
case FINISHED -> 0.0f;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float easeOut(float t) {
|
||||||
|
float inv = 1.0f - t;
|
||||||
|
return 1.0f - inv * inv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float easeIn(float t) {
|
||||||
|
return t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double safeDiv(double value, double divisor) {
|
||||||
|
return divisor <= 0.0 ? 1.0 : value / divisor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float clamp01(double v) {
|
||||||
|
if (Double.isNaN(v)) return 0.0f;
|
||||||
|
if (v < 0.0) return 0.0f;
|
||||||
|
if (v > 1.0) return 1.0f;
|
||||||
|
return (float) v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.player.AbstractClientPlayer;
|
import net.minecraft.client.player.AbstractClientPlayer;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.entity.HumanoidArm;
|
import net.minecraft.world.entity.HumanoidArm;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.items.HoldMyItems;
|
import dev.loki.lovisual.features.module.modules.visuals.items.HoldMyItems;
|
||||||
import dev.loki.lovisual.render.helpers.util.TickDelta;
|
import dev.loki.lovisual.render.helpers.util.TickDelta;
|
||||||
|
|
||||||
|
|
@ -54,7 +55,7 @@ public final class HandDynamics {
|
||||||
*/
|
*/
|
||||||
public static boolean apply(
|
public static boolean apply(
|
||||||
HoldMyItems module, HandAnimState state, AbstractClientPlayer player,
|
HoldMyItems module, HandAnimState state, AbstractClientPlayer player,
|
||||||
float tickDelta, InteractionHand hand, PoseStack matrices
|
float tickDelta, InteractionHand hand, ItemStack item, PoseStack matrices
|
||||||
) {
|
) {
|
||||||
boolean mainHand = hand == InteractionHand.MAIN_HAND;
|
boolean mainHand = hand == InteractionHand.MAIN_HAND;
|
||||||
float bobStrength = module.walkBob.get() ? module.bobStrength.get() : 0.0f;
|
float bobStrength = module.walkBob.get() ? module.bobStrength.get() : 0.0f;
|
||||||
|
|
@ -62,6 +63,7 @@ public final class HandDynamics {
|
||||||
|
|
||||||
boolean usingThisHand = player.isUsingItem() && player.getUsedItemHand() == hand;
|
boolean usingThisHand = player.isUsingItem() && player.getUsedItemHand() == hand;
|
||||||
float drift = usingThisHand ? state.drift(mainHand) : 0.0f;
|
float drift = usingThisHand ? state.drift(mainHand) : 0.0f;
|
||||||
|
boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand;
|
||||||
|
|
||||||
HandPoseMath.Pose pose = HandPoseMath.sway(
|
HandPoseMath.Pose pose = HandPoseMath.sway(
|
||||||
state.sway().yawLag(), state.sway().pitchLag(), module.swayIntensity.get());
|
state.sway().yawLag(), state.sway().pitchLag(), module.swayIntensity.get());
|
||||||
|
|
@ -72,12 +74,18 @@ public final class HandDynamics {
|
||||||
bobStrength));
|
bobStrength));
|
||||||
}
|
}
|
||||||
if (driftStrength > 0.0f && drift > 0.0f) {
|
if (driftStrength > 0.0f && drift > 0.0f) {
|
||||||
boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand;
|
|
||||||
pose = HandPoseMath.add(pose, HandPoseMath.drift(drift, driftStrength, rightArm));
|
pose = HandPoseMath.add(pose, HandPoseMath.drift(drift, driftStrength, rightArm));
|
||||||
}
|
}
|
||||||
if (module.joinAppear.get()) {
|
if (module.joinAppear.get()) {
|
||||||
pose = HandPoseMath.add(pose, HandPoseMath.joinDrop(state.joinProgress()));
|
pose = HandPoseMath.add(pose, HandPoseMath.joinDrop(state.joinProgress()));
|
||||||
}
|
}
|
||||||
|
if (module.gripEnabled.get() && !usingThisHand && HeldAlongFist.test(item)) {
|
||||||
|
float side = rightArm ? 1.0f : -1.0f;
|
||||||
|
pose = HandPoseMath.add(pose, HandPoseMath.grip(
|
||||||
|
module.gripExtend.get(), module.gripSlide.get(), module.gripOut.get(),
|
||||||
|
module.gripForward.get(), module.gripTilt.get(), module.gripTurn.get(),
|
||||||
|
module.gripRoll.get(), side));
|
||||||
|
}
|
||||||
if (!pose.isSignificant(SIGNIFICANT_EPSILON)) {
|
if (!pose.isSignificant(SIGNIFICANT_EPSILON)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,24 @@ public enum HandPoseMath {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Хват вдоль кулака" (grip) для предметов с рукоятью (см. {@link HeldAlongFist}):
|
||||||
|
* лёгкий сдвиг ближе к камере/в сторону и поворот — имитирует держание меча/палки
|
||||||
|
* не прямо по-вертикали ванильного equip, а слегка наискось в кулаке.
|
||||||
|
* {@code side}: +1 — правая рука, -1 — левая.
|
||||||
|
*/
|
||||||
|
public static Pose grip(float extend, float slide, float out, float forward,
|
||||||
|
float tilt, float turn, float roll, float side) {
|
||||||
|
return new Pose(
|
||||||
|
0.32f * side * extend + out * side,
|
||||||
|
slide,
|
||||||
|
-0.95f * extend - forward,
|
||||||
|
tilt,
|
||||||
|
turn * side,
|
||||||
|
roll * side
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Появление при входе в мир: предмет въезжает снизу и распрямляется. */
|
/** Появление при входе в мир: предмет въезжает снизу и распрямляется. */
|
||||||
public static Pose joinDrop(float progress01) {
|
public static Pose joinDrop(float progress01) {
|
||||||
float p = clamp01(progress01);
|
float p = clamp01(progress01);
|
||||||
|
|
|
||||||
|
|
@ -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.hand;
|
||||||
|
|
||||||
|
import net.minecraft.tags.ItemTags;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Предметы с рукоятью, которые выглядят естественно при "хвате вдоль кулака"
|
||||||
|
* (grip): мечи/инструменты/палки. Портировано из идеи Delta HMI
|
||||||
|
* (heldAlongTheFist), список расширен по нашему набору ванильных предметов.
|
||||||
|
*/
|
||||||
|
public enum HeldAlongFist {
|
||||||
|
;
|
||||||
|
|
||||||
|
public static boolean test(ItemStack item) {
|
||||||
|
if (item == null || item.isEmpty()) return false;
|
||||||
|
return item.is(ItemTags.SWORDS) || item.is(ItemTags.AXES) || item.is(ItemTags.PICKAXES)
|
||||||
|
|| item.is(ItemTags.SHOVELS) || item.is(ItemTags.HOES)
|
||||||
|
|| item.is(Items.MACE) || item.is(Items.TRIDENT)
|
||||||
|
|| item.is(Items.FISHING_ROD) || item.is(Items.CARROT_ON_A_STICK)
|
||||||
|
|| item.is(Items.WARPED_FUNGUS_ON_A_STICK) || item.is(Items.STICK)
|
||||||
|
|| item.is(Items.BLAZE_ROD) || item.is(Items.BREEZE_ROD)
|
||||||
|
|| item.is(Items.BONE) || item.is(Items.DEBUG_STICK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud;
|
||||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||||
import dev.loki.lovisual.features.module.modules.combat.pvp.RubberHand;
|
import dev.loki.lovisual.features.module.modules.combat.pvp.RubberHand;
|
||||||
import dev.loki.lovisual.features.module.modules.misc.sound.HitSounds;
|
import dev.loki.lovisual.features.module.modules.misc.sound.HitSounds;
|
||||||
|
import dev.loki.lovisual.features.module.modules.visuals.effects.HitBubbles;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.effects.HitEffect;
|
import dev.loki.lovisual.features.module.modules.visuals.effects.HitEffect;
|
||||||
import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor;
|
import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor;
|
||||||
import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor;
|
import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor;
|
||||||
|
|
@ -165,6 +166,10 @@ public enum AttackUtil {
|
||||||
if (hitEffect != null) {
|
if (hitEffect != null) {
|
||||||
hitEffect.handleHit(target);
|
hitEffect.handleHit(target);
|
||||||
}
|
}
|
||||||
|
HitBubbles hitBubbles = Modules.get(HitBubbles.class);
|
||||||
|
if (hitBubbles != null) {
|
||||||
|
hitBubbles.handleHit(target);
|
||||||
|
}
|
||||||
TargetHud.notifyHit(target);
|
TargetHud.notifyHit(target);
|
||||||
TargetManager.onAttack(target);
|
TargetManager.onAttack(target);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.combat.hit;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.projectile.ProjectileUtil;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.minecraft.world.phys.EntityHitResult;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Точная точка удара по хитбоксу цели — трасса луча от камеры вдоль последнего
|
||||||
|
* известного взгляда игрока. Общая для эффектов, которым важна не позиция цели
|
||||||
|
* целиком, а конкретное место попадания (HitEffect, HitBubbles).
|
||||||
|
*/
|
||||||
|
public enum AttackHitPoint {
|
||||||
|
;
|
||||||
|
|
||||||
|
/** @return точка попадания на хитбоксе {@code target} или null, если трасса не пересекла его. */
|
||||||
|
public static Vec3 resolve(Entity target, float lastYaw, float lastPitch) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.player == null) return null;
|
||||||
|
|
||||||
|
double distance = mc.player.distanceTo(target) + 1.0;
|
||||||
|
Vec3 start = mc.player.position().add(0, mc.player.getEyeHeight(mc.player.getPose()), 0);
|
||||||
|
Vec3 rot = rotationVector(lastPitch, lastYaw);
|
||||||
|
Vec3 end = start.add(rot.scale(distance));
|
||||||
|
AABB box = mc.player.getBoundingBox().expandTowards(rot.scale(distance)).inflate(1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
EntityHitResult hit = ProjectileUtil.getEntityHitResult(
|
||||||
|
mc.player,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
box,
|
||||||
|
e -> !e.isSpectator() && e.isPickable() && e == target,
|
||||||
|
distance * distance
|
||||||
|
);
|
||||||
|
|
||||||
|
return hit != null ? hit.getLocation() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vec3 rotationVector(float pitch, float yaw) {
|
||||||
|
float yawRad = -yaw * ((float) Math.PI / 180F);
|
||||||
|
float pitchRad = pitch * ((float) Math.PI / 180F);
|
||||||
|
float cosYaw = Mth.cos(yawRad);
|
||||||
|
float sinYaw = Mth.sin(yawRad);
|
||||||
|
float cosPitch = Mth.cos(pitchRad);
|
||||||
|
float sinPitch = Mth.sin(pitchRad);
|
||||||
|
return new Vec3(sinYaw * cosPitch, -sinPitch, cosYaw * cosPitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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.world;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Чистая математика GPS-компаса: направление на точку и угол доворота стрелки
|
||||||
|
* от текущего взгляда игрока. Вынесено из модуля для юнит-тестов без Minecraft.
|
||||||
|
*/
|
||||||
|
public enum GpsBearing {
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Азимут от игрока к точке в системе координат Minecraft (0° = юг, +Z),
|
||||||
|
* растёт по часовой при виде сверху — совпадает с {@code Entity.getYRot()}.
|
||||||
|
*/
|
||||||
|
public static float bearingDegrees(double dx, double dz) {
|
||||||
|
return (float) (-(Math.atan2(dx, dz) * (180.0 / Math.PI)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Угол, на который нужно повернуть стрелку-компас (0° — «прямо по курсу»):
|
||||||
|
* разница между азимутом на цель и текущим yaw игрока, приведённая к (-180, 180].
|
||||||
|
*/
|
||||||
|
public static float arrowRotationDegrees(float bearing, float playerYaw) {
|
||||||
|
return wrapDegrees(bearing - playerYaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float wrapDegrees(float degrees) {
|
||||||
|
float wrapped = degrees % 360.0f;
|
||||||
|
if (wrapped >= 180.0f) wrapped -= 360.0f;
|
||||||
|
if (wrapped < -180.0f) wrapped += 360.0f;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double horizontalDistance(double dx, double dz) {
|
||||||
|
return Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3230,6 +3230,7 @@
|
||||||
"command.config.description": "Saves, reloads, or locates the active client config.",
|
"command.config.description": "Saves, reloads, or locates the active client config.",
|
||||||
"command.coordinates.description": "Shows the current player coordinates.",
|
"command.coordinates.description": "Shows the current player coordinates.",
|
||||||
"command.enemy.description": "Manages the client enemy list.",
|
"command.enemy.description": "Manages the client enemy list.",
|
||||||
|
"command.gps.description": "Sets or clears the Gps compass target.",
|
||||||
"command.friend.description": "Manages the client friend list.",
|
"command.friend.description": "Manages the client friend list.",
|
||||||
"command.help.description": "Shows every available client command with clickable pages. Prefix: % (legacy @ also works).",
|
"command.help.description": "Shows every available client command with clickable pages. Prefix: % (legacy @ also works).",
|
||||||
"command.hide.description": "Hides or shows a module in ModuleList without disabling it.",
|
"command.hide.description": "Hides or shows a module in ModuleList without disabling it.",
|
||||||
|
|
|
||||||
|
|
@ -3025,6 +3025,7 @@
|
||||||
"command.config.description": "Сохраняет, перезагружает или показывает путь к конфигурации клиента.",
|
"command.config.description": "Сохраняет, перезагружает или показывает путь к конфигурации клиента.",
|
||||||
"command.coordinates.description": "Показывает текущие координаты игрока.",
|
"command.coordinates.description": "Показывает текущие координаты игрока.",
|
||||||
"command.enemy.description": "Управляет списком врагов клиента.",
|
"command.enemy.description": "Управляет списком врагов клиента.",
|
||||||
|
"command.gps.description": "Задаёт или сбрасывает цель GPS-компаса.",
|
||||||
"command.friend.description": "Управляет списком друзей клиента.",
|
"command.friend.description": "Управляет списком друзей клиента.",
|
||||||
"command.help.description": "Показывает все доступные команды клиента с кликабельными страницами. Префикс: % (старый @ тоже работает).",
|
"command.help.description": "Показывает все доступные команды клиента с кликабельными страницами. Префикс: % (старый @ тоже работает).",
|
||||||
"command.hide.description": "Скрывает или показывает модуль в ModuleList, не отключая его.",
|
"command.hide.description": "Скрывает или показывает модуль в ModuleList, не отключая его.",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ layout (std140) uniform WetSurface {
|
||||||
vec4 u_Screen; // invW, invH, time, mainDepthEnabled
|
vec4 u_Screen; // invW, invH, time, mainDepthEnabled
|
||||||
vec4 u_Params; // wetness, puddleCoverage, reflectionStrength, rippleStrength
|
vec4 u_Params; // wetness, puddleCoverage, reflectionStrength, rippleStrength
|
||||||
vec4 u_Params2; // maxDistance, reflectionSteps, rainAmount, ripplesEnabled
|
vec4 u_Params2; // maxDistance, reflectionSteps, rainAmount, ripplesEnabled
|
||||||
|
vec4 u_Params3; // binarySteps, darkeningFactor, unused, unused
|
||||||
};
|
};
|
||||||
|
|
||||||
in vec2 v_TexCoord;
|
in vec2 v_TexCoord;
|
||||||
|
|
@ -36,12 +37,12 @@ vec3 viewPositionFromDepth(vec2 uv, float rawDepth) {
|
||||||
return view.xyz / view.w;
|
return view.xyz / view.w;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool projectToScreen(vec3 viewPos, out vec2 uv, out float rawDepth) {
|
bool projectToScreen(vec3 viewPos, out vec2 uv, out float ndcZ) {
|
||||||
vec4 clip = u_Projection * vec4(viewPos, 1.0);
|
vec4 clip = u_Projection * vec4(viewPos, 1.0);
|
||||||
if (clip.w <= 0.0001) return false;
|
if (clip.w <= 0.0001) return false;
|
||||||
vec3 ndc = clip.xyz / clip.w;
|
vec3 ndc = clip.xyz / clip.w;
|
||||||
uv = ndc.xy * 0.5 + 0.5;
|
uv = ndc.xy * 0.5 + 0.5;
|
||||||
rawDepth = ndc.z;
|
ndcZ = ndc.z;
|
||||||
return uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
return uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,9 +61,60 @@ float valueNoise(vec2 p) {
|
||||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
float v = 0.0;
|
||||||
|
float a = 0.5;
|
||||||
|
mat2 rot = mat2(0.8, 0.6, -0.6, 0.8);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
v += valueNoise(p) * a;
|
||||||
|
p = rot * p * 2.02 + vec2(1.7, 9.2);
|
||||||
|
a *= 0.5;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
float puddleNoise(vec2 worldXZ) {
|
float puddleNoise(vec2 worldXZ) {
|
||||||
float n = valueNoise(worldXZ * 0.18) * 0.65
|
return valueNoise(worldXZ * 0.18) * 0.65 + valueNoise(worldXZ * 0.45 + 17.0) * 0.35;
|
||||||
+ valueNoise(worldXZ * 0.45 + 17.0) * 0.35;
|
}
|
||||||
|
|
||||||
|
float getDither(vec2 fragCoord) {
|
||||||
|
return fract(52.9829189 * fract(dot(fragCoord, vec2(0.06711056, 0.00583715))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Реконструкция нормали по 4 соседним сэмплам буфера глубины вместо dFdx/dFdy от view-position:
|
||||||
|
// screen-space derivative ломается на границах глубины (силуэты, дальний план) и даёт шумную/
|
||||||
|
// блочную нормаль на почти плоской поверхности -> дрожащая/рваная граница лужи. Сэмплинг соседей
|
||||||
|
// по глубине даёт гладкую нормаль и не зависит от размера треугольника меша чанка.
|
||||||
|
vec3 normalFromDepth(vec2 uv, vec3 viewPos) {
|
||||||
|
vec2 texel = vec2(u_Screen.x, u_Screen.y);
|
||||||
|
|
||||||
|
float dL = texture(u_MainDepth, clamp(uv - vec2(texel.x, 0.0), vec2(0.0001), vec2(0.9999))).r;
|
||||||
|
float dR = texture(u_MainDepth, clamp(uv + vec2(texel.x, 0.0), vec2(0.0001), vec2(0.9999))).r;
|
||||||
|
float dD = texture(u_MainDepth, clamp(uv - vec2(0.0, texel.y), vec2(0.0001), vec2(0.9999))).r;
|
||||||
|
float dU = texture(u_MainDepth, clamp(uv + vec2(0.0, texel.y), vec2(0.0001), vec2(0.9999))).r;
|
||||||
|
|
||||||
|
vec3 pL = viewPositionFromDepth(uv - vec2(texel.x, 0.0), dL);
|
||||||
|
vec3 pR = viewPositionFromDepth(uv + vec2(texel.x, 0.0), dR);
|
||||||
|
vec3 pD = viewPositionFromDepth(uv - vec2(0.0, texel.y), dD);
|
||||||
|
vec3 pU = viewPositionFromDepth(uv + vec2(0.0, texel.y), dU);
|
||||||
|
|
||||||
|
vec3 vL = viewPos - pL;
|
||||||
|
vec3 vR = pR - viewPos;
|
||||||
|
vec3 vD = viewPos - pD;
|
||||||
|
vec3 vU = pU - viewPos;
|
||||||
|
|
||||||
|
// берём более короткую (менее вероятно пересекающую разрыв глубины) сторону по каждой оси
|
||||||
|
vec3 dX = (abs(vL.z) < abs(vR.z)) ? vL : vR;
|
||||||
|
vec3 dY = (abs(vD.z) < abs(vU.z)) ? vD : vU;
|
||||||
|
|
||||||
|
vec3 n = cross(dX, dY);
|
||||||
|
float len = length(n);
|
||||||
|
if (len > 1e-6) {
|
||||||
|
n /= len;
|
||||||
|
} else {
|
||||||
|
n = normalize(-viewPos);
|
||||||
|
}
|
||||||
|
if (dot(n, -viewPos) < 0.0) n = -n;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,18 +134,12 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 viewPos = viewPositionFromDepth(uv, rawDepth);
|
vec3 viewPos = viewPositionFromDepth(uv, rawDepth);
|
||||||
|
vec3 viewNormal = normalFromDepth(uv, viewPos);
|
||||||
vec3 ddxPos = dFdx(viewPos);
|
|
||||||
vec3 ddyPos = dFdy(viewPos);
|
|
||||||
vec3 viewNormal = normalize(cross(ddxPos, ddyPos));
|
|
||||||
vec3 viewDir = normalize(viewPos);
|
vec3 viewDir = normalize(viewPos);
|
||||||
if (dot(viewNormal, -viewDir) < 0.0) {
|
|
||||||
viewNormal = -viewNormal;
|
|
||||||
}
|
|
||||||
|
|
||||||
vec3 worldNormal = normalize(mat3(u_InvView) * viewNormal);
|
vec3 worldNormal = normalize(mat3(u_InvView) * viewNormal);
|
||||||
float groundFactor = clamp(dot(worldNormal, vec3(0.0, 1.0, 0.0)), 0.0, 1.0);
|
float groundFactor = clamp(dot(worldNormal, vec3(0.0, 1.0, 0.0)), 0.0, 1.0);
|
||||||
groundFactor = smoothstep(0.82, 0.98, groundFactor);
|
groundFactor = smoothstep(0.6, 0.92, groundFactor);
|
||||||
if (groundFactor <= 0.001) {
|
if (groundFactor <= 0.001) {
|
||||||
color = base;
|
color = base;
|
||||||
return;
|
return;
|
||||||
|
|
@ -106,9 +152,11 @@ void main() {
|
||||||
float reflectionStrength = clamp(u_Params.z, 0.0, 2.0);
|
float reflectionStrength = clamp(u_Params.z, 0.0, 2.0);
|
||||||
float rippleStrength = clamp(u_Params.w, 0.0, 2.0);
|
float rippleStrength = clamp(u_Params.w, 0.0, 2.0);
|
||||||
float maxDistance = max(u_Params2.x, 1.0);
|
float maxDistance = max(u_Params2.x, 1.0);
|
||||||
int steps = int(clamp(floor(u_Params2.y + 0.5), 1.0, 32.0));
|
int steps = int(clamp(floor(u_Params2.y + 0.5), 1.0, 96.0));
|
||||||
float rainAmount = clamp(u_Params2.z, 0.0, 2.0);
|
float rainAmount = clamp(u_Params2.z, 0.0, 2.0);
|
||||||
bool ripplesEnabled = u_Params2.w > 0.5;
|
bool ripplesEnabled = u_Params2.w > 0.5;
|
||||||
|
int binarySteps = int(clamp(floor(u_Params3.x + 0.5), 0.0, 8.0));
|
||||||
|
float darkeningFactor = clamp(u_Params3.y, 0.0, 1.0);
|
||||||
|
|
||||||
float noise = puddleNoise(worldPos.xz);
|
float noise = puddleNoise(worldPos.xz);
|
||||||
float wetMask = smoothstep(0.5 - puddleCoverage * 0.55, 0.5 + puddleCoverage * 0.35, noise);
|
float wetMask = smoothstep(0.5 - puddleCoverage * 0.55, 0.5 + puddleCoverage * 0.35, noise);
|
||||||
|
|
@ -118,48 +166,90 @@ void main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 reflNormal = viewNormal;
|
vec3 reflNormal = worldNormal;
|
||||||
if (ripplesEnabled && rippleStrength > 0.001) {
|
if (ripplesEnabled && rippleStrength > 0.001) {
|
||||||
|
// Аналитический градиент бегущей волны вместо мёртвого dFdx(ripple)*0.0 из старой версии
|
||||||
|
// (там перемножение на 0 полностью убивало рябь) — считаем волну в двух соседних точках
|
||||||
|
// и берём разность как наклон поверхности, а не саму высоту волны.
|
||||||
float t = u_Screen.z;
|
float t = u_Screen.z;
|
||||||
float ripple = sin((worldPos.x + worldPos.z) * 3.1 + t * 2.4) * 0.5
|
float eps = 0.35;
|
||||||
+ sin((worldPos.x - worldPos.z * 1.7) * 4.3 - t * 3.1) * 0.5;
|
vec2 wp = worldPos.xz;
|
||||||
vec2 perturb = vec2(dFdx(ripple), dFdy(ripple)) * 0.0;
|
float h0 = sin(wp.x * 3.1 + t * 2.4) * 0.5 + sin((wp.x - wp.y * 1.7) * 4.3 - t * 3.1) * 0.5;
|
||||||
reflNormal = normalize(viewNormal + vec3(ripple, 0.0, ripple * 0.6) * rippleStrength * 0.05);
|
float hx = sin((wp.x + eps) * 3.1 + t * 2.4) * 0.5 + sin(((wp.x + eps) - wp.y * 1.7) * 4.3 - t * 3.1) * 0.5;
|
||||||
|
float hy = sin(wp.x * 3.1 + t * 2.4) * 0.5 + sin((wp.x - (wp.y + eps) * 1.7) * 4.3 - t * 3.1) * 0.5;
|
||||||
|
vec2 slope = vec2(hx - h0, hy - h0) / eps;
|
||||||
|
vec3 perturbedWorld = normalize(vec3(-slope.x, 1.0, -slope.y) * vec3(rippleStrength * 0.35, 1.0, rippleStrength * 0.35));
|
||||||
|
reflNormal = normalize(mix(worldNormal, perturbedWorld, groundFactor));
|
||||||
}
|
}
|
||||||
|
// world-space нормаль обратно в view-space через транспонированную (обратную) чистую ротацию u_InvView
|
||||||
|
vec3 reflNormalView = normalize(transpose(mat3(u_InvView)) * reflNormal);
|
||||||
|
if (dot(reflNormalView, -viewDir) < 0.0) reflNormalView = -reflNormalView;
|
||||||
|
|
||||||
vec3 reflDir = reflect(viewDir, reflNormal);
|
vec3 reflDir = reflect(viewDir, reflNormalView);
|
||||||
|
|
||||||
|
vec3 skyPitch3 = normalize(mat3(u_InvView) * reflDir);
|
||||||
|
float skyPitch = clamp(skyPitch3.y * 0.5 + 0.5, 0.0, 1.0);
|
||||||
|
vec3 horizonColor = vec3(0.30, 0.36, 0.45);
|
||||||
|
vec3 zenithColor = vec3(0.10, 0.16, 0.28);
|
||||||
|
vec3 reflectionColor = mix(horizonColor, zenithColor, pow(skyPitch, 0.75));
|
||||||
|
float cloud = fbm(skyPitch3.xz * 1.5 / max(abs(skyPitch3.y) + 0.18, 0.08));
|
||||||
|
reflectionColor = mix(reflectionColor, vec3(0.42, 0.48, 0.56), cloud * 0.28 * smoothstep(-0.1, 0.5, skyPitch3.y));
|
||||||
|
vec3 lightDir = normalize(vec3(0.35, 0.75, 0.25));
|
||||||
|
float sunSpec = pow(max(dot(skyPitch3, lightDir), 0.0), 32.0);
|
||||||
|
reflectionColor += vec3(0.85, 0.88, 0.95) * sunSpec * 0.5;
|
||||||
|
|
||||||
vec3 reflectionColor = mix(vec3(0.45, 0.52, 0.62), vec3(0.75, 0.82, 0.92), clamp(reflDir.y * 0.5 + 0.5, 0.0, 1.0));
|
|
||||||
bool hit = false;
|
bool hit = false;
|
||||||
|
float dither = getDither(gl_FragCoord.xy);
|
||||||
|
float stepLen = maxDistance / float(max(steps, 1));
|
||||||
|
vec3 rayOrigin = viewPos + reflNormalView * 0.02;
|
||||||
|
|
||||||
float stepLen = maxDistance / float(steps);
|
for (int i = 0; i < 96; i++) {
|
||||||
vec3 marchPos = viewPos + reflNormal * 0.02;
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
if (i >= steps) break;
|
if (i >= steps) break;
|
||||||
|
|
||||||
marchPos += reflDir * stepLen;
|
float t = (float(i) + dither) * stepLen + stepLen * 0.5;
|
||||||
|
vec3 marchPos = rayOrigin + reflDir * t;
|
||||||
|
|
||||||
vec2 sampleUv;
|
vec2 sampleUv;
|
||||||
float sampleClipDepth;
|
float sampleNdcZ;
|
||||||
if (!projectToScreen(marchPos, sampleUv, sampleClipDepth)) break;
|
if (!projectToScreen(marchPos, sampleUv, sampleNdcZ)) break;
|
||||||
|
|
||||||
float sceneRawDepth = texture(u_MainDepth, sampleUv).r;
|
float sceneRawDepth = texture(u_MainDepth, sampleUv).r;
|
||||||
if (!validRawDepth(sceneRawDepth)) continue;
|
if (!validRawDepth(sceneRawDepth)) continue;
|
||||||
|
|
||||||
vec3 scenePos = viewPositionFromDepth(sampleUv, sceneRawDepth);
|
vec3 scenePos = viewPositionFromDepth(sampleUv, sceneRawDepth);
|
||||||
|
|
||||||
float marchDist = length(marchPos);
|
float marchDist = length(marchPos);
|
||||||
float sceneDist = length(scenePos);
|
float sceneDist = length(scenePos);
|
||||||
float thickness = max(0.15, marchDist * 0.03);
|
float thickness = max(0.15, marchDist * 0.03);
|
||||||
|
|
||||||
if (marchDist > sceneDist && marchDist - sceneDist < thickness) {
|
if (marchDist > sceneDist && marchDist - sceneDist < thickness) {
|
||||||
reflectionColor = texture(u_Texture, sampleUv).rgb;
|
// Бинарный рефайнмент: сужаем интервал вокруг найденного пересечения, чтобы убрать
|
||||||
|
// блочность отражений на резких перепадах глубины (старая версия брала грубый sampleUv как есть).
|
||||||
|
float tMin = max(0.0, t - stepLen);
|
||||||
|
float tMax = t;
|
||||||
|
vec2 refinedUv = sampleUv;
|
||||||
|
for (int b = 0; b < binarySteps; b++) {
|
||||||
|
float tMid = (tMin + tMax) * 0.5;
|
||||||
|
vec3 midPos = rayOrigin + reflDir * tMid;
|
||||||
|
vec2 midUv;
|
||||||
|
float midNdcZ;
|
||||||
|
if (!projectToScreen(midPos, midUv, midNdcZ)) break;
|
||||||
|
float midDepth = texture(u_MainDepth, midUv).r;
|
||||||
|
if (!validRawDepth(midDepth)) { tMin = tMid; continue; }
|
||||||
|
vec3 midScenePos = viewPositionFromDepth(midUv, midDepth);
|
||||||
|
if (length(midPos) > length(midScenePos)) {
|
||||||
|
tMax = tMid;
|
||||||
|
refinedUv = midUv;
|
||||||
|
} else {
|
||||||
|
tMin = tMid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reflectionColor = texture(u_Texture, refinedUv).rgb;
|
||||||
hit = true;
|
hit = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float fresnel = pow(1.0 - clamp(dot(-viewDir, viewNormal), 0.0, 1.0), 3.0);
|
float fresnel = pow(1.0 - clamp(dot(-viewDir, reflNormalView), 0.0, 1.0), 3.0);
|
||||||
fresnel = mix(0.25, 1.0, fresnel);
|
fresnel = mix(0.25, 1.0, fresnel);
|
||||||
|
|
||||||
float hitBoost = hit ? 1.0 : 0.55;
|
float hitBoost = hit ? 1.0 : 0.55;
|
||||||
|
|
@ -167,7 +257,7 @@ void main() {
|
||||||
* mix(0.6, 1.0, clamp(rainAmount, 0.0, 1.0)) * wetness;
|
* mix(0.6, 1.0, clamp(rainAmount, 0.0, 1.0)) * wetness;
|
||||||
reflectionAmount = clamp(reflectionAmount, 0.0, 1.0);
|
reflectionAmount = clamp(reflectionAmount, 0.0, 1.0);
|
||||||
|
|
||||||
vec3 darkened = base.rgb * mix(1.0, 0.82, wetMask * clamp(wetness, 0.0, 1.0));
|
vec3 darkened = base.rgb * (1.0 - darkeningFactor * wetMask);
|
||||||
vec3 result = mix(darkened, reflectionColor, reflectionAmount);
|
vec3 result = mix(darkened, reflectionColor, reflectionAmount);
|
||||||
|
|
||||||
color = vec4(clamp(result, 0.0, 1.0), 1.0);
|
color = vec4(clamp(result, 0.0, 1.0), 1.0);
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@
|
||||||
"network.game.PlayerInfoMixin",
|
"network.game.PlayerInfoMixin",
|
||||||
"network.packet.ClientCommonPacketListenerImplMixin",
|
"network.packet.ClientCommonPacketListenerImplMixin",
|
||||||
"network.packet.ServerboundUseItemPacketMixin",
|
"network.packet.ServerboundUseItemPacketMixin",
|
||||||
|
"render.entity.EndCrystalRendererMixin",
|
||||||
"render.entity.EntityRenderDispatcherMixin",
|
"render.entity.EntityRenderDispatcherMixin",
|
||||||
"render.entity.ItemEntityRenderStateMixin",
|
"render.entity.ItemEntityRenderStateMixin",
|
||||||
"render.entity.ItemEntityRendererMixin",
|
"render.entity.ItemEntityRendererMixin",
|
||||||
|
|
@ -154,6 +155,7 @@
|
||||||
"render.item.ItemInHandAlwaysVisibleMixin",
|
"render.item.ItemInHandAlwaysVisibleMixin",
|
||||||
"render.item.ItemInHandRendererMixin",
|
"render.item.ItemInHandRendererMixin",
|
||||||
"render.level.CameraMixin",
|
"render.level.CameraMixin",
|
||||||
|
"render.level.FireworkParticlesStarterMixin",
|
||||||
"render.level.LevelRendererMixin",
|
"render.level.LevelRendererMixin",
|
||||||
"render.level.ParticleManagerMixin",
|
"render.level.ParticleManagerMixin",
|
||||||
"render.level.SkyRendererMixin",
|
"render.level.SkyRendererMixin",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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.assertTrue;
|
||||||
|
|
||||||
|
final class ThreeStageProgressTest {
|
||||||
|
|
||||||
|
private static final float EPS = 1.0e-3f;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stagesFollowElapsedTime() {
|
||||||
|
assertEquals(ThreeStageProgress.Stage.APPEAR, ThreeStageProgress.stage(0, 100, 200, 300));
|
||||||
|
assertEquals(ThreeStageProgress.Stage.EXIST, ThreeStageProgress.stage(150, 100, 200, 300));
|
||||||
|
assertEquals(ThreeStageProgress.Stage.DISAPPEAR, ThreeStageProgress.stage(350, 100, 200, 300));
|
||||||
|
assertEquals(ThreeStageProgress.Stage.FINISHED, ThreeStageProgress.stage(700, 100, 200, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void visibilityStartsAtZeroAndEndsAtOneDuringAppear() {
|
||||||
|
assertEquals(0.0f, ThreeStageProgress.visibility(0, 100, 200, 300), EPS);
|
||||||
|
assertEquals(1.0f, ThreeStageProgress.visibility(100, 100, 200, 300), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void visibilityStaysFullDuringExist() {
|
||||||
|
assertEquals(1.0f, ThreeStageProgress.visibility(150, 100, 200, 300), EPS);
|
||||||
|
assertEquals(1.0f, ThreeStageProgress.visibility(299, 100, 200, 300), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void visibilityFadesToZeroDuringDisappear() {
|
||||||
|
assertEquals(1.0f, ThreeStageProgress.visibility(300, 100, 200, 300), EPS);
|
||||||
|
assertTrue(ThreeStageProgress.visibility(450, 100, 200, 300) < 1.0f);
|
||||||
|
assertEquals(0.0f, ThreeStageProgress.visibility(600, 100, 200, 300), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void visibilityIsZeroAfterFinished() {
|
||||||
|
assertEquals(0.0f, ThreeStageProgress.visibility(1000, 100, 200, 300), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isFinishedMatchesStage() {
|
||||||
|
assertFalse(ThreeStageProgress.isFinished(0, 100, 200, 300));
|
||||||
|
assertFalse(ThreeStageProgress.isFinished(599, 100, 200, 300));
|
||||||
|
assertTrue(ThreeStageProgress.isFinished(600, 100, 200, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zeroDurationStagesDoNotDivideByZero() {
|
||||||
|
assertEquals(1.0f, ThreeStageProgress.visibility(0, 0, 200, 300), EPS);
|
||||||
|
assertEquals(ThreeStageProgress.Stage.EXIST, ThreeStageProgress.stage(0, 0, 200, 300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -121,6 +121,30 @@ public final class HandPoseMathTest {
|
||||||
assertEquals(a, HandPoseMath.add(HandPoseMath.ZERO, a));
|
assertEquals(a, HandPoseMath.add(HandPoseMath.ZERO, a));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void gripZeroParamsIsZero() {
|
||||||
|
assertFalse(HandPoseMath.grip(0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f).isSignificant(EPS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void gripMirrorsBySide() {
|
||||||
|
HandPoseMath.Pose right = HandPoseMath.grip(0.3f, 0.1f, 0.05f, 0.14f, 5f, 10f, 15f, 1.0f);
|
||||||
|
HandPoseMath.Pose left = HandPoseMath.grip(0.3f, 0.1f, 0.05f, 0.14f, 5f, 10f, 15f, -1.0f);
|
||||||
|
assertEquals(-right.dx(), left.dx(), EPS);
|
||||||
|
assertEquals(right.dy(), left.dy(), EPS, "slide не зависит от стороны");
|
||||||
|
assertEquals(right.dz(), left.dz(), EPS, "extend/forward не зависят от стороны");
|
||||||
|
assertEquals(right.rx(), left.rx(), EPS, "tilt не зависит от стороны");
|
||||||
|
assertEquals(-right.ry(), left.ry(), EPS);
|
||||||
|
assertEquals(-right.rz(), left.rz(), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void gripExtendPullsCloserAndAcrossFist() {
|
||||||
|
HandPoseMath.Pose pose = HandPoseMath.grip(1.0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f);
|
||||||
|
assertTrue(pose.dz() < 0.0f, "extend тянет предмет ближе к камере (-Z)");
|
||||||
|
assertTrue(pose.dx() > 0.0f, "extend на правой руке сдвигает вбок по стороне");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void significanceThresholdWorks() {
|
void significanceThresholdWorks() {
|
||||||
assertFalse(HandPoseMath.ZERO.isSignificant(1.0e-4f));
|
assertFalse(HandPoseMath.ZERO.isSignificant(1.0e-4f));
|
||||||
|
|
|
||||||
|
|
@ -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.util.world;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
final class GpsBearingTest {
|
||||||
|
|
||||||
|
private static final float EPS = 1.0e-3f;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bearingPointsSouthWhenTargetAheadOnZ() {
|
||||||
|
// +Z (юг) при dx=0 -> азимут 0, совпадает с конвенцией getYRot()
|
||||||
|
assertEquals(0.0f, GpsBearing.bearingDegrees(0.0, 10.0), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bearingPointsWestWhenTargetOnNegativeX() {
|
||||||
|
assertEquals(90.0f, GpsBearing.bearingDegrees(-10.0, 0.0), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void arrowRotationIsZeroWhenFacingTarget() {
|
||||||
|
assertEquals(0.0f, GpsBearing.arrowRotationDegrees(45.0f, 45.0f), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void arrowRotationWrapsToShortestPath() {
|
||||||
|
// цель за спиной (разница 180) должна остаться в диапазоне (-180, 180]
|
||||||
|
float result = GpsBearing.arrowRotationDegrees(170.0f, -170.0f);
|
||||||
|
assertEquals(-20.0f, result, EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void wrapDegreesStaysInHalfOpenRange() {
|
||||||
|
assertEquals(-179.0f, GpsBearing.wrapDegrees(181.0f), EPS);
|
||||||
|
assertEquals(179.0f, GpsBearing.wrapDegrees(-181.0f), EPS);
|
||||||
|
assertEquals(0.0f, GpsBearing.wrapDegrees(360.0f), EPS);
|
||||||
|
assertEquals(90.0f, GpsBearing.wrapDegrees(90.0f), EPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void horizontalDistanceIsPythagorean() {
|
||||||
|
assertEquals(5.0, GpsBearing.horizontalDistance(3.0, 4.0), 1.0e-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue