From 8d9a2ad0d69d30ffe7c1792907abbffe840871f5 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 2 Oct 2026 10:01:37 +0200 Subject: [PATCH] feat(mod): ProjectileTrails, KillEffect death modes (Shatter/Ascend/Explosion/Disintegrate), TNT fuse bar; compact TODO Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ --- mod/TODO.md | 844 ++---------------- .../hud_element_vanilla_tooltips.json | 1 + .../combat/prediction/Predictions.java | 4 + .../render/PredictionsHudRenderer.java | 11 + .../effects/hits/DeathEffectSettings.java | 39 + .../visuals/effects/hits/KillEffect.java | 51 +- .../visuals/effects/hits/death/DeathBody.java | 54 ++ .../effects/hits/death/DeathEffectHelper.java | 178 ++++ .../visuals/effects/hits/death/DeathMesh.java | 94 ++ .../effects/hits/death/DeathPhysics.java | 137 +++ .../hits/effects/FallingLavaHelper.java | 28 + .../projectiletrails/ProjectileTrails.java | 95 ++ .../effects/projectiletrails/TrailRibbon.java | 99 ++ .../effects/projectiletrails/TrailStore.java | 83 ++ .../resources/assets/lovisual/lang/en_us.json | 49 +- .../resources/assets/lovisual/lang/ru_ru.json | 49 +- .../effects/hits/death/DeathPhysicsTest.java | 81 ++ 17 files changed, 1097 insertions(+), 800 deletions(-) create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/DeathEffectSettings.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathBody.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathEffectHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathMesh.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysics.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/FallingLavaHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/ProjectileTrails.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailRibbon.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailStore.java create mode 100644 mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysicsTest.java diff --git a/mod/TODO.md b/mod/TODO.md index 55f4f12..3ecd2d4 100644 --- a/mod/TODO.md +++ b/mod/TODO.md @@ -1,770 +1,74 @@ -# LoVisual Mod (combatant-client-26.2) — план переписи - -Мод — форк Combatant Client, переписываем под LoVisual. - -## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат) -- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга -- **Макс. 250 строк на файл** (было >1500) — превышение = разбить на модули -- **Макс. 4 файла на папку** — больше = новая подпапка по смыслу -- **Публичный API можно менять свободно** (решение владельца 2026-09-05): - KISS важнее обратной совместимости; при рефакторинге разрешено менять - сигнатуры, удалять делегирование и мёртвый код, обновляя вызывающих -- **Коммиты — на английском** (решение 2026-09-23, действует с этого момента - вперёд; более старые коммиты на русском не переписываем) -- **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле -- **Никаких this-escape** — конструктор не публикует `this` -- **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel -- **Без пустышек** — файл без реализации не создаётся -- **Каждый этап компилируется** — `./gradlew build` перед переходом к следующему -- **Тесты обязательны** для новых модулей (JUnit) - ---- - -## СТАТУС ПРОБЛЕМ (что вижу сейчас) - -### 1. Ребрендинг не закончен (пункт 2) -- 288 файлов / **1464 упоминания** `combatant` в main-коде -- Классы: `CombatantRenderSystem`, `CombatantRenderEngineBootstrap`, `CombatantRenderPipelines`, `CombatantVulkanBackend`, `CombatantFrameGraph`, `CombatantTerrainVertex` и др. -- Заголовки файлов: «This file is part of the Combatant Client distribution» / копирайт изначального автора - -### 2. Файлы-гиганты (пункт 3) — 12 файлов > 1500 строк -| Файл | Строк | -|------|-------| -| render/engine/renderer/Renderer2D.java | 3379 | -| features/gui/chat/BetterChatRenderer.java | 2870 | -| features/gui/clickgui/settings/UnifiedSettingRenderer.java | 2798 | -| features/module/modules/player/Scaffold.java | 2416 | -| features/module/modules/combat/AttributeSwap.java | 1922 | -| features/gui/clickgui/ClickGuiRenderer.java | 1900 | -| features/module/modules/visuals/NameTags.java | 1791 | -| features/module/modules/combat/ElytraTarget.java | 1711 | -| features/gui/hud/draggable/impl/TargetHud.java | 1687 | -| features/module/modules/visuals/ESP.java | 1520 | -| features/module/modules/combat/AutoCrystal.java | 1516 | -| features/gui/hud/nondraggable/impl/CustomBar.java | 1496 | - -### 3. Runtime / JarReplacement (пункт 4) -- `runtime/` (ClientRuntime, RuntimeGate, ClientRuntimeState, SOFT_PANIC/JAR_REPLACEMENT_PANIC, shutdown/resume-участники) -- `runtime/loader/` (JarReplacementCoordinator, LoaderBridge, PanicOnlyJarReplacementCoordinator) -- **Моё предложение: выкинуть.** Живая замена jar в памяти — источник classloader-утечек, хрупкая, для клиента не нужна (обновление делает лаунчер). Заменить на простой Lifecycle. - -### 4. Security (пункт 7) -- `features/security/BackdoorProtection` — на деле защита от SSRF/translate-инъекций (порт ExploitPreventer, MIT). Полезное, но `enum`-синглтон со статикой. -- `features/relations/StaffTracker` + `StaffHeuristicsMatcher` — детект «стафа» по эвристикам TAB. Сомнительно, решить оставить/выкинуть. - -### 5. Тесты (пункт 6) -- 5 тестов на ~213k строк. Добавлять по мере переписи модулей. - ---- - -## Фаза 1: Ребрендинг (пункт 2) -- [x] **Восстановлен `config/`-пакет** (55+3 файла) из `ref/zip/combatant-client-26.2 (1).zip` — он отсутствовал в форке, без него проект не собирался. Переименован `combatant.client.config` → `dev.loki.lovisual.config`. `compileJava` ✓ -- [x] Заменить `combatant` → `lovisual` в main-коде (1464 вхождения, 288 файлов) -- [x] Переименовать классы `Combatant*` → `LoVisual*` (23 шт., включая `IrisCombatantFrameHooks` → `IrisLoVisualFrameHooks`) -- [x] Заголовки файлов → LoVisual, пересобрать копирайты (GPLv3 форка) -- [x] Проверить `fabric.mod.json` / `lovisual.mixins.json` на остатки -- [x] Проверить строки/сообщения/логи на «combatant» -- [x] JNI-имена в native: `Java_combatant_client_` → `Java_dev_loki_lovisual_` (34 шт.) + FindClass-пути - -### Проверка: `rg -i combatant src` → 0 ✓, `./gradlew build` ✓ (1406 файлов изменено) - -## Фаза 2: Выкинуть Runtime/JarReplacement (пункт 4) -> **Вместе с рантаймом удалить и модуль Panic** (`features/module/modules/misc/Panic.java`), -> интерфейс `RuntimeControlModule`, команду `RuntimeCommand` и все ветки на них -> в `ModuleManager`/`Module`. Panic — единственная реализация `RuntimeControlModule`, -> без него они становятся мёртвым кодом. -- [x] Удалить `runtime/` и `runtime/loader/` (14 файлов) -- [x] Удалить `Panic.java` + `RuntimeControlModule.java` + `RuntimeCommand.java` -- [x] Убрать ветки `instanceof RuntimeControlModule` и `tickRuntimeControllers()` из ModuleManager/Module -- [x] Убрать `RuntimeGate` из точек входа (LoVisual.java, модули, рендер) — заменён на `Lifecycle` -- [x] Заменить на простой Lifecycle-класс (`dev.loki.lovisual.Lifecycle`: INIT → ACTIVE → SHUTDOWN, гейты `canRunRender/Hud/ShaderBridge` + `RenderResourceReadiness`) -- [x] Panic-фичи удалены; `ModuleManager.disableAll()` оставлен как простая утилита -- [x] Мёртвые cleanup-методы удалены (`shutdownForRuntime`, `clearRuntimeReferences`, `VanillaChatRuntimeCleanup`, i18n-ключи panic/runtime) - -### Проверка: `./gradlew build` ✓, тесты ✓ (13), `rg runtime-подсистема` → 0 - -## Фаза 3: Разбивка файлов-гигантов (пункт 3) — ЗАВЕРШЕНА -> Стратегия: публичный фасад исключён из лимита ≤250 (делегирует ~все публичные методы), -> вся логика вынесена в package-private final-хелперы в том же пакете (плоско), -> ≤250 строк каждый. 0 вызывающих файлов изменено. AttributeSwap/Scaffold/ElytraTarget/ -> AutoCrystal удалены в Фазе 3.5 — их разбивать не нужно. -- [x] Renderer2D (3379) → фасад 2150 + 14 хелперов (Blur, Chamfered, Circles, Connectors, - Items, Lines, LiquidGlass, Orbiz, Polygons, Rects, RectsExtra, RectsGlow, SmokeFill, Textures) -- [x] BetterChatRenderer (2870) → фасад 210 + 21 хелпер (LayoutEngine, Viewport, FrameRenderer, - MessageRenderer, GlyphRenderer, InputRenderer, Suggestion*, InputHandler, MouseClick, - ContextMenu, Tooltip, Draw/Font/EditBox/Segment/CommandMerge + 3 model-файла) -- [x] UnifiedSettingRenderer (2798) → фасад 208 + 24 хелпера (по типам настроек + цвет-редактор - из 9 классов: Renderer, SquareRenderer, PresetRenderer/Handler, HexInput/State, Drag, Layout, Math) -- [x] ClickGuiRenderer (1887) → фасад 542 + 15 хелперов (Input, Keyboard, RenderEngine, Lifecycle, - Screen, TextBatch, DrawHelpers, KeyBind, Editor, PickerIcon, TopLayer, TabBar, TabState + Model + Fonts) -- [x] NameTags (1791) → фасад 438 + 11 хелперов (Renderer, Plate, LayoutEngine, Face, Equipment, - Opponent, Effects, LabelBuilder, EnchantFormatter, Cache + Model) -- [x] TargetHud (1655) → фасад 340 + 11 хелперов (Renderer, TargetResolver, Panel, Face, Content, - Text, Arc, Equipment, Effects, Formatter + Model) -- [x] ESP (1538) → фасад 269 + 11 хелперов (EntityCollector, ShaderRenderer/Mask/Color, 2D/3D, - SearchOverlay, Outline, MeshBuilder, ColorUtil + Model) -- [x] CustomBar (1495) → фасад 426 + 11 хелперов (XpBar, JumpBar, Locator*×7, Resolver + Model) - -### Проверка: после каждого файла `./gradlew build` ✓; хелперы ≤250, фасады исключены; -### 0 вызывающих файлов изменено во всей Фазе 3 - -### Долг (файлы >250 вне списка гигантов, в т.ч. новые фасады): -### Renderer3D 433, Module 1181, LoVisualRenderPipelines 1329, DraggableHudElementRegistry 1449, -### Predictions 1363, DynamicIsland 1255, CustomHealthBar 412, CustomHotbar 547, HudNotifier 1116, -### Triangulator 1102, BetterButtons 1033, Themes 1012, SvgMeshBackend 1014, UiPrimitiveRenderer 1042, -### BetterTooltips 948, CustomTabList 829, InventorySwap 1009, ModuleManager 521, LoVisual 441, -### а также многие HUD/module/rendering-файлы 250–800 строк. Разбирать по желанию. - -## Фаза 3.5: Удаление читерских модулей (легит-направление) -> Пользователь решил: LoVisual — легитное приложение. Все читерские модули удаляются. -- [x] combat: удалены AttributeSwap, AutoAnchor, AutoAttack, AutoBed, AutoBow, AutoCrystal, Backtrack, - Criticals, ElytraTarget, Hitbox, KillAura, KTLeave, MaceKill. **Оставлены:** ProjectilePuncher, - PvpCooldowns, Reach, RubberHand, SpearAssist, TPSSync -- [x] movement: удалены ВСЕ модули (AirJump, AutoDodge, ElytraFly, ElytraHelper, Flight, Freeze, - InventoryMove, NoFall, NoPush, NoStun, Parkour, Phase, ReverseStep, SafeWalk, Speed, Sprint, - Step, Strafe, SuperFirework, TargetStrafe, Timer, Velocity, WindJump) — 22 файла -- [x] player: удалены AirPlace, AutoArmor, AutoEat, AutoTotem, Scaffold, XCarry. - **Оставлены:** AutoTool, ClickPearl, FakeLag, NoDelay, NoInteract, ShitDropper -- [x] Осиротевшие хелпер-пакеты: удалены `combat/autocrystal/` (17), `combat/autobed/` (9), - `combat/autoanchor/` (9), `util/block/scaffold/` (10), `BlockPlacer` + тест ScaffoldPlacementGateTest -- [x] Enum-заглушки: `KillAura.RaycastMode/RotationTiming/AngleSmoothMode` → локальные enum в - ProjectilePuncher/Reach; `Criticals.SelectionMode` → удалено; `Hitbox`-зависимость убрана из ESP -- [x] ESP переписан под легит: новые настройки `only_visible` (LOS-луч от камеры), `skip_invisible`, - `only_armor` (без брони не подсвечивать) — по умолчанию `true`, только для игроков - -### Проверка: `./gradlew build` ✓, тесты ✓ (13), остатки только в комментариях/строках - -## Фаза 4: Security (пункт 7) -- [x] BackdoorProtection: убрать `enum`-синглтон, сделать инстансный сервис, ленивый `Minecraft.getInstance()` -- [x] Решить судьбу StaffTracker/StaffHeuristics — согласовать и выкинуть или переписать - (решение: StaffHeuristics удалён, StaffTracker+StaffData оставлены, переведены на INSTANCE-паттерн) -- [x] Покрыть логику фильтров тестами (BackdoorProtectionTest, 11 тестов) - -## Фаза 5: Модули — нормализация под правила -- [x] Прочесать все 131 модуль: убрать `static final Minecraft` в полях (6 шт. уже найдено) - (LegacyHudNotifier, ScissorFunction, ShitDropper, ProtocolUtil, CooldownRender, ClickGuiRenderer.MC - + 7 файлов-потребителей ClickGui-пакета — всё переведено на ленивый `Minecraft.getInstance()`) -- [x] Проверить модули на `System.currentTimeMillis()` → заменить на игровое время - (16 файлов модулей → новый `util/time/GameClock.millis()`, откат на стенное время вне мира) -- [ ] Разнести addon/events/mixininterface по смыслу — ОТЛОЖЕНО: blast-radius 54/41/88 файлов-импортёров, - чистый import-churn без функциональной ценности; делать только отдельным скриптовым проходом - -## Фаза 6: Тесты (пункт 6) -- [x] Юнит-тесты: EventBus (12), конфиг (values: NumberValue/Boolean/String/Set/ItemIdSet/BooleanMap, - ModeValue, EnumValue, RGB/RGBA color; profile: DiffEngine, ValueCodec), - утилиты (LegacyTextUtil — 12). Игровые классы без моков: тесты только на чистую логику. -- [x] По мере переписи модулей — тест на каждый (Текущий итог: 292 теста, все зелёные) - -## Фаза 7: QoL-модули из Delta (легит) — ПЛАН, обсуждаем -> Референс: `ref/delta-26.2-main` (чит-клиент, код обфусцирован — берём только идеи, -> переписываем под наши правила; у Delta 114 модулей, у нас 63). -> Уже перенесено: SeeInvisibles→InvisESP, HandsShader→Chams/Noise, AtmoDawnFog, -> ShulkerPreview, Animations, ItemPhysic (из WorldTweaks), HoldMyItems (фикс ordinal-бага, -> математика в EaseCurves). Реф-исходники: ref/hmi-src, ref/soupvisuals-src. - -### Решения по обсуждению (2026-09-05) -- [x] Wasted — НЕ делаем -- [x] TapeMouse — делаем (пользователь: AFK-фермы, не чит-софт) -- [x] DeathCoords — делаем -- [x] ItemScroller / Arrows — НЕ делаем -- [x] ColorPlus ≈ наш PostFX — PostFX восстановлен и починен (дефолтный пресет был near-identity → добавлен Cinematic + тесты) -- [x] Правило папок: максимум 4 файла (было 3) - -### Перенос (список пользователя) -- [x] **StreamerMode** (misc) — маскирует ники-подобные токены и названия серверов в чате - (общая точка NameProtect: ChatComponentMixin.addMessage), скины заменяются на - стандартные (AbstractClientPlayerMixin.getSkin). Чистая логика — `util/text/NameMasker` - (тест NameMaskerTest). -- [x] **LockSlot** (player, ~36) — запрет выброса из выбранных слотов 1–9 (анти-случайный Q), - опция «только в PVP», предупреждение в чат. Точка: перехват drop-пакета. -- [x] **AutoRespawn** (player, ~28) — авто-возрождение после смерти (tick на death screen). -- [x] **AutoLeave** (player) — выход по триггерам: низкое ХП, N игроков в радиусе - (друзья игнорируются опцией), задержка + предупреждение. Выход через - `mc.disconnect(null, false)`; чистая state-machine `LeavePolicy` (AutoLeaveTest). -- [x] **AutoAccept** (player) — авто-ответ на ТП/клан-запросы: паттерны фраз (SetValue), - команда ответа, «только друзья» (PlayerRelations), задержка, кулдаун ≥500мс. - Пакетный путь: PvpChatEvent (handleSystemChat/handlePlayerChat), отправка - `connection.sendCommand(...)`. Чистый парсер `TeleportRequestParser` (AutoAcceptTest). -- [x] **SoundReducer** (player, ~42) — РЕШЕНО: развитие NoSound, а не новый модуль. - Режимы Mute (старое поведение через ClientPacketListenerMixin) и Reduce — - новая громкость 1–100% через SoundEngineMixin (@Redirect getVolume в - SoundEngine.play). Чистая математика `NoSound.resolveScale` (NoSoundTest). -- [x] **Wasted** (misc, ~100) — ОТМЕНЕНО (решение пользователя) -- [x] **ThirdPerson** (player) — свободный обзор от 3-го лица по удержанию бинда (L, - настраивается), движение игрока не поворачивается. MouseMixin.turnPlayer копит - orbit-yaw/pitch, CameraMixin.update (TAIL) ставит камеру за игрока по орбите - (+ cull-frustum). Орбиты сбрасываются, когда бинд отпущен. -- [x] **UseTracker** (player) — уведомления: мои тотемы (N осталось), тотемы других, - эффекты оппонентов (группа: invisibility/speed/strength/regeneration/fire_res/weakness). - Пакетный путь: ClientboundEntityEventPacket (event 35) + ClientboundUpdateMobEffectPacket. - Тест UseTrackerTest. -- [x] **NoServerPack** (misc) — авто-отклонение принудительного серверного ресурспака - (спрашивать/отклонять всегда). Точка: ClientCommonPacketListenerImplMixin. -- [x] **NoFriendDamage** (combat, ~19) — отмена атаки, если цель — друг (PlayerRelations). -- [x] **TapeMouse** — ОСТАВЛЕН (решение владельца 2026-09-08): автоклик по таймеру - (Attack/Use) для AFK-ферм, интервал, опция «только при зажатии». Считаем - допустимым исключением из Фазы 3.5 по явному решению владельца. - -### Кандидаты из отчёта (не в списке пользователя — обсудить) -- [x] **DeathCoords** (~27) — координаты последней смерти в чат. Дёшево, полезно. -- [x] **ItemScroller** (misc) — shift-перекладка предметов колёсиком в контейнерах: - QUICK_MOVE клик по слоту под курсором, иначе слот хотбара (опция). - Точка: AbstractContainerScreenMixin.mouseScrolled; чистая логика - `ScrollMovePolicy` (ScrollMovePolicyTest). -- [x] **Arrows / off-screen индикаторы** — ОТМЕНЕНО (решение владельца 2026-09-08). - -### Проверка после каждого модуля: `./gradlew build` ✓, тесты на чистую логику ✓ - -## Фаза 7.5: Команды — префикс % + автокомплит -- [x] Префикс команд сменён `@` → **`%`** (старый `@` принимается как legacy). - Источник истины — `features/command/CommandPrefix` (CHAR/STRING/isCommandLike/strip); - вызовы обновлены: CommandManager.handle/suggest, ScreenMixin (RunCommand), - BetterChat (TabHandler, SuggestionState, SuggestionRenderer, ContextMenuHandler), - usage-строки и RunCommand-ссылки всех команд (help/modules и др.). -- [x] Аргументный автокомплит дополнен usage-подсказками: если `suggest()` команды - не дал вариантов на позиции — показывается подсказка вида `%toggle ` - (`features/command/UsageHints`, парсит `<...>`/`[...]` из usage-строки). - Существующий UI автокомплита BetterChat (стрелки/Tab/клик) работает как раньше. - -### Проверка: `./gradlew build` ✓, `./gradlew test` ✓ (CommandPrefixTest, UsageHintsTest, -### CommandDiscoveryTest обновлён) - -## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН -> Правило ≤250 уже есть (Фаза 1–3), но долг: ~280 файлов >250 строк. -> У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) / -> inject(accessors, invokers, mixin). Наша пакетная структура сопоставима — проблема в размере файлов. - -- [ ] Топ-гигантов к разбивке (после Фазы 3): LoVisualRenderPipelines 1527, - DraggableHudElementRegistry 1449, Predictions 1363, DynamicIsland 1254, Module 1181, - HudNotifier 1116, AltManager 1115, Triangulator 1102, TargetESP 1061, UiPrimitiveRenderer 1042 -- [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤250, 0 изменений вызывающих, - `./gradlew build` после каждого; по мере касания файлов в Фазе 7 — разбивать на ходу -- [ ] Facades >250 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам) -- [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша - -### Проверка: `./gradlew test` зелёный - -## Фаза 8.5: Аудит структуры (правила ≤250 строк / ≤4 файла) — ПОЛНЫЙ СПИСОК НА РЕФАКТОРИНГ -> Снимок 2026-09-09 (`src/main/java`, 1459 файлов / 197k строк). -> **Гейт перед Фазой 9.2 (Optimize)**: движковые файлы, которые Optimize будет трогать, -> должны быть разбиты заранее. 9.1 (мёртвые грузы) от этого НЕ зависит — идёт своим порядком. -> Стратегия та же, что Фаза 3/8: фасад + package-private хелперы ≤250, 0 правок вызывающих, -> билд после каждого файла. Фасад-исключение (Renderer2D) — см. оговорку ниже. - -### Итоговая картина -- **Файлы >250 строк: 244 шт.** → >800 строк: 26 | 400–800: 78 | 201–400: 140 -- **Папки >4 файлов: 86 шт.** -- Разбивка больших файлов по зонам: features/gui — 73 | render/engine — 54 | - features/module — 35 | util — ~35 | mixins — 3 | config — ~6 | прочее — остальное - -### 8.5.1 Папки >4 файлов — топ должников (разбить на подпапки по смыслу) -- [x] `mixins/` — 108 файлов → leaf-пакеты (entity/blaze3d/input/world/network/gui/hud/render), - mixins.json перегенерирован, FQN-строки LoVisualMixinPlugin обновлены. Коммиты 94d5af7, 3c76817. -- [x] `mixins/accessors/` — 45 → 12 групп (entity/renderer/world/state/packet/inventory/...). Коммит 3c76817. - Также разбиты iris/sodium/security на подпакеты. -- [x] `features/module/modules/visuals/` — 67 → esp/nametags/hats/effects/weather/scene/camera/ - player/items/fx + lyrics/lrc. Конфиг-id НЕ менялись (проверено). Коммит 073ac26. -- [x] `features/gui/clickgui/settings/` — 51 → core/bool/mode/slider/text/text-list/group/bind/ - color(+render/preset/hex)/i18n/render/skin/owner/cooldown/util. Коммит 9db1fec. -- [x] `features/gui/hud/draggable/impl/` — 40 → stats/metrics/status/combat/panels/targethud(+/render, - render/parts)/scripted + hudnotifier/model, targethud-модель свёрнута в класс с record-ами. Коммит 6b89e40. -- [x] `events/impl/` — 32 → input/movement/world/network/entity/pvp/render/player; - events/ верхний уровень: core(Event,EventBus,Events) + meta(EventHandler,UsedImplicitly). -- [x] `features/gui/chat/` — 31 → `render/`, `suggestions/`, `menu/`, `model/`. -- [x] `render/engine/uniform/impl/` — 28 → по потребителям (`uniform/impl/pipeline/`, `uniform/impl/effects/`). -- [x] `render/engine/renderer/ui/` — 28: верхний уровень разнести по уже существующим - `draw/`, `runtime/`, добавить `blur/`, `batches/`. -- [x] `render/engine/text/` — 24 и `text/backend/` — 12 → `text/` разбить на `text/render/`, - `text/layout/`, `text/msdf/`. -- [x] `render/engine/renderer/ui/runtime/script/` — 23, `ui/draw/` — 23 → внутри по 4-файловым кластерам. -- [x] `features/gui/hud/nondraggable/impl/` — 20 (+`impl/tab/`) → `impl/overlays/`, `impl/tablist/`. -- [x] `features/gui/clickgui/` — 20 на верхнем уровне → всё в существующие `layout/`, `sections/`, - `picker/`, `protocol/`, `util/`. -- [x] `render/engine/command/` — 17, `render/helpers/` — 16, `features/command/impl/` — 16, - `config/values/` — 16 → по 2-3 подпапки (типам: `values/numeric/`, `values/color/`…). -- [x] `render/engine/profiler/` — 14, `modules/player/` — 14, `features/gui/hud/` — 14, - `addon/` — 14 — средние: раскидать при касании. -- [x] Хвост (5–13 файлов, ~55 папок): `util/pvp` 13, `util/projectile` 13, `render/iris` 13, - `config/profile` 13, `features/module/misc` 12, `features/module` 12, `config` 12 и т.д. — - ЗАКРЫТ выделенным проходом (коммиты 8846d53…303b17e, волна 8.5.1 завершена). - Снимок после волны: 0 папок >4 файлов в `src/main/java` (скан 2026-09-10). - Замечания: command-discovery рекурсивный (ClassGraph acceptPackages), конфиг-id - и ключи i18n не менялись; package-private члены, ставшие кросс-пакетными, - расширены до public/protected; svgMsdfTool-импорты починены после split svg. - -### 8.5.2 Файлы-гиганты >600 строк — приоритетная очередь разбивки (53 файла) — ЗАВЕРШЕНА -> Проверено 2026-09-28: `find src/main/java -name "*.java" | xargs wc -l` → 0 файлов >600 строк -> (самый крупный — Renderer2D.java 474, осознанный фасад-делегат, исключение Фазы 8). Все 53 -> пункта очереди ниже закрыты. Остаток структурного долга — 8.5.3 (201–400 строк), не марафон, -> а правило «трогаешь файл по другой задаче — первым коммитом режешь до ≤250». -> Формат: файл (строк) → идея разбивки. Порядок сверху вниз. Facade-оговорка: -> Renderer2D остаётся фасадом-делегатом (правило Фазы 8), но его вспомогательные -> классы (kawase/blur/glass) — вынести из OrderedUiBatcher (935) в `ui/blur/`. -- [x] Renderer2D 2179→1597 → ГОТОВО 7b848ddd+464f7868+098c5515 (2026-09-25): ФАСАД; - далее 1597→475 наследованием (волна 2026-09-25 ниже) - (исключение по правилу Фазы 8) — делегирование оставлено; −234 мёртвые шимы - совместимости, −271 недостижимые перегрузки liquidGlass/item/Corners, - −115 прочие мёртвые перегрузки+импорты; макс. тело метода 35 строк (блоков >250 нет), - хелперы в `renderer/renderer2d/` (Rects2D/Strokes2D/…), build+test ✓ -- [x] LoVisualAltManagerScreen 1113 → ГОТОВО (2026-09-10): 11 файлов `mainmenu/alt/` - (Screen 132, Controller 250, Ops 59, Anims 108 + auth/ 171 + render/ 157/156/139/100 - + render/cards/ 178/166), все ≤250, build+test ✓ -- [x] UiPrimitiveRenderer 1053 → ГОТОВО (2026-09-10): фасад 188 (коннекторы) + - render/primitive/ 4 (Context 153, Props 158, Painter 145, GlowPainter 147) + - render/rect/ 2 (RectPainter 150, CornerPainter 150), build+test ✓ -- [x] BetterButtons 1032 → ГОТОВО 7e86f8d: 388 + 7×≤250 (state/hover/parallax/queue/render) — extraction+tooltip+render → `hud/nondraggable/impl/buttons/` -- [x] InventorySwap 1031 → УДАЛЁН 5acafc8: чит-инфра, заменён ShitEsp + хотбар-only — по стратегиям свапа; чистая логика отдельными классами с тестами -- [x] SvgMeshBackend 1014 → ГОТОВО c11ebe3/aafe7de: 1020→267 + Parser181/Geometry173/Texture223/Render35/Cache25 — parser/triangulate/cache разнести -- [x] Themes 1012 → ГОТОВО e51a610: 1012→562 + 3×163 пресета — реестр тем в `theme/presets/` (по 4 темы/файл), дефолты отдельно -- [x] WorldParticles 991 → ГОТОВО 1d757a6+5a463d7e: 999→821→260 (фасад) + ambient/render/ 3×≤182 - (Batches/Bubble/Funnel) + ambient/particle/ 197 + geometry/ доработаны (offset/multiplyAlpha/STACKS) — - типы частиц в `visuals/effects/ambient/` -- [x] Module 989 → ГОТОВО c8da336: 1031→139 + 4×≤134 (ext/) — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!) -- [x] LoVisualAddonManagerScreen 989 → ГОТОВО de56b4b9: 989→112 + 10 файлов mainmenu/addon/ - (Controller 164, Ops 112, PanelAnims 56, auth/AddonScanState 52 + render/ 115/162/183 - + render/cards/ 79/88), все ≤250 — по образцу AltManager -- [x] Chams 976 → ГОТОВО 78b5335: 985→129 + 4×≤195 — режимы рендера в подклассы-политики (фасад модуля тонкий) -- [x] ClickGuiPickerState 962 → ГОТОВО 234f0d9: 970→166 + 4×≤140; CombatProtocolHeuristicsEditorState 932→79 +4×≤192 03ae4a6 — state/actions хелперы -- [x] ModulesMenuScreen 960 → ГОТОВО 234f0d9: 966→145 + 4×≤178 — рендер категорий отдельно от состояния -- [x] UiMeshGeometry 954 → ГОТОВО sub-agent: 956→99 + 4×≤135 — по примитивам геометрии -- [x] BetterTooltips 953 → ГОТОВО 0b8ea41: 960→181 +4×≤199; OrderedUiBatcher 935 → ГОТОВО 6e0bf7f: 944→188 (→ batches + blur), Scoreboard 924, - Rain 893 → ГОТОВО sub-agent: 903→159 +4×≤162; CompactHudStatModel 893 → ГОТОВО 8979140: 894→125 +4×≤175; TazikHat 859 ✓ ✓ закрыт портированием в - Cosmetics built-in (2026-09-11, см. Фазу 7), - ShaderPatchCompiler 843 → ГОТОВО 2b55fbc: 843→66+4×≤174; OnlineRelationPlayerPickerComponent 837→74+4×≤192 17fc665; CustomTabList 830 → ГОТОВО 9e9b71d: 834→136+4×≤166, - GameRendererMixin 830 → ГОТОВО f45f46e: 830→344 (только хуки+Host/BobInvoker) + - mixins/render/main/handlers/ 4×≤250 (ProjectionHelper/WorldEngine/WorldPost/WorldDebug), Predictions 805 → ГОТОВО (см. хвост) -- [x] Хвост 400–800: ЧАСТИЧНО — ConfigProfilesComponent 799→125 d95bfec, HitEffect 770→120 5e1ce05, PlayerMovementSimulation 757→157 4b253d1, ProjectilePuncher 747→104 fba4f7d, MediaPlayer 739→174 8ce3927, Crosshair 737→143 4cf0970, Itemizer 699→193/Cooldowns 707→151 11245ac, Potions 617→176 2e4912a, Admins 702→118 f18cb96, Keybinds 611→157 cc18feb, ModuleList 499→181 52327dc, Radar 392→161 f580373, Fps 309→168 5df2cc0, Memory 304→151 de7cc2b, Tps 339→154 e167f0c, Ping 309→145 15d04cb, SpeedBps 334→156 b78a638, GameTime 314→150 922d3bc, SystemTime 300→136 3cb6e40, Coordinates 337→174 9412982, Armor 324→162 932639d, Inventory 476→153 f4124f4, BlockHighlight 692→271+4×≤250 9f76e5a5 (scene/highlight/), см. /tmp-снимок в истории команд: ConfigProfilesComponent 791, - HitEffect 770, PlayerMovementSimulation 755, ProjectilePuncher 746, MediaPlayer 739, - Crosshair 737, MeshBuilder 729, SettingsPanelComponent 703, Admins 702, Cooldowns 701, - Itemizer 699, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673, - CosmeticModel 671, MenuScreen 667, GlStencilFramebufferSupport 663, ConfigSerializer 663, - DynamicIsland 661, ReimaginedVisual 660, RelationsComponent 659, UiStyle 640, Potions 617, - ModuleComponent 612, MsdfFont 606, Keybinds 603, MediaSessionService 594 и др.) -- [x] Волна 2026-09-24 (HUD/settings): NameTags 453→223 (nametags/impl/ Keys+Collector+Projection) b1414d7a, - MainSettingsComponent 448→291 (+Scroll+Model) 3f0d452f, LinuxMediaSession 441→148 3d11bb4e, - CustomTextRenderer 229→195 (GradientTexts), CustomBar 438→185 (+CustomBarModel константы - +bars/impl/BarSettings, forwardеры удалены, mixins правлены на реальные рендереры) 4e442fb7, - CustomHealthBar 406→146 (+bars/impl/HealthBarPainter+HealthScriptRenderer, dead-code удалён) a3415435. - bars/ закрыт: все файлы ≤250, impl/ = 3 файла. -- [x] Волна 2026-09-25 (реестры): Renderer2D 2179→1597 (см. строку 8.5.2 выше, - 7b848ddd+464f7868+098c5515), LoVisualRenderPipelines 432→311 9577ed3d - (SHADER_* 105 констант → pipeline/ShaderIds 130, мёртвые импорты удалены; - логика ≤26 строк, фасад-исключение), build+test ✓. -- [x] Волна 2026-09-25 (наследование фасада): Renderer2D 1597→475 (leaf), цепочка - Renderer2D extends Item(124) extends Glass(183) extends Shapes(313) extends - Path(245) extends Rounded(315) extends Base(23); API вызывающих не менялся - (0 правок callers, static-import ITEM_OVERLAY_* и Renderer2D.LiquidGlassPreset - работают по наследству); renderer/facade/ = 4 файла, facade/shape/ = 2 ✓ лимит; - Base.self() для this→Renderer2D-делегатов; семейства >250 — фасад-части - (исключение TODO:Facades); 37c7c410+64e23ef8+9ae72eeb+2b8c40a3+6204017b+ - 9fffc58d, build+test+checkstyle ✓. - -### 8.5.3 201–400 строк (~140 файлов) — проход «касаться = чинить» -- [x] Волна 2026-09-30 (топ-4 по объёму, выбор владельца): каждый файл — отдельный - коммит, фасад + хелперы, публичный API и вызывающие не тронуты: - ConfigDiffPanelComponent 393→167 (фасад + `config/diff/` - HeaderRenderer/BodyRenderer/Scrollbar) 7e8b6b64; - RotationManager 391→199 (фасад + `rotation/internal/` - RotationState/RotationStepper/RotationPackets, @EventHandler-методы остались - на фасаде — регистрация не менялась) 3e57542b; - ScriptedTriangulatorHudPanel 386→189 (записи Panel/Palette остались - вложенными → `ScriptedTriangulatorHudPanel.Panel` у вызывающих работает, - рендер → ScriptedTriangulatorRenderer, пропсы/сигнатуры → `scripted/panel/`) - 76bd8e95; BetterButtons 381→197 (настройки → ButtonStyleSettings по образцу - BarSettings, иконки → ButtonIconRenderer, −3 мёртвых метода без вызовов) - 71f779c8; `./gradlew build` ✓ test 438 ✓ checkFolderLimit ✓ на каждом шаге; - файлов >250: 155 → 151 -- [ ] Не делать отдельным марафоном: при правке файла по другой фазе — первым коммитом - разбивка на ≤250, вторым — целевое изменение (иначе diff-каша, правило Фазы 8) - -### 8.5.4 Порядок и критерии готовности -- [x] Очередь: 8.5.1 крупные папки (mixins, visuals, clickgui/settings, hud impl) → - 8.5.2 гиганты >800 → далее по убыванию. Каждая папка/файл = отдельный коммит. - → 8.5.1 ЗАВЕРШЕНА (2026-09-10): 0 папок >4 файлов. -- [ ] После каждого шага: `./gradlew build` ✓, `./gradlew test` ✓; конфиг-id модулей и - ключи сеттингов НЕ ломаем (совместимость .lvcfg); скрипты-переименования пакетов - не трогают string-literals id — проверять grep'ом после moves. -- [x] Критерий закрытия для старта 9.2: ноль файлов >250 в путях, которые трогает Optimize - (Renderer2D-секции blur/glass, OrderedUiBatcher, HUD-имплементации bgEffect, - MsdfFont/CustomTextRenderer), и ноль папок >4 в этих же пакетах. Полный ноль по всем - 244/86 — долгий хвост, 9.2 им не блокируется. - → 2026-09-24: MsdfFont 155 ✓, OrderedUiBatcher 180 ✓, CustomTextRenderer 229→195 ✓ - (glyph-pass вынесен в GradientTexts). Остаток гейта — bgEffect-HUD >250: - TargetHud 333, Triangulator 304, Scoreboard 233 — ВСЕ фасады Фазы 3 с helper-пакетами - (targethud/render/, triangulator/{render,Solver,Tracker,View}, panels/scoreboard/) → - исключение по правилу фасадов. ГЕЙТ 9.2 ЗАКРЫТ. -- [ ] Обновить этот раздел при подходе к волне (отмечать [x], доснимать счётчики). - -## Фаза 9: Оптимизация FPS (аудит 2026-09-09) — ПЛАН -> Диагноз: у клиента есть «налог каждый кадр», который платится даже при -> выключенных модулях (друг: просадка FPS на чистом конфиге, Windows). -> Решение владельца: мёртвые грузы — ЧИНИМ в коде; авто-деградации по FPS НЕ ДЕЛАЕМ; -> вместо неё — ручной модуль **Optimize** (включил → порезали стекло/blur/тяжёлые проходы). - -### 9.1 Мёртвые грузы (чиним в коде всегда, независимо от модулей) — приоритет -- [x] **glGetError на каждом render pass** — ГОТОВО 2026-09-11 (коммит 137121e): гейт по clip-active || stencil-debug — `GlStencilShapeClipBackend.endRenderPass` - (`:140`) и `beginRenderPass` (`:98/:104/:110/:124`) дёргают `checkGlErrors` - (`:428-434`) на КАЖДЫЙ проход (миксин `CommandEncoderMixin:102-112` висит на - submitRenderPass → это и все ванильные/Sodium проходы). `glGetError` = синк с - Windows-драйвером, несколько мс/кадр. Фикс: проверять только если - `mode != DISABLED || renderPassAttachmentRequired`, либо под dev-флагом; - в релизе — no-op. -- [x] **`lovisual$isVulkanBackend()` на каждый submitRenderPass** — ГОТОВО (c9a65b1), кэш Boolean - (`CommandEncoderMixin:66-74`) — `getDeviceInfo().backendName().toLowerCase()` - заново на кадр×проходы. Фикс: вычислить один раз, закэшировать boolean. -- [x] **PreTranslucentDepth — полноэкранный depth-copy каждый кадр без нужды** — ГОТОВО (b747b47): demand-флаг requestForNextFrame()/takeRequested() у потребителей getDepthView(For), capture в vanilla- и Iris-путях по запросу (1-кадр задержка, фолбэк на live view) — - `LevelRendererMixin:275-283` гейтится только «нет Iris». Фикс: гейт - `needsPreTranslucentDepthCapture()` по образцу `WorldSceneDepth` - (`LevelRendererMixin:160-218`); потребители: `PostProcessGraphResources:61`, - `LoVisualVisuals:79`, `WorldPassCompiler:174`. -- [x] **copyMainColor каждый кадр ДО гейта** — ГОТОВО (118eb2a): ранний return по liquidGlassBlurRequested — `UiBlurResources.captureWorldSource` - (`:116-123`): полноэкранный color-copy делается до проверки - `liquidGlassBlurRequested` (`:122`). Фикс: переставить — copy только когда - blur реально запрошен. -- [x] **Строки профайлера при выключенном профайлере** — ГОТОВО (aa55c4d): 2-arg scope()/section()/postPass(prefix,suffix) в ProfilerPhase/RenderProfiler2D/3D/RenderCostProfiler (DEV-гейт конкатенации) — конкатенации на call-site - каждый кадр: `PostProcessGraph:59` (`"graph:" + phase`), `ModuleManager:219-221/:290` - (`"3d:module:" + m.name()`). Фикс: ленивые supplier'ы / ранний isEnabled-check. -- [x] **Мелочь, по ходу**: `new PoseStack`/`new Matrix4f`×2/`new Frustum`×2 в кадре - (`GameRendererMixin:203-239/:341-347/:609-617`) — закэшить переиспользуемые копии. Frustum-часть отменена (da6525c): @Invoker("calculateFrustum") не резолвится на remapped-рантайме = краш; scratch-кэш матриц/PoseStack остался. - -### 9.2 Модуль **Optimize** (misc) — ручной рубильник экономии -> ⛔ ГЕЙТ: не начинать, пока не закрыта Фаза 8.5 (рефакторинг-аудитStructure ниже) — -> Optimize трогает Renderer2D/OrderedUiBatcher/HUD-имплементации, все они >250 строк. -> Идея владельца: один тумблер «хочу максимум FPS». НИКАКОГО автовключения по FPS. -> Пока активен — движок и HUD работают в лёгком режиме. -- [x] Каркас — ГОТОВО ade82f06: `modules/misc/optimize/Optimize.java` (в подпапке — лимит - `checkFolderLimit`=4 файла в `misc/`), 4 bool-тумблера (default on), тест `OptimizeTest`. - Отклонения от плана при реализации: - - `no_glass` — **9246e010**: не мутим bgEffect (конфиг элементов неизменяем) и не - ищем `prepareSharedBlurInternal` (метода больше нет). Гейты: `Blur2D.source()` - → null при no_glass (вместе с `FpsOptimizer.isGlassDisabled` → все - blurRect/glassBlurRect мимо) + ранние return в - `UiBlurResources.requestLiquidGlassBlur/requestBeforeNextShapeClip` - (флаги не ставятся → `captureWorldSource` early-out → нет полноэкранного copy, - kawase и prewarm). Побочный эффект: гасит и низкоконтрастный blur текста - (осознанно — «все glass/blur off» по замыслу тумблера). - - `lite_post` — **52463246**: MotionBlur `SHADER_TAPS=11 → LITE_TAPS=5` - (план «15→5» устарел — тапов уже 11), `historyValid = hasHistory && !lite` - (temporal-история off = ещё ~10 neighborhood-fetch'ей в шейдерный early-out; - цена: хвостов истории нет, остаётся чисто пространственный блюр). - DoF: `taps = lite ? LOW : quality` (LOW=8; план «32→8» устарел — квалити 8/14/22). - - `lite_sky` — **c403cb53**: `SkyboxShaderPasses.LITE_SKY_LAYER_MASK=0xF00F` - (базовые слои 0-3 + звёзды 12-15, чистит nebula/curtains/bursts/polar/water/…), - `layerMask &=` при включённом тумблере; в `reimagined_skybox.frag` 4 - безусловных fbm + bursts-блок обёрнуты в layer-гейты; pulsar/thunder fbm - 4→2 октавы через `u_Thunder.w` (ранее всегда 0). Тест `SkyboxShaderPassesTest`, - все 3 шейдера проходят `glslangValidator -S frag`. - - `lean_hud` — **8746074d**: `GradientTexts.horizontalFade` → plain-текст (без - per-glyph `new String` и gradient-лямбд; marquee-clipped — clip остаётся, - fade-градиенты гаснут) в Custom и Vanilla-реализациях; Fps digit-roll off - (`FpsAnimator.widerText/digitProgress` → settled-прогресс). - Не в scope: `GradientTexts.gradient/quadGradient` (визуал списка модулей), - аллокация в `HorizontalFade.glyphAdvance` (её просто обходит lean-гейт). -- [x] Механизм «как движок узнаёт» — ГОТОВО ade82f06: `render/engine/optimize/ - OptimizeState.java` (enum-utility, 4 volatile boolean, `apply/reset`, чистый - статик без состояния модуля), движок читает геттеры, модуль пушит в onEnable/onFrame. -- [x] Взаимодействие с 9.1 — ГОТОВО: фиксы 9.1 не завязаны на Optimize и продолжают - работать всегда; Optimize только добавляет гейты сверху. -- [x] i18n en_us + ru_ru — ГОТОВО ade82f06: `module.optimize.*` + `setting.optimize.*` - (4 ключа, симметрия en/ru проверена). -- [x] Нотификация при первом включении — ГОТОВО ade82f06: once-флаг в инстансе модуля + - `Notifier.info("Optimize: " + summaryOf(...))` со списком включённых тумблеров. - -### 9.3 Проверка -- [x] `./gradlew build` после каждого пункта; `./gradlew test` зелёный — выполнялось - после каждого из 5 коммитов 9.2 (итог: build+test зелёные на 8746074d). -- [x] Порядок: 9.1 (по одному пункту на коммит) → 9.2 — 9.1 закрыта (коммиты выше), - 9.2 закрыта ade82f06..8746074d. -- A/B-мерка до/после 9.1 на чистом конфиге — УБРАНА из плана (решение владельца - 2026-09-25): требует запуска Minecraft на Windows, задачей не является. - -## Фаза 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 отмечен - -## Фаза 11: Аудит attack-визуалов — sacura 1.21.4 / GodWeer-26.2 (2026-09-22) -> Референсы распакованы в `ref/clients/sacura-1.21.4` и `ref/clients/godweer-26.2`. -> Вывод по большинству эффектов: у нас уже полноценнее (больше режимов, единый render -> engine с пайплайнами вместо сырого GL). Ниже — конкретные пробелы, которые реально -> стоит доделать; всё остальное сравнённое (HitEffect, KillEffect, JumpCircles, -> Hitboxes, ItemPhysic, ViewModel offsets) уже на уровне или выше обоих референсов. - -- [ ] **ViewModel — `Custom` swing-режим.** sacura (`SwingAnimationsModule`) имеет режим - "Кастомная" с explicit слайдерами угол/наклон. У нас 8 фиксированных пресетов - (Hammer/Chop/Arc/Double/Shake/Shove/Slash/Thrust), но нет свободно настраиваемого. - Также посмотреть на sacura-режимы "Возвращение"/"Возвращение к себе"/"Круговая" — - возможно стоят портирования как доп. пресеты, если визуально отличаются от наших. -- [ ] **HitEffect — опциональный distortion/glow post-process слой.** godweer - (`HitEffectRenderer`) кроме частиц кладёт screen-space distortion+glow слайдеры. - У нас частицы (Bubbles/Wave/Impact) есть, post-process слоя над ними нет. - Паттерн `PostProcessPass` уже есть (см. `TotemFX`), можно переиспользовать — - но не блокирующая, это чистый апгрейд поверх уже более богатой системы. -- [ ] **JumpCircles — easing на появление/исчезновение.** sacura использует `EaseBackIn` - для пружинки при спавне/затухании круга. Проверить `render/engine/math` — если - easing-утилита уже есть, подключить к текущему fade вместо линейного. -- [ ] **KillEffect — through-walls поведение.** godweer (`PopEffect`) имеет явный toggle - "сквозь стены" для beam/взрыв эффектов. Проверить DepthMode у Lightning/Orthodox - beam в нашем KillEffect — сейчас режется окклюзией или нет; если режется и это не - специально — добавить toggle по аналогии. -- [ ] **Trails — живое сравнение с sacura `TrailsModule`.** Не разбирался по коду в этом - заходе (визуальный эффект, решение лучше принимать посмотрев обе стороны вживую - в игре, а не по исходникам). - -### Итог: ничего из sacura/GodWeer не даёт принципиально нового attack-эффекта, -### которого нет в LoVisual — расхождение только в деталях полировки (easing-кривые, -### доп. custom-режим свинга, возможный distortion-слой, through-walls toggle). - ---- - -## Фаза 12: Сессия 2026-10-01 — новые визуалы, меню, войсчат, автокомплит, починка запуска - -> Всё за 2026-10-01 лежит ОДНИМ коммитом (владелец просит большие коммиты, не дробить). -> Игру в этой сессии запускал только владелец (`./gradlew runClient`, из каталога `mod/`); -> всё, что ниже помечено «не проверено вживую», видно только в игре. - -### Сделано (код + сборка + тесты зелёные) - -- [x] **TargetESP: режим «Созвездие» (`constellation`).** Звёзды на наклонной орбите вокруг цели, - линии между ними, свечение. Настройки: звёзды 3-24, скорость, орбита, линии, размер, свечение. - Геометрия: `combat/targetesp/constellation/` (чистый `ConstellationLayout` + тест). - `TargetESP.java` сокращён 329 -> 161 строка: отслеживание цели и плавное появление вынесены в - `targetesp/state/TargetEspTracker`, цвет в `TargetEspColors`. -- [x] **JumpCircles переписан** (саб-агент): 7 новых форм (Звезда, Ромб, Спираль, Гексагон, Волна, Крест, - Лепестки) к Круг/Кольцо/Пузырь; общая анимация (`CircleAnimation`), цвета из `AnimatedRenderColors`, - круг рождается на высоте земли в момент отрыва. Пакет `visuals/player/jumpcircles/` (+ `shape/`), - 12 тестов. Удалены настройки `ease_out`, `pulse_max_size`, `pulse_lifetime`. -- [x] **KillEffect: режим «Кровь» (`Blood`)** по архиву killeffect.zip. Капли с физикой, отскоки от стен, - лужи на полу, мелкие брызги; через наш `Renderer3D`. Пакет `hits/blood/`. Настройки: цвет, - количество, сила, размер, время пятен, тип разлёта (Burst/Fountain/Ring/Spray; Spray летит от атакующего). -- [x] **Главное меню: тип «Hero»** по архиву Ya_Lublu_Femboyev.zip (экран Vectra). Настройка `menuLayout` - (`orbs` по умолчанию / `hero`), фабрика `MainMenus`, экран `mainmenu/hero/*`, чистая логика - `hero/logic/*` + тесты. Интеграция: альт-менеджер, `SkinManager`, наши темы. -- [x] **Simple Voice Chat (мягкая совместимость, API 2.6.24).** `compat/voicechat/` (плагин, `VoiceState`, - `VoiceChatBridge`), модуль `VoiceChat` (кольца и свечение над говорящими, крест при заглушённом - микрофоне), HUD-панель `VoiceChat` (список говорящих + статус микрофона). API не попадает в jar. -- [x] **`render/engine/renderer/quick/Draw`** — короткий статический фасад над `Renderer2D.COLOR` - (rect, gradient, outline, glassOutline, shadow, circle) + `GlassEdge` с тестом. - Заметка: у `Renderer2D` короткие перегрузки `roundedRect(x,y,w,h,radius,argb)` уже были, - 6-й параметр в длинной форме это `softness`, а не alpha. -- [x] **Автокомплит `%` в ванильном чате** (как `#` у Baritone): `mixins/gui/chat/CommandSuggestionsMixin` - подставляет наши команды/аргументы в попап по мере набора; подсказки аргументов `` - показываются серым текстом (`command/suggest/SuggestionPlan` + тест). BetterChat идёт своим путём. - Раньше подсказки работали только в BetterChat. -- [x] **Починка запуска клиента** (игра не стартовала с августа, все пять дефектов найдены по логам runClient): - 1. MixinExtras 0.5.4 падал на `@Redirect(at=...)` (ClassCastException при трансформации `Entity`): - добавлен `mixinextras-fabric:0.5.5` (`gradle.properties: mixinextras_version`), вшит в jar через `include`. - 2. `ServerPolicyClient`: `registerReceiver` -> `registerGlobalReceiver`. - 3. Классы-помощники лежали в пакете миксинов (Mixin запрещает их загрузку): вынесены в - `render/gamerenderer/` и `mixinhandlers/{player,hud,network}/`, сделаны public. - 4. `Themes`: `STORE` создавался раньше таблицы пресетов (NPE в `ThemeStoreIO`): порядок инициализации. - 5. `MainSettingsComponent.rebuildSettings`: `addAll` в неизменяемый список (падал ClickGUI по RShift). -- [x] 2026-10-01 все сегодняшние коммиты объединены в один по просьбе владельца. - -### Отзывы владельца после запуска (скриншоты) — СДЕЛАНО в коде, ждёт проверки в игре - -Итог 2026-10-01 (сборка и тесты зелёные, визуально не проверено): -- Тултипы: причина была в том, что пользовательский тултип, перекраска, фон и предпросмотр шалкера читали - собственные флаги и игнорировали переключатель модуля; теперь везде `isEnabled()`. Синие строки - категорий красятся акцентом темы (`TooltipColors`). Тема по умолчанию `Themes.THEME_DEFAULT = AMBER` - (сохранённый выбор пользователя не трогается). -- Описания: ~98 новых ru/en (62 модуля, 36 HUD), `HudElement.getDescription()`, тест `DescriptionCoverageTest`. -- ClickGUI: плашка с описанием над колонками (`ModulesMenuDescriptionRenderer`, `DescriptionLines`). -- Hero-меню: раскладка по измеренному тексту (`TextMetrics`, `HeroLayout`), кнопки с SVG-иконками - (`hero/paint/*`), orbs: подпись под кнопкой при наведении (`MainMenuHoverLabel`). -- Настройки меню: новая категория «Главное меню» (`MAIN_MENU`), `MainConfig.getMainMenuSettingDefs()`. -- Проверить в игре: положение и размер плашки описания, вид amber-тултипа, иконки и шрифты Hero-меню, - подпись orbs-кнопок, видимость настроек (`visibleWhen`) в категории «Главное меню». - -Задача А (форк, тултипы и описания): -- [x] **Тултип предметов (BetterTooltips) странный и не выключается.** При первом запуске у камня синие - строки-категории («Building Blocks / Natural Blocks») и тёмная панель; даже при выключенном модуле - стиль остаётся. Нужно: найти причину (миксин `mixins/hud/tooltips/TooltipRenderUtilMixin` - применяет стиль без проверки включённости / кэш `TooltipState`), при выключении тултип строго - ванильный, при включении аккуратный стиль. **Стиль по умолчанию должен быть `amber`, а не `classic`** - (`Themes.THEME_AMBER`); старые конфиги не ломать. -- [x] **Нет описаний модулей и HUD-элементов в настройках.** На всех карточках HUD/UI написано - «Module description is missing.». Нужно добавить описания ru/en для каждого `@ModuleInfo`-модуля и - каждого HUD-элемента (ключи `module..description` и HUD-аналог), проверить, что не осталось - пустых (тест или скрипт сверки id <-> lang). -- [x] **Описание функции при наведении в обычном ClickGUI** (колонки Combat/Player/Visuals/Other). - В области сверху по центру над колонками показывать описание функции под курсором (плашка в нашем - стиле, плавное появление, перенос/обрезка по ширине). - -Задача Б (форк, главное меню): -- [x] **Hero-меню «калово»**: заголовок налезает на подзаголовок и кнопки, нет иконок на кнопках, - мелкие и кривые шрифты. Эталон: референс Vectra (иконки слева в кнопках, акцентная Multiplayer, - тёмное стекло у остальных, пара Options/Quit внизу, карточка аккаунта слева сверху, виджет - времени справа сверху). Раскладку считать по измеренным размерам текста, а не по константам. -- [x] **Основное (orbs) меню тоже улучшить стилем** (типографика, тени, hover, появление). -- [x] **Настройки главного меню разбросаны и спрятаны** (`menuLayout`, `menuBg`, `menuBgUseTheme`, - `menuClockShowSeconds` в `MainConfig`). Сгруппировать в понятный раздел «Главное меню» в - настройках (ключи конфигов не менять, чтобы сохранённые значения не потерялись). - -### Правки после второго запуска (2026-10-01) - -- [x] `%tier `: не было `suggest` (подсказка показывала только серый ``): теперь ники игроков и режимы - (`vanilla|uhc|pot|nethop|smp|sword|axe|mace`). Остальные команды без `suggest` не имеют аргументов. -- [x] KillEffect Blood было видно сквозь блоки: пайплайн `WORLD_COLORED` без depth-теста заменён на `WORLD_COLORED_DEPTH`. -- [x] TargetESP: новый режим `swords` (8 мечей на орбите, пружинная физика, отдача при ударе, подсветка при хите). - `targetesp/sword/` (`SwordPhysics` чистая + 4 теста, `TargetEspSwords` рендер), текстура `particles/sword.png` - нарисована скриптом (пиксель-арт, окрашивается цветом палитры). Мечи рисуются поверх блоков (ESP, без depth). - Не проверено вживую: вид, размер, яркость свечения, текстура меча (при желании заменить на свою). - -### Фаза 13: разбор evo клиента (2026-10-01), план работ - -Отчёт: `mod/docs/EVO_REPORT.md` (29 модулей evo, которых нет у нас; крылья; ножи; HMI/Lua). -Лицензия: `ref/evo/README.md` объявляет код и ассеты проприетарными. Для релиза только свои модели/скрипты -или разрешение автора; формат (Bedrock geo/anim, HMI-Lua) стандартный, код пишем свой. -Порядок по просьбе владельца: сначала 1, потом остальное. - -- [x] 1. HoldMyItems Scripted на LuaJ (пункты 1-8 отчёта, частично): настройка `Режим` = Dynamics / Scripts / Both - (по умолчанию Dynamics, LuaJ грузится лениво). `held/lua/`: `HandScripts` (загрузка + горячая перезагрузка раз - в секунду из `config/lovisual/holdmyitems/{hand_pose,item_pose}.lua`, иначе встроенные примеры в - `assets/lovisual/holdmyitems/`), `HandFrame`; `runtime/`: `LuaSandbox` (только base/bit32/table/string/math, - без io/os/luajava/require/load/debug, лимит инструкций через хук), `LuaPreprocessor` (`global.x = v;` -> registry), - `ScriptSlot` (ошибка выключает только этот скрипт, лог один раз); `api/`: `M` (move/rotate с pivot/scale/push/pop/ - math), `P`, `I`, `Easings` (8 функций), без отдачи Java-объектов в Lua. Хуки в `ItemInHandRendererMixin`: - hand_pose в `submitArmWithItem`, item_pose в `renderItem` (first person). 7 тестов `LuaSandboxTest`. - (Хвост из 1-8 закрыт в пункте 1b ниже.) -- [x] 1b. HMI хвост (сделано): 4 вида скриптов `hand_pose`, `hand_relative_pose`, `item_pose`, `item_model` + аддоны - `hand_addon`, `hand_relative_addon`, `item_addon`, `item_model_addon` (только из `config/lovisual/holdmyitems/`, - выполняются сразу после основного). `V:cancelEquip/cancelSwing(bool)` отключают ваниль (флаги читаются на следующем - кадре); настройка «Всегда рисовать обе руки» (`always_hands`); `animator` (`ext/ModelAnimator` + миксин - `ItemModelAnimatorMixin` подменяет список квадов слоя, операции rotate/move/scale по диапазону индексов, pivot - в пикселях); `particleManager:addParticle{...}` с Lua-колбэком `tick` (`ext/HandParticles`, аддитивный BLOOM в - мире); `C:setCamRot/setCamPos` (`CameraScriptMixin`, действует 150 мс после вызова). Встроенные примеры для всех - скриптов. Тесты: `QuadOpsTest`. Не сделано: события `swingMHand/interact/blockBreaking`, `use_duration`/ - `using_item` для моделей, ItemStack-поля (`setRenderAsBlock`), крен камеры (roll), загрузка из ресурс-паков. - Не проверено вживую: оси и знаки M:rotate, видимость Lua-частиц, анимация квадов на реальной модели. -- [ ] 2. Wings: режим «Модель» из evo (подробности: `docs/EVO_REPORT.md`, раздел 2, шаги в 2.4). - Формат Bedrock `geo.json` + `animation.json`; 14 видов: angel, blue_dragon, dark, demon, dragon, easter, ghoul, - pulse, rocker, spider, trident, vulcano, wota (+ baby_dragon). angel/demon/dragon анимируются процедурно в коде - evo (`Internal29/41/42`), остальные из animation.json; wota с анимированной текстурой (64x640, 10 кадров по 150 мс). - Источник (локально): `ref/evo/mod/jar/assets/evolution/cosmetic/wings/*.{geo,animation}.json`, - `.../textures/cosmetic/wings/*.png`. Целевые пути: `assets/lovisual/wings/`, `assets/lovisual/textures/wings/`. - - [ ] `BedrockGeoParser` (общий с ножами) + запекание кубов в позиции/UV - - [ ] `BedrockAnimation` (keyframe, loop) и `ProceduralWingAnims` (angel/demon/dragon) - - [ ] `WingsRenderer.renderModel` (WORLD_TEXTURED[_DEPTH], якорь как у `drawWingSide`, подобрать знак Z и ориентацию) - - [ ] настройки в `Wings`: `wingSource` (Procedural/Model) + `wingModel` (14 ключей), tint/glow, lang en/ru - - [ ] тесты парсера, знаков pivot/rotation, UV, интерполяции -- [ ] 3. Модуль Knife (Melee Skins), раздел 3 отчёта, таблица классов в 3.7. 33 скина (21 геометрия, 12 перекрасок), - клипы draw/draw_1/inspect/inspect_1/melee_1..3/static_idle/run_start/run/run_end; ЛКМ чередует melee_1/2, - ПКМ melee_3, бег при спринте; first person и third person; звуки (94 ogg); шейдер holy. - Источник: `ref/evo/mod/jar/assets/evolution/cosmetic/melee/` (index.json, *.geo.json, *.anim.json), `textures/cosmetic/melee/`, - `sounds/melee/`, `shaders/core/melee_holy.*`. Целевые: `assets/lovisual/knife/`, `textures/knife/`, `sounds/knife/`. - Порядок: (1) общий BedrockGeoParser, (2) клипы + `KnifeAnimationState`, (3) first person без рук, (4) руки игрока, - (5) third person (миксин `PlayerItemInHandLayer`), (6) звуки, (7) бег/варианты/holy, (8) UI выбора скина и сохранение. - Миксины: ввод (атака/ПКМ), `ItemInHandRenderer`, `PlayerItemInHandLayer`. -- [ ] 4. Быстрые модули (P1, размер S): TNTTimer, AutoSprint (есть `SprintControlEvent`), TotemCounter, EnchantGlint, - PlaceAnimation, GodRays (референс-шейдеры `shaders/core/godrays`), ProjectileTrails (на нашем `Trails`), ArmorColor. -- [ ] 5. Дальше (P1-P2): DeathEffects (распад на части, шейдер `death_blast`), Blizzard, ElytraTrails, TotemSound, Sounds, - InventorySort, StorageTracker, ConsumeESP, CustomHand, DynamicLights (проверить `Light`), FoodInfo, PatPat, Showcase. - P3: Chunk Cache, Schematics, HitFix (риск античита), Camera, Graffiti, Skin Wheel, Discord RPC, Replay, Trainer. - Пропустить: GuiPresence (нужен бэкенд evo). Полный список из 29 модулей с оценкой: отчёт, раздел 1.2. -- [ ] 6. Лицензия: `ref/evo/README.md` объявляет код и ассеты evo проприетарными. Перед релизом заменить на свои модели/ - звуки или получить разрешение автора; до тех пор пути из отчёта только для локальной отладки, в репозиторий - ассеты evo не коммитить. - -### Не проверено вживую / открытые вопросы - -- [ ] Проверить в игре автокомплит `%`: ввод `%`, `%to`, Tab; серая подсказка `` после `%toggle `. - Если миксин не применится, прислать лог (поля `CommandSuggestions` сверены по байткоду 26.2). -- [ ] Визуально подогнать: режим «Созвездие», новые формы JumpCircles (ориентация текстур, яркость - аддитивного бленда), кровь (лужи, яркость), Hero/orbs-меню, индикаторы войсчата. -- [ ] Войсчат: события SVC трактованы по javap (`MicrophoneMuteEvent.isDisabled()` как «заглушён или - выключен», `getEntityId()` как UUID игрока); проверить с настоящим SVC 2.6.24; значок шапки HUD-панели - сейчас wifi-глиф (подходящего микрофона в шрифте нет). -- [ ] Тип ClickGUI (идея владельца): селектор «Тип ClickGUI» в настройках + точка выбора экрана - (`ClickGuiScreenManager`, три места `new ClickGuiScreen()`), `classic` по умолчанию, второй вид позже; - бинд и звуки остаются в модуле `ClickGui`. Ждёт решения, какой второй вид делать первым. -- [ ] `.gitignore` для `server-plugin/.classpath`, `.project`, `.settings/`. -- [ ] Фаза 8.5.3 по-прежнему в очереди (около 150 файлов >250 строк), по мере касания. -- [ ] Приёмка серверного плагина (Фаза 11 в корневом TODO.md): Velocity + `%policy` на стенде. - -### Заметки для следующих сессий - -- Игру запускает владелец: `cd mod && ./gradlew runClient`. Из корня репозитория `./gradlew` не существует. -- Параллельные gradle-сборки блокируют друг друга: форкам запускать через - `flock /tmp/claude-1000/gradle.lock ./gradlew build --offline -q`. -- Новые классы-помощники для миксинов НЕЛЬЗЯ класть в пакет `dev.loki.lovisual.mixins.*` - (Mixin: `IllegalClassLoadError`); им место в `mixinhandlers/` или `render/`. -- Лимиты проекта: ≤250 строк на файл, ≤4 файла на папку (`checkFolderLimit` падает при нарушении), - без длинного тире в текстах сайта и ответах. +# LoVisual Mod (26.2): план и статус + +Мод вырос из форка Combatant Client. Подробная история (фазы 1-12) лежит в git: `git log -p -- mod/TODO.md`. + +## Правила (обязательные) +- KISS / DRY / SOLID, без оверинжиниринга; публичный API менять можно, мёртвый код удалять. +- ≤250 строк на файл (новый код держим ≤200), ≤4 .java файла на папку (`checkFolderLimit` падает при нарушении). +- Коммиты на английском; владелец просит большие коммиты, не дробить; ничего не пушить без просьбы. +- Без `static Minecraft` в полях, без this-escape, без мёртвых миксинов и файлов-пустышек. +- Каждый этап компилируется (`./gradlew build`), у нового кода тесты (JUnit) на чистую логику. +- Тексты сайта и ответы без длинного тире; ответы владельцу по-русски. + +## Сделано (кратко) +- Ребрендинг, удаление Runtime/JarReplacement, разбивка файлов-гигантов (>600 строк), удаление читерских модулей, тесты. +- Перенос QoL-модулей из Delta (StreamerMode, LockSlot, AutoRespawn/Leave/Accept, ThirdPerson, UseTracker, NoServerPack, + DeathCoords, ItemScroller, TapeMouse, SoundReducer и др.), команды с префиксом `%` и автокомплит (в том числе в ванильном чате). +- Оптимизация FPS (мёртвые грузы, модуль Optimize), аудит attack-визуалов по sacura/GodWeer. +- Сессия 2026-10-01: TargetESP (Созвездие, Мечи), JumpCircles, KillEffect Blood (с depth), Hero/orbs главное меню и его + настройки, Simple Voice Chat (мягкая интеграция, модуль, HUD), тултипы (amber по умолчанию, выключаются, авто-масштаб под GUI + Scale), описания модулей и плашка описания в ClickGUI, `%tier` с подсказками ников, краши запуска (MixinExtras 0.5.5, helper-классы + вне `mixins.*`, порядок static init тем). + +## Фаза 13: порт идей из evo (отчёт: `docs/EVO_REPORT.md`) +Лицензия: код и ассеты evo проприетарные (`ref/evo/README.md`). Для релиза только свои модели/звуки или разрешение автора; +пути из отчёта нужны для локальной отладки, ассеты evo в репозиторий не коммитить. + +- [x] HoldMyItems Scripted на LuaJ: режим Dynamics/Scripts/Both; `hand_pose`, `hand_relative_pose`, `item_pose`, `item_model` + + аддоны (из `config/lovisual/holdmyitems/`, горячая перезагрузка); песочница без io/os/luajava; `V:cancelEquip/cancelSwing`, + `animator` (квады модели), `particleManager`, `C:setCamRot/setCamPos`, «всегда обе руки». Код: `held/lua/{runtime,api,ext}`. + Не сделано: события `swingMHand/interact/blockBreaking`, `use_duration`/`using_item`, ItemStack-поля, roll камеры, ресурс-паки. +- [x] TNTTimer: уже был в `Predictions` (плашка с таймером); добавлены «Иконка TNT» и «Полоска фитиля». +- [x] ProjectileTrails: новый модуль `visuals/effects/projectiletrails/` (`TrailStore`, `TrailRibbon`): ленты за стрелами, трезубцами, + жемчугом, снежками, яйцами, зельями, огненными шарами, ветряными зарядами, фейерверками; длительность, ширина, заливка + Solid/Glow, контур, глубина, 8 режимов цвета. Шейдерные заливки evo (Nebula, Plasma, Matrix и т.д.) не портированы. +- [x] DeathEffects: режимы KillEffect Shatter, Ascend, Explosion, Disintegrate (`hits/death/`: `DeathPhysics` чистая + 6 тестов, + `DeathBody`, `DeathMesh`, `DeathEffectHelper`; тело из 6 частей с UV скина игрока, у мобов серые кубы). Настройки в + `DeathEffectSettings`. Не сделано: скрытие ванильного трупа на время эффекта, шейдер `death_blast`, режим Lightning из evo + (у нас уже есть свой Lightning). +- [ ] Wings: режим «Модель» (Bedrock geo.json + animation.json, 14 видов; отчёт раздел 2.4). Шаги: `BedrockGeoParser` (общий с ножами), + `BedrockAnimation` + процедурные angel/demon/dragon, `WingsRenderer.renderModel`, настройки `wingSource`/`wingModel`, тесты. + Ресурсы (локально): `ref/evo/mod/jar/assets/evolution/cosmetic/wings/`, `.../textures/cosmetic/wings/`. +- [ ] Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person, звуки, шейдер holy (отчёт раздел 3.7, + порядок работ из 8 шагов). Ресурсы: `ref/evo/mod/jar/assets/evolution/cosmetic/melee/`. +- [ ] Быстрые модули P1: AutoSprint (есть `SprintControlEvent`), TotemCounter, EnchantGlint, PlaceAnimation, GodRays + (референс `shaders/core/godrays`), ArmorColor. +- [ ] Дальше P1-P2: Blizzard, ElytraTrails (на нашем `Trails`), TotemSound, Sounds, InventorySort, StorageTracker, ConsumeESP, + CustomHand, DynamicLights (проверить `Light`), FoodInfo, PatPat, Showcase. P3: Chunk Cache, Schematics, HitFix (риск античита), + Camera, Graffiti, Skin Wheel, Discord RPC, Replay, Trainer. Пропустить: GuiPresence. Полный список: отчёт, раздел 1.2. + +## Открытые задачи +### Проверить в игре (код готов, вживую не смотрели) +- Автокомплит `%` в ванильном чате (`%`, `%to`, Tab, `%tier `); если миксин не применится, прислать лог. +- Режимы TargetESP Созвездие и Мечи; формы JumpCircles; кровь (лужи, яркость, depth); новые режимы KillEffect (ориентация + граней частей, UV скина, яркость искр); ProjectileTrails (вид ленты, глубина); Hero/orbs-меню. +- HoldMyItems Scripted: знаки осей `M:rotate`, видимость Lua-частиц, анимация квадов на реальной модели. +- Войсчат с настоящим SVC 2.6.24 (события mute/speaking, значок шапки HUD). +- Размер плашки описания в ClickGUI и авто-масштаб тултипа при GUI Scale 2/3/4. + +### Решения и долги +- Тип ClickGUI: селектор «Тип ClickGUI» (`classic` по умолчанию), точка выбора экрана `ClickGuiScreenManager`; ждёт решения, + какой второй вид делать. +- ViewModel: свободный `Custom` swing и режимы sacura (Возвращение, Круговая); HitEffect: distortion/glow post-process; + JumpCircles: easing на появление; KillEffect: toggle «сквозь стены»; Trails: сравнение с sacura вживую. +- Фаза 8.5.3: около 150 файлов 250-400 строк и папки >4 файлов, чинить по мере касания. +- `.gitignore` для `server-plugin/.classpath`, `.project`, `.settings/`. +- Приёмка серверного плагина (Velocity + `%policy`), бэкенд chat-service и addons-registry (корневой TODO.md). + +## Заметки для сессий +- Игру запускает владелец: `cd mod && ./gradlew runClient` (из корня репозитория `./gradlew` нет). +- Параллельные сборки блокируют друг друга: `flock /tmp/claude-1000/gradle.lock ./gradlew build --offline -q`. +- Helper-классы для миксинов нельзя класть в пакет `dev.loki.lovisual.mixins.*` (Mixin `IllegalClassLoadError`): + им место в `mixinhandlers/`, `render/` или рядом с фичей. +- Мировые пайплайны (`WorldPipelines`) рисуют без отсечения задних граней и без записи глубины: порядок обхода вершин не важен. +- Параллельная сессия может коммитить в тот же репозиторий: перед `git commit --amend` смотреть `git log -3`. diff --git a/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json index 4ce8e27..4035d93 100644 --- a/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json +++ b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json @@ -5,6 +5,7 @@ "item_info_colorize": true, "tooltip_font": "Iosevka", "tooltip_text_size": 1.0, + "tooltip_auto_scale": true, "top_enchant_ignore": [ "minecraft:binding_curse", "minecraft:vanishing_curse", diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java index 7e833a9..5e351a3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java @@ -62,6 +62,8 @@ public class Predictions extends Module { private final BooleanValue glowWallsValue = bool("pred_glow_walls", SETTING_GLOW_WALLS, false); private final BooleanValue tntTimerValue = bool("pred_tnt_timer", SETTING_TNT_TIMER, true); private final RGBColorValue tntColorValue = visibleWhen(colorNoAlpha("pred_tnt_color", SETTING_TNT_COLOR, "#FF5555"), tntTimerValue::get); + private final BooleanValue tntIconValue = visibleWhen(bool("pred_tnt_icon", "tnt_icon", true), tntTimerValue::get); + private final BooleanValue tntBarValue = visibleWhen(bool("pred_tnt_bar", "tnt_fuse_bar", true), tntTimerValue::get); private final BooleanMapValue kindToggles = group("pred_kind_toggles", SETTING_PROJECTILES, new java.util.LinkedHashMap<>() {{ put("arrow", true); put("trident", true); put("fireball", true); put("wind_charge", true); put("firework", true); put("snowball", true); put("egg", true); @@ -139,7 +141,9 @@ public class Predictions extends Module { public BooleanValue getTargetHudOnAimValue() { return targetHudOnAimValue; } public RGBColorValue getTntColorValue() { return tntColorValue; } public List getTimerPlates() { return timerPlates; } + public boolean isTntBarEnabled() { return tntBarValue.get(); } public ItemStack getTntTimerIcon() { + if (!tntIconValue.get()) return ItemStack.EMPTY; if (tntTimerIcon.isEmpty()) tntTimerIcon = new ItemStack(Items.TNT); return tntTimerIcon; } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java index 626592e..711ddaf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java @@ -27,6 +27,7 @@ public final class PredictionsHudRenderer { private static final float PLATE_TEXT_SCALE = 0.68f; private static final float PLATE_TEXT_PADDING = 24.0f; private static final float PLATE_HEIGHT = 16.0f; + private static final float TNT_FUSE_TICKS = 80.0f; private static final float PLATE_BG_OFFSET_X = 2.0f; private static final float PLATE_BG_OFFSET_Y = 0.0f; private static final float PLATE_BG_SHRINK = 3.0f; @@ -62,9 +63,19 @@ public final class PredictionsHudRenderer { double seconds = Math.max(0.0, (fuse - tickDelta) / 20.0); drawTimerPlate(facade, renderer, textRenderer, formatTime(seconds), (float) screen.x, (float) screen.y, plateColor, facade.getTntTimerIcon(), null); + if (facade.isTntBarEnabled()) drawFuseBar(renderer, (float) screen.x, (float) screen.y, (fuse - tickDelta) / TNT_FUSE_TICKS, plateColor); } } + private static void drawFuseBar(Renderer2D renderer, float cx, float plateY, float progress, int argb) { + float w = 30.0f; + float x = cx - w * 0.5f; + float y = plateY + PLATE_HEIGHT + 2.0f; + renderer.roundedRect(x, y, w, 2.5f, 1.2f, 0xA0000000); + float fill = w * Math.max(0.0f, Math.min(1.0f, progress)); + if (fill > 1.0f) renderer.roundedRect(x, y, fill, 2.5f, 1.2f, argb | 0xFF000000); + } + public static void drawTimerPlate(Predictions facade, Renderer2D renderer, TextRenderer textRenderer, String txt, float sx, float sy, int argb, ItemStack iconStack, Identifier iconId) { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/DeathEffectSettings.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/DeathEffectSettings.java new file mode 100644 index 0000000..44c9f5a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/DeathEffectSettings.java @@ -0,0 +1,39 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +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.modules.visuals.effects.hits.death.DeathEffectHelper; + +/** Настройки режимов смерти KillEffect (Shatter, Ascend, Explosion, Disintegrate); вынесены из модуля ради лимита строк. */ +abstract class DeathEffectSettings extends Module { + + protected abstract String currentMode(); + + protected final boolean isMode(String value) { + return value.equals(currentMode()); + } + + protected final boolean isDeathMode() { + return DeathEffectHelper.handles(currentMode()); + } + + protected final NumberValue deathPower = visibleWhen( + num("killEffectDeathPower", "death_power", 1.0f, 0.3f, 3.0f), this::isDeathMode); + protected final NumberValue deathDuration = visibleWhen( + num("killEffectDeathDuration", "death_duration", 2.5f, 1.0f, 6.0f), this::isDeathMode); + protected final NumberValue deathSpin = visibleWhen( + num("killEffectDeathSpin", "death_spin", 1.0f, 0.2f, 3.0f), () -> isMode("Shatter") || isMode("Ascend")); + protected final BooleanValue deathBounce = visibleWhen( + bool("killEffectDeathBounce", "death_bounce", true), () -> isMode("Shatter")); + protected final NumberValue deathSparks = visibleWhen( + num("killEffectDeathSparks", "death_sparks", 70, 10, 150), () -> isMode("Explosion")); + protected final ModeValue blastColorMode = visibleWhen( + modeSetting("killEffectBlastColor", "blast_color", "Natural", "Natural", "Client", "Custom"), + () -> isMode("Explosion")); + + protected final boolean customBlastColor() { + return isMode("Explosion") && "Custom".equals(blastColorMode.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java index b7f5555..44651fa 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java @@ -5,16 +5,16 @@ 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.modules.visuals.effects.hits.blood.BloodKillHelper; +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathEffectHelper; +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.effects.FallingLavaHelper; +import dev.loki.lovisual.features.theme.Theme; import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor; import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.util.wav.engine.CustomSoundEngine; import net.minecraft.client.Minecraft; -import net.minecraft.core.particles.ParticleTypes; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; @@ -25,12 +25,13 @@ import java.util.Map; //todo Description @ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS) -public class KillEffect extends Module { +public class KillEffect extends DeathEffectSettings { private static final Identifier ORTHODOX_SOUND = Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav"); private final ModeValue mode = modeSetting("killEffectMode", "mode", "Orthodox", - "Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado", "Blood"); + "Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado", "Blood", + "Shatter", "Ascend", "Explosion", "Disintegrate"); private final NumberValue ySpeed = visibleWhen( num("killEffectYSpeed", "y_speed", 0, -10, 10), () -> isMode("Orthodox")); private final BooleanValue playSound = visibleWhen( @@ -66,13 +67,15 @@ public class KillEffect extends Module { private final KillLightningHelper lightningHelper = new KillLightningHelper(); private final KillBeamHelper beamHelper = new KillBeamHelper(); private final BloodKillHelper bloodHelper = new BloodKillHelper(); + private final DeathEffectHelper deathHelper = new DeathEffectHelper(); - private boolean isMode(String value) { - return value.equals(mode.get()); + @Override + protected String currentMode() { + return mode.get(); } private boolean usesColorSetting() { - return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings(); + return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings() || customBlastColor(); } private boolean usesBeamSettings() { @@ -126,7 +129,7 @@ public class KillEffect extends Module { CustomSoundEngine.get().play(ORTHODOX_SOUND, soundVolume.get(), 1.0, false, true); } } else if ("FallingLava".equals(current)) { - spawnFallingLava(entity); + FallingLavaHelper.spawn(entity); } else if ("EmbersArc".equals(current)) { emberHelper.spawn(pos); } else if ("Lightning".equals(current)) { @@ -135,6 +138,9 @@ public class KillEffect extends Module { beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.PORTAL); } else if ("Tornado".equals(current)) { beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.TORNADO); + } else if (DeathEffectHelper.handles(current)) { + deathHelper.spawn(entity, DeathEffectHelper.styleOf(current), deathPower.get(), deathDuration.get(), + deathSpin.get(), deathBounce.get(), deathSparks.get(), blastColor()); } else if ("Blood".equals(current)) { bloodHelper.spawn(new Vec3(pos.x, pos.y + entity.getBbHeight() * 0.6, pos.z), Math.clamp(entity.getBbWidth(), 0.3f, 1.6f), Math.clamp(entity.getBbHeight(), 0.3f, 2.6f), @@ -151,6 +157,7 @@ public class KillEffect extends Module { lightningHelper.clear(); beamHelper.clear(); bloodHelper.clear(); + deathHelper.clear(); } @Override @@ -167,28 +174,16 @@ public class KillEffect extends Module { } else if ("EmbersArc".equals(current)) { emberHelper.render(renderer, tickDelta); } + deathHelper.render(renderer, tickDelta); beamHelper.render(renderer, mc, beamIntensity.get(), beamSize.get()); bloodHelper.render(renderer, mc.gameRenderer.mainCamera().position(), tickDelta, bloodTime.get()); } - private void spawnFallingLava(LivingEntity entity) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) { - return; - } - int height = (int) (entity.getBbHeight() * 10); - int width = (int) (entity.getBbWidth() * 10); - for (int i = 0; i < height; i++) { - for (int j = 0; j < width; j++) { - for (int k = 0; k < width; k++) { - ((ClientLevelAccessor) mc.level).invokeAddParticle( - ParticleTypes.FALLING_LAVA, false, true, - entity.getX() + j * 0.1, - entity.getY() + i * 0.1, - entity.getZ() + k * 0.1, - 0, 0, 0); - } - } - } + private int blastColor() { + return switch (blastColorMode.get()) { + case "Client" -> Theme.theme().accent() | 0xFF000000; + case "Custom" -> color.getArgb(); + default -> 0xFFFFA030; + }; } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathBody.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathBody.java new file mode 100644 index 0000000..839ac5c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathBody.java @@ -0,0 +1,54 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death; + +import net.minecraft.world.entity.LivingEntity; + +import java.util.ArrayList; +import java.util.List; + +/** Разбиение тела на 6 частей (голова, торс, две руки, две ноги) с UV-прямоугольниками стандартного скина 64x64. */ +public final class DeathBody { + + /** Размеры в пикселях (w,h,d), начало UV (u,v) и смещение центра в блоках: kind -> {w,h,d,u,v,cx,cy}. */ + private static final float[][] LAYOUT = { + {8, 8, 8, 0, 0, 0.0f, 1.75f}, + {8, 12, 4, 16, 16, 0.0f, 1.125f}, + {4, 12, 4, 40, 16, -0.375f, 1.125f}, + {4, 12, 4, 32, 48, 0.375f, 1.125f}, + {4, 12, 4, 0, 16, -0.125f, 0.375f}, + {4, 12, 4, 16, 48, 0.125f, 0.375f}, + }; + + private DeathBody() { + } + + public static float[] layout(int kind) { + return LAYOUT[kind]; + } + + /** + * Части в мировых координатах: поза «стоя», повёрнутая по yaw тела, масштаб по росту сущности. + * Центр тела считается на высоте груди. + */ + public static List build(double px, double py, double pz, float bodyYawDeg, float height) { + float scale = Math.max(0.3f, height / 1.8f); + double yaw = Math.toRadians(-bodyYawDeg); + float cos = (float) Math.cos(yaw); + float sin = (float) Math.sin(yaw); + List parts = new ArrayList<>(LAYOUT.length); + for (int i = 0; i < LAYOUT.length; i++) { + float[] l = LAYOUT[i]; + float lx = l[5] * scale; + float wx = lx * cos; + float wz = -lx * sin; + DeathPhysics.Part p = new DeathPhysics.Part(i, (float) px + wx, (float) py + l[6] * scale, (float) pz + wz, + l[0] / 32.0f * scale, l[1] / 32.0f * scale, l[2] / 32.0f * scale); + p.ry = (float) yaw; + parts.add(p); + } + return parts; + } + + public static float height(LivingEntity e) { + return e.getBbHeight(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathEffectHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathEffectHelper.java new file mode 100644 index 0000000..c7be44f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathEffectHelper.java @@ -0,0 +1,178 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death; + +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Part; +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Style; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.player.health.PlayerSkinResolver; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.LivingEntity; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.addBillboardVertices; +import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.applyOpacity; + +/** Режимы смерти из evo: тело рассыпается на части (Shatter/Explosion), взлетает (Ascend) или тает (Disintegrate). */ +public final class DeathEffectHelper { + + private static final int FALLBACK_TINT = 0xFF9A9A9A; + private static final float SPARK_LIFE = 0.9f; + private static final float FLASH_LIFE = 0.45f; + + private static final class Spark { + float x, y, z, vx, vy, vz; + } + + private static final class Corpse { + final List parts; + final List sparks = new ArrayList<>(); + final Style style; + final long start; + final float duration; + final float groundY; + final boolean bounce; + final Identifier skin; + final int sparkColor; + final float[] center; + long last; + + Corpse(List parts, Style style, long start, float duration, float groundY, boolean bounce, + Identifier skin, int sparkColor, float[] center) { + this.parts = parts; + this.style = style; + this.start = start; + this.last = start; + this.duration = duration; + this.groundY = groundY; + this.bounce = bounce; + this.skin = skin; + this.sparkColor = sparkColor; + this.center = center; + } + } + + private final List corpses = new ArrayList<>(); + private final Random random = new Random(); + + public static Style styleOf(String mode) { + return switch (mode) { + case "Ascend" -> Style.ASCEND; + case "Explosion" -> Style.EXPLOSION; + case "Disintegrate" -> Style.DISINTEGRATE; + default -> Style.SHATTER; + }; + } + + public static boolean handles(String mode) { + return "Shatter".equals(mode) || "Ascend".equals(mode) || "Explosion".equals(mode) || "Disintegrate".equals(mode); + } + + public void clear() { + corpses.clear(); + } + + public void spawn(LivingEntity e, Style style, float power, float duration, float spin, boolean bounce, + int sparks, int sparkColor) { + float height = DeathBody.height(e); + List parts = DeathBody.build(e.getX(), e.getY(), e.getZ(), e.yBodyRot, height); + float cx = (float) e.getX(), cy = (float) e.getY() + height * 0.6f, cz = (float) e.getZ(); + for (Part p : parts) DeathPhysics.launch(p, style, power, spin, duration, cx, cy, cz, random); + Identifier skin = e instanceof AbstractClientPlayer player ? PlayerSkinResolver.resolvePlayerSkin(player) : null; + Corpse c = new Corpse(parts, style, GameClock.millis(), duration, (float) e.getY(), bounce, skin, sparkColor, + new float[]{cx, cy, cz}); + if (style == Style.EXPLOSION) { + for (int i = 0; i < sparks; i++) c.sparks.add(spark(cx, cy, cz, power)); + } + corpses.add(c); + } + + private Spark spark(float cx, float cy, float cz, float power) { + Spark s = new Spark(); + s.x = cx; + s.y = cy; + s.z = cz; + float theta = random.nextFloat() * 6.2831855f; + float up = random.nextFloat() * 2.0f - 0.6f; + float speed = (2.0f + random.nextFloat() * 5.0f) * power; + float horiz = (float) Math.sqrt(Math.max(0.0, 1.0 - up * up * 0.25)); + s.vx = (float) Math.cos(theta) * horiz * speed; + s.vz = (float) Math.sin(theta) * horiz * speed; + s.vy = up * speed; + return s; + } + + public void render(Renderer3D renderer, float tickDelta) { + if (corpses.isEmpty() || renderer == null) return; + long now = GameClock.millis(); + for (Iterator it = corpses.iterator(); it.hasNext(); ) { + Corpse c = it.next(); + float t = (now - c.start) / 1000.0f; + if (t > c.duration + 0.2f) { + it.remove(); + continue; + } + advance(c, now, t); + drawParts(renderer, c, t); + if (c.style == Style.EXPLOSION) drawSparks(renderer, c, t); + } + } + + private void advance(Corpse c, long now, float t) { + float dt = Math.min(0.1f, (now - c.last) / 1000.0f); + c.last = now; + int steps = Math.max(1, (int) Math.ceil(dt / 0.02f)); + float h = dt / steps; + for (int i = 0; i < steps; i++) { + for (Part p : c.parts) DeathPhysics.step(p, c.style, h, t, c.groundY, c.bounce); + } + for (Spark s : c.sparks) { + s.vy -= 9.0f * dt; + s.x += s.vx * dt; + s.y += s.vy * dt; + s.z += s.vz * dt; + } + } + + private void drawParts(Renderer3D renderer, Corpse c, float t) { + boolean textured = c.skin != null; + MeshBuilder mesh = textured + ? renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_DEPTH, c.skin, Renderer3D.DepthMode.MAIN) + : renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_DEPTH, Renderer3D.DepthMode.MAIN); + if (mesh == null) return; + for (Part p : c.parts) { + float alpha = DeathPhysics.alpha(p, c.style, t, c.duration); + if (alpha > 0.01f) DeathMesh.draw(mesh, p, DeathBody.layout(p.kind), textured ? 0xFFFFFF : FALLBACK_TINT & 0xFFFFFF, alpha, textured); + } + } + + private void drawSparks(Renderer3D renderer, Corpse c, float t) { + MeshBuilder glow = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, TextureStorage.BLOOM, + Renderer3D.DepthMode.NONE); + if (glow == null) return; + Vector3f right = new Vector3f(1, 0, 0).rotate(RenderState.cameraRotation); + Vector3f up = new Vector3f(0, 1, 0).rotate(RenderState.cameraRotation); + if (t < FLASH_LIFE) { + float k = t / FLASH_LIFE; + float size = 0.6f + k * 3.2f; + int col = applyOpacity(c.sparkColor, (1.0f - k) * (1.0f - k)); + addBillboardVertices(glow, c.center[0], c.center[1], c.center[2], new Vector3f(right).mul(size), + new Vector3f(up).mul(size), col, col, col, col); + } + float fade = Math.max(0.0f, 1.0f - t / SPARK_LIFE); + if (fade <= 0.0f) return; + int col = applyOpacity(c.sparkColor, fade); + Vector3f r = new Vector3f(right).mul(0.07f); + Vector3f u = new Vector3f(up).mul(0.07f); + for (Spark s : c.sparks) addBillboardVertices(glow, s.x, s.y, s.z, r, u, col, col, col, col); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathMesh.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathMesh.java new file mode 100644 index 0000000..902c82f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathMesh.java @@ -0,0 +1,94 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +/** + * Рисует часть тела как куб с UV скина. Мировые пайплайны рисуют без отсечения задних граней, + * поэтому порядок обхода вершин не важен. + */ +final class DeathMesh { + + private static final float TEX = 64.0f; + /** Яркость граней: +x, -x, +y, -y, +z, -z. */ + private static final float[] SHADE = {0.8f, 0.8f, 1.0f, 0.55f, 0.9f, 0.7f}; + + private DeathMesh() { + } + + static void draw(MeshBuilder mesh, DeathPhysics.Part p, float[] layout, int rgb, float alpha, boolean textured) { + float w = p.hw, h = p.hh, d = p.hd; + float u = layout[3], v = layout[4]; + float pw = layout[0], ph = layout[1], pd = layout[2]; + float[][] c = corners(p, w, h, d); + // порядок углов: 0:-x-y-z 1:+x-y-z 2:+x+y-z 3:-x+y-z 4:-x-y+z 5:+x-y+z 6:+x+y+z 7:-x+y+z + face(mesh, c, 4, 5, 6, 7, u + pd, v + pd, pw, ph, rgb, alpha, SHADE[4], textured); // +z (перед) + face(mesh, c, 1, 0, 3, 2, u + pd + pw + pd, v + pd, pw, ph, rgb, alpha, SHADE[5], textured); // -z (спина) + face(mesh, c, 0, 4, 7, 3, u, v + pd, pd, ph, rgb, alpha, SHADE[1], textured); // -x (правая сторона) + face(mesh, c, 5, 1, 2, 6, u + pd + pw, v + pd, pd, ph, rgb, alpha, SHADE[0], textured); // +x (левая сторона) + face(mesh, c, 7, 6, 2, 3, u + pd, v, pw, pd, rgb, alpha, SHADE[2], textured); // +y (верх) + face(mesh, c, 0, 1, 5, 4, u + pd + pw, v, pw, pd, rgb, alpha, SHADE[3], textured); // -y (низ) + } + + /** Углы куба после поворота Эйлера (Y, затем X, затем Z) и переноса в центр части. */ + private static float[][] corners(DeathPhysics.Part p, float w, float h, float d) { + float[][] out = new float[8][3]; + for (int i = 0; i < 8; i++) { + float x = (i & 1) == 0 ? -w : w; + float y = (i & 2) == 0 ? -h : h; + float z = (i & 4) == 0 ? -d : d; + // индексация: бит0 = x, бит1 = y, бит2 = z; пересобираем в порядок из draw() + out[i] = rotate(p, x, y, z); + } + return remap(out); + } + + private static float[][] remap(float[][] bits) { + // bits[i]: i = x | y<<1 | z<<2. Нужен порядок 0:(-,-,-) 1:(+,-,-) 2:(+,+,-) 3:(-,+,-) 4:(-,-,+) 5:(+,-,+) 6:(+,+,+) 7:(-,+,+) + return new float[][]{bits[0], bits[1], bits[3], bits[2], bits[4], bits[5], bits[7], bits[6]}; + } + + private static float[] rotate(DeathPhysics.Part p, float x, float y, float z) { + float cy = (float) Math.cos(p.ry), sy = (float) Math.sin(p.ry); + float x1 = x * cy + z * sy, z1 = -x * sy + z * cy; + float cx = (float) Math.cos(p.rx), sx = (float) Math.sin(p.rx); + float y2 = y * cx - z1 * sx, z2 = y * sx + z1 * cx; + float cz = (float) Math.cos(p.rz), sz = (float) Math.sin(p.rz); + float x3 = x1 * cz - y2 * sz, y3 = x1 * sz + y2 * cz; + return new float[]{p.x + x3, p.y + y3, p.z + z2}; + } + + private static void face(MeshBuilder m, float[][] c, int a, int b, int cc, int dd, float u, float v, + float uw, float vh, int rgb, float alpha, float shade, boolean textured) { + int argb = argb(rgb, alpha, shade); + float u0 = u / TEX, u1 = (u + uw) / TEX, v0 = v / TEX, v1 = (v + vh) / TEX; + // a: низ-слева, b: низ-справа, cc: верх-справа, dd: верх-слева (для боковых граней) + quad(m, c[a], c[b], c[cc], c[dd], u0, v1, u1, v1, u1, v0, u0, v0, argb, textured); + } + + private static void quad(MeshBuilder m, float[] p1, float[] p2, float[] p3, float[] p4, + float ua, float va, float ub, float vb, float uc, float vc, float ud, float vd, + int argb, boolean textured) { + m.ensureQuadCapacity(); + RenderColor col = new RenderColor(argb); + int i1 = vertex(m, p1, ua, va, col, textured); + int i2 = vertex(m, p2, ub, vb, col, textured); + int i3 = vertex(m, p3, uc, vc, col, textured); + int i4 = vertex(m, p4, ud, vd, col, textured); + m.quad(i1, i2, i3, i4); + } + + private static int vertex(MeshBuilder m, float[] p, float u, float v, RenderColor col, boolean textured) { + var b = m.vec3(p[0], p[1], p[2]); + if (textured) b = b.vec2(u, v); + return b.color(col).next(); + } + + private static int argb(int rgb, float alpha, float shade) { + int a = Math.max(0, Math.min(255, Math.round(alpha * 255.0f))); + int r = Math.round(((rgb >> 16) & 0xFF) * shade); + int g = Math.round(((rgb >> 8) & 0xFF) * shade); + int bl = Math.round((rgb & 0xFF) * shade); + return (a << 24) | (r << 16) | (g << 8) | bl; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysics.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysics.java new file mode 100644 index 0000000..94006cb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysics.java @@ -0,0 +1,137 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death; + +import java.util.Random; + +/** Чистая физика частей тела для режимов смерти (Shatter, Ascend, Explosion, Disintegrate); без Minecraft. */ +public final class DeathPhysics { + + public enum Style { SHATTER, ASCEND, EXPLOSION, DISINTEGRATE } + + /** Часть тела: центр, скорость, углы Эйлера (рад), угловая скорость, половинные размеры, задержка. */ + public static final class Part { + public float x, y, z, vx, vy, vz; + public float rx, ry, rz, wx, wy, wz; + public final float hw, hh, hd; + public final int kind; + public float delay; + public float phase; + boolean resting; + + public Part(int kind, float x, float y, float z, float hw, float hh, float hd) { + this.kind = kind; + this.x = x; + this.y = y; + this.z = z; + this.hw = hw; + this.hh = hh; + this.hd = hd; + } + + float radius() { + return Math.max(hw, Math.max(hh, hd)); + } + } + + private static final float GRAVITY = 16.0f; + + private DeathPhysics() { + } + + /** Начальный импульс части относительно центра тела (cx, cy, cz). */ + public static void launch(Part p, Style style, float power, float spin, float duration, + float cx, float cy, float cz, Random r) { + float dx = p.x - cx + (r.nextFloat() - 0.5f) * 0.2f; + float dy = p.y - cy; + float dz = p.z - cz + (r.nextFloat() - 0.5f) * 0.2f; + float len = Math.max(0.05f, (float) Math.sqrt(dx * dx + dy * dy + dz * dz)); + p.phase = r.nextFloat() * 6.2831855f; + switch (style) { + case SHATTER -> { + float k = (1.5f + r.nextFloat() * 1.5f) * power; + set(p, dx / len * k, 2.5f * power + dy / len * k, dz / len * k, spin * 7.0f, r); + } + case EXPLOSION -> { + float k = (5.0f + r.nextFloat() * 3.0f) * power; + set(p, dx / len * k, 3.0f * power + dy / len * k * 0.6f, dz / len * k, spin * 12.0f, r); + } + case ASCEND -> set(p, 0.0f, (0.5f + r.nextFloat() * 0.4f) * power, 0.0f, spin * 1.2f, r); + default -> { + set(p, 0.0f, 0.0f, 0.0f, spin * 0.5f, r); + p.delay = Math.max(0.0f, (2.0f - p.y) / 2.0f) * duration * 0.45f; + } + } + } + + private static void set(Part p, float vx, float vy, float vz, float spin, Random r) { + p.vx = vx; + p.vy = vy; + p.vz = vz; + p.wx = (r.nextFloat() - 0.5f) * 2.0f * spin; + p.wy = (r.nextFloat() - 0.5f) * 2.0f * spin; + p.wz = (r.nextFloat() - 0.5f) * 2.0f * spin; + } + + /** Шаг физики; t: секунды от начала эффекта, groundY: уровень пола, bounce: отскок от земли. */ + public static void step(Part p, Style style, float dt, float t, float groundY, boolean bounce) { + if (p.resting) return; + switch (style) { + case ASCEND -> { + p.x += (float) Math.sin(t * 2.0f + p.phase) * 0.25f * dt; + p.y += p.vy * dt; + spinStep(p, dt); + } + case DISINTEGRATE -> { + if (t > p.delay) p.y += 0.6f * dt; + spinStep(p, dt); + } + default -> ballistic(p, dt, groundY, bounce); + } + } + + private static void ballistic(Part p, float dt, float groundY, boolean bounce) { + p.vy -= GRAVITY * dt; + float drag = (float) Math.pow(0.985, dt * 60.0); + p.vx *= drag; + p.vz *= drag; + p.x += p.vx * dt; + p.y += p.vy * dt; + p.z += p.vz * dt; + spinStep(p, dt); + float floor = groundY + p.radius(); + if (p.y >= floor) return; + p.y = floor; + if (bounce && Math.abs(p.vy) > 1.0f) { + p.vy = -p.vy * 0.45f; + p.vx *= 0.8f; + p.vz *= 0.8f; + p.wx *= 0.7f; + p.wy *= 0.7f; + p.wz *= 0.7f; + } else { + p.vx = p.vy = p.vz = 0.0f; + p.wx = p.wy = p.wz = 0.0f; + p.resting = true; + } + } + + private static void spinStep(Part p, float dt) { + p.rx += p.wx * dt; + p.ry += p.wy * dt; + p.rz += p.wz * dt; + } + + /** Непрозрачность части в момент t (0..1). */ + public static float alpha(Part p, Style style, float t, float duration) { + float a = switch (style) { + case SHATTER -> 1.0f - ramp(t, duration * 0.6f, duration * 0.4f); + case EXPLOSION -> 1.0f - ramp(t, duration * 0.4f, duration * 0.6f); + case ASCEND -> 1.0f - ramp(t, duration * 0.3f, duration * 0.7f); + default -> 1.0f - ramp(t, p.delay, duration * 0.35f); + }; + return Math.max(0.0f, Math.min(1.0f, a)); + } + + private static float ramp(float t, float start, float length) { + return Math.max(0.0f, Math.min(1.0f, (t - start) / Math.max(0.001f, length))); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/FallingLavaHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/FallingLavaHelper.java new file mode 100644 index 0000000..3a6b5ef --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/FallingLavaHelper.java @@ -0,0 +1,28 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits.effects; + +import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.world.entity.LivingEntity; + +/** Лавовые капли по объёму тела (режим FallingLava у KillEffect). */ +public final class FallingLavaHelper { + + private FallingLavaHelper() { + } + + public static void spawn(LivingEntity entity) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return; + int height = (int) (entity.getBbHeight() * 10); + int width = (int) (entity.getBbWidth() * 10); + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + for (int k = 0; k < width; k++) { + ((ClientLevelAccessor) mc.level).invokeAddParticle(ParticleTypes.FALLING_LAVA, false, true, + entity.getX() + j * 0.1, entity.getY() + i * 0.1, entity.getZ() + k * 0.1, 0, 0, 0); + } + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/ProjectileTrails.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/ProjectileTrails.java new file mode 100644 index 0000000..98992f4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/ProjectileTrails.java @@ -0,0 +1,95 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +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.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Затухающие ленты за стрелами, жемчугом, трезубцами и другими снарядами. */ +@ModuleInfo( + id = "projectiletrails", + displayName = "ProjectileTrails", + category = ModuleCategory.VISUALS, + description = "Draws fading trails behind arrows, pearls and other projectiles" +) +public class ProjectileTrails extends Module { + + private final TrailStore store = new TrailStore(); + private final NumberValue duration = num("projectileTrailsDuration", "duration", 500, 100, 2000); + private final NumberValue width = num("projectileTrailsWidth", "width", 0.15f, 0.05f, 1.5f); + private final BooleanValue fill = bool("projectileTrailsFill", "fill", true); + private final ModeValue fillType = visibleWhen(modeSetting("projectileTrailsFillType", "fill_type", "Solid", "Solid", "Glow"), fill::get); + private final BooleanValue lines = bool("projectileTrailsLines", "lines", true); + private final NumberValue lineWidth = visibleWhen(num("projectileTrailsLineWidth", "line_width", 2.0f, 1.0f, 5.0f), lines::get); + private final BooleanValue depthTest = bool("projectileTrailsDepth", "depth_test", true); + private final ModeValue colorMode = modeSetting("projectileTrailsColorMode", "color_mode", "Theme", + "Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"); + private final RGBAColorValue color = color("projectileTrailsColor", "color", "#CCFF55FF"); + private final RGBAColorValue color2 = color("projectileTrailsColor2", "color2", "#CC55FFFF"); + private final NumberValue colorSpeed = num("projectileTrailsColorSpeed", "color_speed", 18, 2, 54); + private final BooleanMapValue kinds = group("projectileTrailsKinds", "projectiles", Map.of( + "arrow", true, "trident", true, "pearl", true, "snowball", true, "egg", true, + "potion", true, "fireball", true, "wind_charge", true, "firework", false)); + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.END_MAIN; + } + + @Override + public void onDisable() { + store.clear(); + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null || mc.gameRenderer.mainCamera() == null) return; + long now = GameClock.millis(); + long life = duration.get(); + for (Entity e : mc.level.entitiesForRendering()) { + String kind = TrailStore.kindOf(e); + if (kind != null && kinds.get(kind) && e.isAlive()) store.sample(e, tickDelta, now, life); + } + store.prune(now, life, id -> mc.level.getEntity(id) != null); + if (store.tracks().isEmpty()) return; + int phase = Math.round((mc.player == null ? 0 : mc.player.tickCount + tickDelta) * 4.0f); + AnimatedRenderColors.Mode mode = parse(colorMode.get()); + int primary = color.getArgb(); + int secondary = color2.getArgb(); + int speed = colorSpeed.get(); + List tracks = new ArrayList<>(store.tracks().values()); + Vec3 cam = mc.gameRenderer.mainCamera().position(); + TrailRibbon.render(renderer, tracks, now, life, width.get(), fill.get(), "Glow".equals(fillType.get()), + lines.get(), lineWidth.get(), depthTest.get(), cam, + (i, n) -> AnimatedRenderColors.resolve(mode, speed, phase + Math.round(i / (float) Math.max(1, n - 1) * 180), primary, secondary, true)); + } + + private static AnimatedRenderColors.Mode parse(String mode) { + return switch (mode) { + 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; + }; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailRibbon.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailRibbon.java new file mode 100644 index 0000000..833992c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailRibbon.java @@ -0,0 +1,99 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails; + +import dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails.TrailStore.Point; +import dev.loki.lovisual.render.engine.RenderState; +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.uniform.MeshBuilder; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntBinaryOperator; + +/** Строит ленту, повёрнутую к камере, с затуханием и сужением к хвосту, плюс контурные линии по краям. */ +final class TrailRibbon { + + private TrailRibbon() { + } + + /** + * @param color цвет точки: (индекс от хвоста, число точек) -> ARGB + */ + static void render(Renderer3D renderer, List tracks, long now, long lifeMs, float width, + boolean fill, boolean glow, boolean lines, float lineWidth, boolean depth, Vec3 cam, + IntBinaryOperator color) { + MeshBuilder fillMesh = null; + if (fill) { + var pipeline = glow + ? (depth ? LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH : LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE) + : (depth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED); + fillMesh = renderer.batch(pipeline, depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN); + } + MeshBuilder lineMesh = null; + if (lines) { + float prev = RenderState.lineWidth; + try { + RenderState.lineWidth = Math.max(0.5f, lineWidth); + lineMesh = renderer.batch(depth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_COLORED_LINES, depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN); + } finally { + RenderState.lineWidth = prev; + } + } + if (fillMesh == null && lineMesh == null) return; + for (TrailStore.Track track : tracks) { + List pts = new ArrayList<>(track.points); + if (pts.size() >= 2) draw(pts, now, lifeMs, width, fillMesh, lineMesh, cam, color); + } + } + + private static void draw(List pts, long now, long lifeMs, float width, MeshBuilder fill, MeshBuilder line, + Vec3 cam, IntBinaryOperator color) { + int n = pts.size(); + double[][] left = new double[n][3]; + double[][] right = new double[n][3]; + int[] argb = new int[n]; + for (int i = 0; i < n; i++) { + Point p = pts.get(i); + Point a = pts.get(Math.max(0, i - 1)); + Point b = pts.get(Math.min(n - 1, i + 1)); + double dx = b.x() - a.x(), dy = b.y() - a.y(), dz = b.z() - a.z(); + double vx = cam.x - p.x(), vy = cam.y - p.y(), vz = cam.z - p.z(); + double sx = dy * vz - dz * vy, sy = dz * vx - dx * vz, sz = dx * vy - dy * vx; + double len = Math.sqrt(sx * sx + sy * sy + sz * sz); + if (len < 1e-6) len = 1.0; + float life = 1.0f - Math.min(1.0f, (now - p.ms()) / (float) lifeMs); + double half = width * 0.5 * (0.25 + 0.75 * life) / len; + left[i] = new double[]{p.x() + sx * half, p.y() + sy * half, p.z() + sz * half}; + right[i] = new double[]{p.x() - sx * half, p.y() - sy * half, p.z() - sz * half}; + int base = color.applyAsInt(i, n); + int alpha = Math.round(((base >>> 24) & 0xFF) * life * life); + argb[i] = (alpha << 24) | (base & 0x00FFFFFF); + } + for (int i = 1; i < n; i++) { + if (fill != null) quad(fill, left[i - 1], right[i - 1], right[i], left[i], argb[i - 1], argb[i]); + if (line != null) { + segment(line, left[i - 1], left[i], argb[i - 1], argb[i]); + segment(line, right[i - 1], right[i], argb[i - 1], argb[i]); + } + } + } + + private static void quad(MeshBuilder m, double[] l0, double[] r0, double[] r1, double[] l1, int c0, int c1) { + m.ensureQuadCapacity(); + int a = m.vec3(l0[0], l0[1], l0[2]).color(new RenderColor(c0)).next(); + int b = m.vec3(r0[0], r0[1], r0[2]).color(new RenderColor(c0)).next(); + int c = m.vec3(r1[0], r1[1], r1[2]).color(new RenderColor(c1)).next(); + int d = m.vec3(l1[0], l1[1], l1[2]).color(new RenderColor(c1)).next(); + m.quad(a, b, c, d); + } + + private static void segment(MeshBuilder m, double[] p0, double[] p1, int c0, int c1) { + m.ensureLineCapacity(); + int a = m.vec3(p0[0], p0[1], p0[2]).color(new RenderColor(c0)).next(); + int b = m.vec3(p1[0], p1[1], p1[2]).color(new RenderColor(c1)).next(); + m.line(a, b); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailStore.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailStore.java new file mode 100644 index 0000000..c84f88b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/projectiletrails/TrailStore.java @@ -0,0 +1,83 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Map; +import java.util.HashMap; +import java.util.function.Predicate; + +/** История позиций летящих снарядов: точки с метками времени, устаревшие и пропавшие следы вычищаются. */ +final class TrailStore { + + /** Минимальный сдвиг между соседними точками (блоки), чтобы лента не копила шум. */ + private static final double MIN_STEP = 0.04; + + record Point(double x, double y, double z, long ms) { + } + + static final class Track { + final ArrayDeque points = new ArrayDeque<>(); + long lastSeen; + } + + private final Map tracks = new HashMap<>(); + + Map tracks() { + return tracks; + } + + void clear() { + tracks.clear(); + } + + /** Ключ вида снаряда для настроек: arrow, trident, pearl, snowball, egg, potion, fireball, wind_charge, firework. */ + static String kindOf(Entity entity) { + if (!(entity instanceof Projectile)) return null; + String path = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).getPath(); + if (path.contains("arrow")) return "arrow"; + if (path.contains("trident")) return "trident"; + if (path.contains("pearl")) return "pearl"; + if (path.contains("snowball")) return "snowball"; + if (path.contains("egg")) return "egg"; + if (path.contains("potion")) return "potion"; + if (path.contains("wind_charge")) return "wind_charge"; + if (path.contains("fireball") || path.contains("fire_charge")) return "fireball"; + if (path.contains("firework")) return "firework"; + return null; + } + + /** Добавляет текущую позицию снаряда (интерполированную) и отбрасывает старые точки. */ + void sample(Entity entity, float partialTicks, long now, long lifeMs) { + Track track = tracks.computeIfAbsent(entity.getId(), id -> new Track()); + track.lastSeen = now; + Vec3 p = entity.getPosition(partialTicks); + Point last = track.points.peekLast(); + if (last == null || last.ms() != now && dist(last, p) >= MIN_STEP) { + track.points.addLast(new Point(p.x, p.y, p.z, now)); + } + trim(track, now, lifeMs); + } + + /** Чистит следы: пропавшие из мира снаряды дотлевают, потом удаляются. */ + void prune(long now, long lifeMs, Predicate alive) { + for (Iterator> it = tracks.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + if (!alive.test(e.getKey())) trim(e.getValue(), now, lifeMs); + if (e.getValue().points.isEmpty() || now - e.getValue().lastSeen > lifeMs * 2) it.remove(); + } + } + + private static void trim(Track track, long now, long lifeMs) { + while (!track.points.isEmpty() && now - track.points.peekFirst().ms() > lifeMs) track.points.pollFirst(); + } + + private static double dist(Point a, Vec3 b) { + double dx = a.x() - b.x, dy = a.y() - b.y, dz = a.z() - b.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } +} diff --git a/mod/src/main/resources/assets/lovisual/lang/en_us.json b/mod/src/main/resources/assets/lovisual/lang/en_us.json index a234cbc..30dea30 100644 --- a/mod/src/main/resources/assets/lovisual/lang/en_us.json +++ b/mod/src/main/resources/assets/lovisual/lang/en_us.json @@ -1042,6 +1042,8 @@ "setting.predictions.aim_trajectory": "Aim Trajectory", "setting.predictions.aim_indicator": "Aim Indicator", "setting.predictions.tnt_timer": "TNT Timer", + "setting.predictions.tnt_icon": "TNT icon", + "setting.predictions.tnt_fuse_bar": "TNT fuse bar", "setting.predictions.tnt_color": "TNT Color", "setting.predictions.show_plate": "Show Impact Plate", "setting.reimaginedvisual.night_mode.option.NIGHT": "Dark night", @@ -3632,5 +3634,50 @@ "module.rain.description": "Customizes rain visuals and sounds.", "module.spatialgui.description": "Renders the interface as a floating panel in 3D space.", "module.trinket.description": "Attaches decorative trinkets to your character.", - "module.wet_world.description": "Makes surfaces look wet during rain." + "module.wet_world.description": "Makes surfaces look wet during rain.", + "module.projectiletrails.description": "Draws fading trails behind arrows, pearls and other projectiles.", + "setting.projectiletrails.duration": "Duration (ms)", + "setting.projectiletrails.width": "Width", + "setting.projectiletrails.fill": "Fill", + "setting.projectiletrails.fill_type": "Fill type", + "setting.projectiletrails.fill_type.option.Solid": "Solid", + "setting.projectiletrails.fill_type.option.Glow": "Glow", + "setting.projectiletrails.lines": "Outline", + "setting.projectiletrails.line_width": "Outline width", + "setting.projectiletrails.depth_test": "Depth test", + "setting.projectiletrails.color_mode": "Color mode", + "setting.projectiletrails.color": "Color", + "setting.projectiletrails.color2": "Second color", + "setting.projectiletrails.color_speed": "Color speed", + "setting.projectiletrails.projectiles": "Projectiles", + "setting.projectiletrails.color_mode.option.Static": "Static", + "setting.projectiletrails.color_mode.option.Rainbow": "Rainbow", + "setting.projectiletrails.color_mode.option.LightRainbow": "Light Rainbow", + "setting.projectiletrails.color_mode.option.Sky": "Sky", + "setting.projectiletrails.color_mode.option.Fade": "Fade", + "setting.projectiletrails.color_mode.option.DoubleColor": "Double Color", + "setting.projectiletrails.color_mode.option.Analogous": "Analogous", + "setting.projectiletrails.color_mode.option.Theme": "Theme", + "setting.projectiletrails.projectiles.option.arrow": "Arrows", + "setting.projectiletrails.projectiles.option.trident": "Tridents", + "setting.projectiletrails.projectiles.option.pearl": "Ender pearls", + "setting.projectiletrails.projectiles.option.snowball": "Snowballs", + "setting.projectiletrails.projectiles.option.egg": "Eggs", + "setting.projectiletrails.projectiles.option.potion": "Potions", + "setting.projectiletrails.projectiles.option.fireball": "Fireballs", + "setting.projectiletrails.projectiles.option.wind_charge": "Wind charges", + "setting.projectiletrails.projectiles.option.firework": "Fireworks", + "setting.killeffect.mode.option.Shatter": "Shatter", + "setting.killeffect.mode.option.Ascend": "Ascend", + "setting.killeffect.mode.option.Explosion": "Explosion", + "setting.killeffect.mode.option.Disintegrate": "Disintegrate", + "setting.killeffect.death_power": "Power", + "setting.killeffect.death_duration": "Duration (s)", + "setting.killeffect.death_spin": "Spin", + "setting.killeffect.death_bounce": "Bounce off the ground", + "setting.killeffect.death_sparks": "Sparks", + "setting.killeffect.blast_color": "Blast color", + "setting.killeffect.blast_color.option.Natural": "Natural", + "setting.killeffect.blast_color.option.Client": "Client theme", + "setting.killeffect.blast_color.option.Custom": "Custom" } diff --git a/mod/src/main/resources/assets/lovisual/lang/ru_ru.json b/mod/src/main/resources/assets/lovisual/lang/ru_ru.json index 6601fcf..b1deda4 100644 --- a/mod/src/main/resources/assets/lovisual/lang/ru_ru.json +++ b/mod/src/main/resources/assets/lovisual/lang/ru_ru.json @@ -976,6 +976,8 @@ "setting.predictions.aim_trajectory": "Траектория для прицеливания", "setting.predictions.aim_indicator": "Индикатор прицеливания", "setting.predictions.tnt_timer": "Таймер TNT", + "setting.predictions.tnt_icon": "Иконка TNT", + "setting.predictions.tnt_fuse_bar": "Полоска фитиля TNT", "setting.predictions.tnt_color": "Цвет TNT", "setting.predictions.show_plate": "Показывать точку приземления", "setting.reimaginedvisual.night_mode.option.NIGHT": "Темная ночь", @@ -3423,5 +3425,50 @@ "module.rain.description": "Настраивает вид и звук дождя.", "module.spatialgui.description": "Рисует интерфейс плавающей панелью в 3D-пространстве.", "module.trinket.description": "Крепит декоративные безделушки к вашему персонажу.", - "module.wet_world.description": "Делает поверхности мокрыми во время дождя." + "module.wet_world.description": "Делает поверхности мокрыми во время дождя.", + "module.projectiletrails.description": "Рисует затухающие ленты за стрелами, жемчугом и другими снарядами.", + "setting.projectiletrails.duration": "Длительность (мс)", + "setting.projectiletrails.width": "Ширина", + "setting.projectiletrails.fill": "Заливка", + "setting.projectiletrails.fill_type": "Тип заливки", + "setting.projectiletrails.fill_type.option.Solid": "Сплошная", + "setting.projectiletrails.fill_type.option.Glow": "Свечение", + "setting.projectiletrails.lines": "Контур", + "setting.projectiletrails.line_width": "Толщина контура", + "setting.projectiletrails.depth_test": "Проверка глубины", + "setting.projectiletrails.color_mode": "Режим цвета", + "setting.projectiletrails.color": "Цвет", + "setting.projectiletrails.color2": "Второй цвет", + "setting.projectiletrails.color_speed": "Скорость цвета", + "setting.projectiletrails.projectiles": "Снаряды", + "setting.projectiletrails.color_mode.option.Static": "Статичный", + "setting.projectiletrails.color_mode.option.Rainbow": "Радуга", + "setting.projectiletrails.color_mode.option.LightRainbow": "Светлая радуга", + "setting.projectiletrails.color_mode.option.Sky": "Небо", + "setting.projectiletrails.color_mode.option.Fade": "Плавный", + "setting.projectiletrails.color_mode.option.DoubleColor": "Два цвета", + "setting.projectiletrails.color_mode.option.Analogous": "Аналоговый", + "setting.projectiletrails.color_mode.option.Theme": "Тема", + "setting.projectiletrails.projectiles.option.arrow": "Стрелы", + "setting.projectiletrails.projectiles.option.trident": "Трезубцы", + "setting.projectiletrails.projectiles.option.pearl": "Жемчуг Края", + "setting.projectiletrails.projectiles.option.snowball": "Снежки", + "setting.projectiletrails.projectiles.option.egg": "Яйца", + "setting.projectiletrails.projectiles.option.potion": "Зелья", + "setting.projectiletrails.projectiles.option.fireball": "Огненные шары", + "setting.projectiletrails.projectiles.option.wind_charge": "Ветряные заряды", + "setting.projectiletrails.projectiles.option.firework": "Фейерверки", + "setting.killeffect.mode.option.Shatter": "Раскол", + "setting.killeffect.mode.option.Ascend": "Вознесение", + "setting.killeffect.mode.option.Explosion": "Взрыв", + "setting.killeffect.mode.option.Disintegrate": "Распад", + "setting.killeffect.death_power": "Сила", + "setting.killeffect.death_duration": "Длительность (с)", + "setting.killeffect.death_spin": "Вращение", + "setting.killeffect.death_bounce": "Отскок от земли", + "setting.killeffect.death_sparks": "Искры", + "setting.killeffect.blast_color": "Цвет взрыва", + "setting.killeffect.blast_color.option.Natural": "Природный", + "setting.killeffect.blast_color.option.Client": "Цвет темы", + "setting.killeffect.blast_color.option.Custom": "Свой" } diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysicsTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysicsTest.java new file mode 100644 index 0000000..934fb3f --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/death/DeathPhysicsTest.java @@ -0,0 +1,81 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death; + +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Part; +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Style; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +class DeathPhysicsTest { + + private static Part part(float y) { + return new Part(1, 0, y, 0, 0.25f, 0.375f, 0.125f); + } + + @Test + void shatterFallsAndRestsOnGround() { + Part p = part(1.1f); + DeathPhysics.launch(p, Style.SHATTER, 1.0f, 1.0f, 2.5f, 0, 1.0f, 0, new Random(1)); + for (int i = 0; i < 400; i++) DeathPhysics.step(p, Style.SHATTER, 0.02f, i * 0.02f, 0.0f, false); + assertEquals(0.375f, p.y, 1e-4f); + assertEquals(0.0f, p.vy, 1e-6f); + } + + @Test + void bounceKeepsPartAboveFloorAndLosesEnergy() { + Part p = part(1.5f); + p.vy = -6.0f; + float maxAfterFirst = 0; + boolean bounced = false; + for (int i = 0; i < 100; i++) { + DeathPhysics.step(p, Style.SHATTER, 0.02f, i * 0.02f, 0.0f, true); + assertTrue(p.y >= 0.375f - 1e-4f); + if (p.vy > 0) bounced = true; + if (bounced) maxAfterFirst = Math.max(maxAfterFirst, p.y); + } + assertTrue(bounced); + assertTrue(maxAfterFirst < 1.5f); + } + + @Test + void ascendRisesAndFadesOut() { + Part p = part(1.0f); + DeathPhysics.launch(p, Style.ASCEND, 1.0f, 1.0f, 2.5f, 0, 1.0f, 0, new Random(2)); + for (int i = 0; i < 50; i++) DeathPhysics.step(p, Style.ASCEND, 0.02f, i * 0.02f, 0.0f, false); + assertTrue(p.y > 1.4f); + assertEquals(1.0f, DeathPhysics.alpha(p, Style.ASCEND, 0.1f, 2.5f), 1e-6f); + assertEquals(0.0f, DeathPhysics.alpha(p, Style.ASCEND, 2.5f, 2.5f), 1e-6f); + } + + @Test + void disintegrateDissolvesTopFirst() { + Part head = new Part(0, 0, 1.75f, 0, 0.25f, 0.25f, 0.25f); + Part leg = new Part(4, 0, 0.375f, 0, 0.125f, 0.375f, 0.125f); + Random r = new Random(3); + DeathPhysics.launch(head, Style.DISINTEGRATE, 1f, 1f, 2.5f, 0, 1f, 0, r); + DeathPhysics.launch(leg, Style.DISINTEGRATE, 1f, 1f, 2.5f, 0, 1f, 0, r); + assertTrue(head.delay < leg.delay); + float t = (head.delay + leg.delay) * 0.5f + 0.3f; + assertTrue(DeathPhysics.alpha(head, Style.DISINTEGRATE, t, 2.5f) < DeathPhysics.alpha(leg, Style.DISINTEGRATE, t, 2.5f)); + } + + @Test + void explosionLaunchesFasterThanShatter() { + Part a = part(1.1f), b = part(1.1f); + a.x = 0.4f; + b.x = 0.4f; + DeathPhysics.launch(a, Style.SHATTER, 1f, 1f, 2.5f, 0, 1f, 0, new Random(5)); + DeathPhysics.launch(b, Style.EXPLOSION, 1f, 1f, 2.5f, 0, 1f, 0, new Random(5)); + assertTrue(Math.abs(b.vx) > Math.abs(a.vx)); + } + + @Test + void bodyHasSixPartsAroundFeet() { + var parts = DeathBody.build(10, 64, -5, 0, 1.8f); + assertEquals(6, parts.size()); + assertEquals(64 + 1.75f, parts.get(0).y, 1e-4f); + assertTrue(parts.get(2).x < parts.get(3).x); + } +}