feat(mod): ProjectileTrails, KillEffect death modes (Shatter/Ascend/Explosion/Disintegrate), TNT fuse bar; compact TODO
Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
This commit is contained in:
parent
483d6bfb93
commit
8d9a2ad0d6
17 changed files with 1097 additions and 800 deletions
844
mod/TODO.md
844
mod/TODO.md
|
|
@ -1,770 +1,74 @@
|
||||||
# LoVisual Mod (combatant-client-26.2) — план переписи
|
# LoVisual Mod (26.2): план и статус
|
||||||
|
|
||||||
Мод — форк Combatant Client, переписываем под LoVisual.
|
Мод вырос из форка Combatant Client. Подробная история (фазы 1-12) лежит в git: `git log -p -- mod/TODO.md`.
|
||||||
|
|
||||||
## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат)
|
## Правила (обязательные)
|
||||||
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга
|
- KISS / DRY / SOLID, без оверинжиниринга; публичный API менять можно, мёртвый код удалять.
|
||||||
- **Макс. 250 строк на файл** (было >1500) — превышение = разбить на модули
|
- ≤250 строк на файл (новый код держим ≤200), ≤4 .java файла на папку (`checkFolderLimit` падает при нарушении).
|
||||||
- **Макс. 4 файла на папку** — больше = новая подпапка по смыслу
|
- Коммиты на английском; владелец просит большие коммиты, не дробить; ничего не пушить без просьбы.
|
||||||
- **Публичный API можно менять свободно** (решение владельца 2026-09-05):
|
- Без `static Minecraft` в полях, без this-escape, без мёртвых миксинов и файлов-пустышек.
|
||||||
KISS важнее обратной совместимости; при рефакторинге разрешено менять
|
- Каждый этап компилируется (`./gradlew build`), у нового кода тесты (JUnit) на чистую логику.
|
||||||
сигнатуры, удалять делегирование и мёртвый код, обновляя вызывающих
|
- Тексты сайта и ответы без длинного тире; ответы владельцу по-русски.
|
||||||
- **Коммиты — на английском** (решение 2026-09-23, действует с этого момента
|
|
||||||
вперёд; более старые коммиты на русском не переписываем)
|
## Сделано (кратко)
|
||||||
- **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле
|
- Ребрендинг, удаление Runtime/JarReplacement, разбивка файлов-гигантов (>600 строк), удаление читерских модулей, тесты.
|
||||||
- **Никаких this-escape** — конструктор не публикует `this`
|
- Перенос QoL-модулей из Delta (StreamerMode, LockSlot, AutoRespawn/Leave/Accept, ThirdPerson, UseTracker, NoServerPack,
|
||||||
- **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel
|
DeathCoords, ItemScroller, TapeMouse, SoundReducer и др.), команды с префиксом `%` и автокомплит (в том числе в ванильном чате).
|
||||||
- **Без пустышек** — файл без реализации не создаётся
|
- Оптимизация FPS (мёртвые грузы, модуль Optimize), аудит attack-визуалов по sacura/GodWeer.
|
||||||
- **Каждый этап компилируется** — `./gradlew build` перед переходом к следующему
|
- Сессия 2026-10-01: TargetESP (Созвездие, Мечи), JumpCircles, KillEffect Blood (с depth), Hero/orbs главное меню и его
|
||||||
- **Тесты обязательны** для новых модулей (JUnit)
|
настройки, 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`). Для релиза только свои модели/звуки или разрешение автора;
|
||||||
### 1. Ребрендинг не закончен (пункт 2)
|
пути из отчёта нужны для локальной отладки, ассеты evo в репозиторий не коммитить.
|
||||||
- 288 файлов / **1464 упоминания** `combatant` в main-коде
|
|
||||||
- Классы: `CombatantRenderSystem`, `CombatantRenderEngineBootstrap`, `CombatantRenderPipelines`, `CombatantVulkanBackend`, `CombatantFrameGraph`, `CombatantTerrainVertex` и др.
|
- [x] HoldMyItems Scripted на LuaJ: режим Dynamics/Scripts/Both; `hand_pose`, `hand_relative_pose`, `item_pose`, `item_model` +
|
||||||
- Заголовки файлов: «This file is part of the Combatant Client distribution» / копирайт изначального автора
|
аддоны (из `config/lovisual/holdmyitems/`, горячая перезагрузка); песочница без io/os/luajava; `V:cancelEquip/cancelSwing`,
|
||||||
|
`animator` (квады модели), `particleManager`, `C:setCamRot/setCamPos`, «всегда обе руки». Код: `held/lua/{runtime,api,ext}`.
|
||||||
### 2. Файлы-гиганты (пункт 3) — 12 файлов > 1500 строк
|
Не сделано: события `swingMHand/interact/blockBreaking`, `use_duration`/`using_item`, ItemStack-поля, roll камеры, ресурс-паки.
|
||||||
| Файл | Строк |
|
- [x] TNTTimer: уже был в `Predictions` (плашка с таймером); добавлены «Иконка TNT» и «Полоска фитиля».
|
||||||
|------|-------|
|
- [x] ProjectileTrails: новый модуль `visuals/effects/projectiletrails/` (`TrailStore`, `TrailRibbon`): ленты за стрелами, трезубцами,
|
||||||
| render/engine/renderer/Renderer2D.java | 3379 |
|
жемчугом, снежками, яйцами, зельями, огненными шарами, ветряными зарядами, фейерверками; длительность, ширина, заливка
|
||||||
| features/gui/chat/BetterChatRenderer.java | 2870 |
|
Solid/Glow, контур, глубина, 8 режимов цвета. Шейдерные заливки evo (Nebula, Plasma, Matrix и т.д.) не портированы.
|
||||||
| features/gui/clickgui/settings/UnifiedSettingRenderer.java | 2798 |
|
- [x] DeathEffects: режимы KillEffect Shatter, Ascend, Explosion, Disintegrate (`hits/death/`: `DeathPhysics` чистая + 6 тестов,
|
||||||
| features/module/modules/player/Scaffold.java | 2416 |
|
`DeathBody`, `DeathMesh`, `DeathEffectHelper`; тело из 6 частей с UV скина игрока, у мобов серые кубы). Настройки в
|
||||||
| features/module/modules/combat/AttributeSwap.java | 1922 |
|
`DeathEffectSettings`. Не сделано: скрытие ванильного трупа на время эффекта, шейдер `death_blast`, режим Lightning из evo
|
||||||
| features/gui/clickgui/ClickGuiRenderer.java | 1900 |
|
(у нас уже есть свой Lightning).
|
||||||
| features/module/modules/visuals/NameTags.java | 1791 |
|
- [ ] Wings: режим «Модель» (Bedrock geo.json + animation.json, 14 видов; отчёт раздел 2.4). Шаги: `BedrockGeoParser` (общий с ножами),
|
||||||
| features/module/modules/combat/ElytraTarget.java | 1711 |
|
`BedrockAnimation` + процедурные angel/demon/dragon, `WingsRenderer.renderModel`, настройки `wingSource`/`wingModel`, тесты.
|
||||||
| features/gui/hud/draggable/impl/TargetHud.java | 1687 |
|
Ресурсы (локально): `ref/evo/mod/jar/assets/evolution/cosmetic/wings/`, `.../textures/cosmetic/wings/`.
|
||||||
| features/module/modules/visuals/ESP.java | 1520 |
|
- [ ] Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person, звуки, шейдер holy (отчёт раздел 3.7,
|
||||||
| features/module/modules/combat/AutoCrystal.java | 1516 |
|
порядок работ из 8 шагов). Ресурсы: `ref/evo/mod/jar/assets/evolution/cosmetic/melee/`.
|
||||||
| features/gui/hud/nondraggable/impl/CustomBar.java | 1496 |
|
- [ ] Быстрые модули P1: AutoSprint (есть `SprintControlEvent`), TotemCounter, EnchantGlint, PlaceAnimation, GodRays
|
||||||
|
(референс `shaders/core/godrays`), ArmorColor.
|
||||||
### 3. Runtime / JarReplacement (пункт 4)
|
- [ ] Дальше P1-P2: Blizzard, ElytraTrails (на нашем `Trails`), TotemSound, Sounds, InventorySort, StorageTracker, ConsumeESP,
|
||||||
- `runtime/` (ClientRuntime, RuntimeGate, ClientRuntimeState, SOFT_PANIC/JAR_REPLACEMENT_PANIC, shutdown/resume-участники)
|
CustomHand, DynamicLights (проверить `Light`), FoodInfo, PatPat, Showcase. P3: Chunk Cache, Schematics, HitFix (риск античита),
|
||||||
- `runtime/loader/` (JarReplacementCoordinator, LoaderBridge, PanicOnlyJarReplacementCoordinator)
|
Camera, Graffiti, Skin Wheel, Discord RPC, Replay, Trainer. Пропустить: GuiPresence. Полный список: отчёт, раздел 1.2.
|
||||||
- **Моё предложение: выкинуть.** Живая замена jar в памяти — источник classloader-утечек, хрупкая, для клиента не нужна (обновление делает лаунчер). Заменить на простой Lifecycle.
|
|
||||||
|
## Открытые задачи
|
||||||
### 4. Security (пункт 7)
|
### Проверить в игре (код готов, вживую не смотрели)
|
||||||
- `features/security/BackdoorProtection` — на деле защита от SSRF/translate-инъекций (порт ExploitPreventer, MIT). Полезное, но `enum`-синглтон со статикой.
|
- Автокомплит `%` в ванильном чате (`%`, `%to`, Tab, `%tier `); если миксин не применится, прислать лог.
|
||||||
- `features/relations/StaffTracker` + `StaffHeuristicsMatcher` — детект «стафа» по эвристикам TAB. Сомнительно, решить оставить/выкинуть.
|
- Режимы TargetESP Созвездие и Мечи; формы JumpCircles; кровь (лужи, яркость, depth); новые режимы KillEffect (ориентация
|
||||||
|
граней частей, UV скина, яркость искр); ProjectileTrails (вид ленты, глубина); Hero/orbs-меню.
|
||||||
### 5. Тесты (пункт 6)
|
- HoldMyItems Scripted: знаки осей `M:rotate`, видимость Lua-частиц, анимация квадов на реальной модели.
|
||||||
- 5 тестов на ~213k строк. Добавлять по мере переписи модулей.
|
- Войсчат с настоящим SVC 2.6.24 (события mute/speaking, значок шапки HUD).
|
||||||
|
- Размер плашки описания в ClickGUI и авто-масштаб тултипа при GUI Scale 2/3/4.
|
||||||
---
|
|
||||||
|
### Решения и долги
|
||||||
## Фаза 1: Ребрендинг (пункт 2)
|
- Тип ClickGUI: селектор «Тип ClickGUI» (`classic` по умолчанию), точка выбора экрана `ClickGuiScreenManager`; ждёт решения,
|
||||||
- [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 файлов)
|
- ViewModel: свободный `Custom` swing и режимы sacura (Возвращение, Круговая); HitEffect: distortion/glow post-process;
|
||||||
- [x] Переименовать классы `Combatant*` → `LoVisual*` (23 шт., включая `IrisCombatantFrameHooks` → `IrisLoVisualFrameHooks`)
|
JumpCircles: easing на появление; KillEffect: toggle «сквозь стены»; Trails: сравнение с sacura вживую.
|
||||||
- [x] Заголовки файлов → LoVisual, пересобрать копирайты (GPLv3 форка)
|
- Фаза 8.5.3: около 150 файлов 250-400 строк и папки >4 файлов, чинить по мере касания.
|
||||||
- [x] Проверить `fabric.mod.json` / `lovisual.mixins.json` на остатки
|
- `.gitignore` для `server-plugin/.classpath`, `.project`, `.settings/`.
|
||||||
- [x] Проверить строки/сообщения/логи на «combatant»
|
- Приёмка серверного плагина (Velocity + `%policy`), бэкенд chat-service и addons-registry (корневой TODO.md).
|
||||||
- [x] JNI-имена в native: `Java_combatant_client_` → `Java_dev_loki_lovisual_` (34 шт.) + FindClass-пути
|
|
||||||
|
## Заметки для сессий
|
||||||
### Проверка: `rg -i combatant src` → 0 ✓, `./gradlew build` ✓ (1406 файлов изменено)
|
- Игру запускает владелец: `cd mod && ./gradlew runClient` (из корня репозитория `./gradlew` нет).
|
||||||
|
- Параллельные сборки блокируют друг друга: `flock /tmp/claude-1000/gradle.lock ./gradlew build --offline -q`.
|
||||||
## Фаза 2: Выкинуть Runtime/JarReplacement (пункт 4)
|
- Helper-классы для миксинов нельзя класть в пакет `dev.loki.lovisual.mixins.*` (Mixin `IllegalClassLoadError`):
|
||||||
> **Вместе с рантаймом удалить и модуль Panic** (`features/module/modules/misc/Panic.java`),
|
им место в `mixinhandlers/`, `render/` или рядом с фичей.
|
||||||
> интерфейс `RuntimeControlModule`, команду `RuntimeCommand` и все ветки на них
|
- Мировые пайплайны (`WorldPipelines`) рисуют без отсечения задних граней и без записи глубины: порядок обхода вершин не важен.
|
||||||
> в `ModuleManager`/`Module`. Panic — единственная реализация `RuntimeControlModule`,
|
- Параллельная сессия может коммитить в тот же репозиторий: перед `git commit --amend` смотреть `git log -3`.
|
||||||
> без него они становятся мёртвым кодом.
|
|
||||||
- [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 <module>`
|
|
||||||
(`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`
|
|
||||||
подставляет наши команды/аргументы в попап по мере набора; подсказки аргументов `<module>`
|
|
||||||
показываются серым текстом (`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.<id>.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 <player>`: не было `suggest` (подсказка показывала только серый `<player>`): теперь ники игроков и режимы
|
|
||||||
(`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; серая подсказка `<module>` после `%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` падает при нарушении),
|
|
||||||
без длинного тире в текстах сайта и ответах.
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"item_info_colorize": true,
|
"item_info_colorize": true,
|
||||||
"tooltip_font": "Iosevka",
|
"tooltip_font": "Iosevka",
|
||||||
"tooltip_text_size": 1.0,
|
"tooltip_text_size": 1.0,
|
||||||
|
"tooltip_auto_scale": true,
|
||||||
"top_enchant_ignore": [
|
"top_enchant_ignore": [
|
||||||
"minecraft:binding_curse",
|
"minecraft:binding_curse",
|
||||||
"minecraft:vanishing_curse",
|
"minecraft:vanishing_curse",
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ public class Predictions extends Module {
|
||||||
private final BooleanValue glowWallsValue = bool("pred_glow_walls", SETTING_GLOW_WALLS, false);
|
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 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 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<>() {{
|
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("arrow", true); put("trident", true); put("fireball", true); put("wind_charge", true);
|
||||||
put("firework", true); put("snowball", true); put("egg", 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 BooleanValue getTargetHudOnAimValue() { return targetHudOnAimValue; }
|
||||||
public RGBColorValue getTntColorValue() { return tntColorValue; }
|
public RGBColorValue getTntColorValue() { return tntColorValue; }
|
||||||
public List<TimerPlate> getTimerPlates() { return timerPlates; }
|
public List<TimerPlate> getTimerPlates() { return timerPlates; }
|
||||||
|
public boolean isTntBarEnabled() { return tntBarValue.get(); }
|
||||||
public ItemStack getTntTimerIcon() {
|
public ItemStack getTntTimerIcon() {
|
||||||
|
if (!tntIconValue.get()) return ItemStack.EMPTY;
|
||||||
if (tntTimerIcon.isEmpty()) tntTimerIcon = new ItemStack(Items.TNT);
|
if (tntTimerIcon.isEmpty()) tntTimerIcon = new ItemStack(Items.TNT);
|
||||||
return tntTimerIcon;
|
return tntTimerIcon;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ public final class PredictionsHudRenderer {
|
||||||
private static final float PLATE_TEXT_SCALE = 0.68f;
|
private static final float PLATE_TEXT_SCALE = 0.68f;
|
||||||
private static final float PLATE_TEXT_PADDING = 24.0f;
|
private static final float PLATE_TEXT_PADDING = 24.0f;
|
||||||
private static final float PLATE_HEIGHT = 16.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_X = 2.0f;
|
||||||
private static final float PLATE_BG_OFFSET_Y = 0.0f;
|
private static final float PLATE_BG_OFFSET_Y = 0.0f;
|
||||||
private static final float PLATE_BG_SHRINK = 3.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);
|
double seconds = Math.max(0.0, (fuse - tickDelta) / 20.0);
|
||||||
drawTimerPlate(facade, renderer, textRenderer, formatTime(seconds),
|
drawTimerPlate(facade, renderer, textRenderer, formatTime(seconds),
|
||||||
(float) screen.x, (float) screen.y, plateColor, facade.getTntTimerIcon(), null);
|
(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,
|
public static void drawTimerPlate(Predictions facade, Renderer2D renderer, TextRenderer textRenderer,
|
||||||
String txt, float sx, float sy, int argb,
|
String txt, float sx, float sy, int argb,
|
||||||
ItemStack iconStack, Identifier iconId) {
|
ItemStack iconStack, Identifier iconId) {
|
||||||
|
|
|
||||||
|
|
@ -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<Float> deathPower = visibleWhen(
|
||||||
|
num("killEffectDeathPower", "death_power", 1.0f, 0.3f, 3.0f), this::isDeathMode);
|
||||||
|
protected final NumberValue<Float> deathDuration = visibleWhen(
|
||||||
|
num("killEffectDeathDuration", "death_duration", 2.5f, 1.0f, 6.0f), this::isDeathMode);
|
||||||
|
protected final NumberValue<Float> 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<Integer> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.mode.ModeValue;
|
||||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
import dev.loki.lovisual.features.module.core.Module;
|
|
||||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodKillHelper;
|
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.features.module.phase.WorldPhase;
|
||||||
import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor;
|
|
||||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
import dev.loki.lovisual.util.wav.engine.CustomSoundEngine;
|
import dev.loki.lovisual.util.wav.engine.CustomSoundEngine;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.core.particles.ParticleTypes;
|
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
|
@ -25,12 +25,13 @@ import java.util.Map;
|
||||||
|
|
||||||
//todo Description
|
//todo Description
|
||||||
@ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS)
|
@ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS)
|
||||||
public class KillEffect extends Module {
|
public class KillEffect extends DeathEffectSettings {
|
||||||
private static final Identifier ORTHODOX_SOUND =
|
private static final Identifier ORTHODOX_SOUND =
|
||||||
Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav");
|
Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav");
|
||||||
|
|
||||||
private final ModeValue mode = modeSetting("killEffectMode", "mode", "Orthodox",
|
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<Integer> ySpeed = visibleWhen(
|
private final NumberValue<Integer> ySpeed = visibleWhen(
|
||||||
num("killEffectYSpeed", "y_speed", 0, -10, 10), () -> isMode("Orthodox"));
|
num("killEffectYSpeed", "y_speed", 0, -10, 10), () -> isMode("Orthodox"));
|
||||||
private final BooleanValue playSound = visibleWhen(
|
private final BooleanValue playSound = visibleWhen(
|
||||||
|
|
@ -66,13 +67,15 @@ public class KillEffect extends Module {
|
||||||
private final KillLightningHelper lightningHelper = new KillLightningHelper();
|
private final KillLightningHelper lightningHelper = new KillLightningHelper();
|
||||||
private final KillBeamHelper beamHelper = new KillBeamHelper();
|
private final KillBeamHelper beamHelper = new KillBeamHelper();
|
||||||
private final BloodKillHelper bloodHelper = new BloodKillHelper();
|
private final BloodKillHelper bloodHelper = new BloodKillHelper();
|
||||||
|
private final DeathEffectHelper deathHelper = new DeathEffectHelper();
|
||||||
|
|
||||||
private boolean isMode(String value) {
|
@Override
|
||||||
return value.equals(mode.get());
|
protected String currentMode() {
|
||||||
|
return mode.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean usesColorSetting() {
|
private boolean usesColorSetting() {
|
||||||
return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings();
|
return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings() || customBlastColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean usesBeamSettings() {
|
private boolean usesBeamSettings() {
|
||||||
|
|
@ -126,7 +129,7 @@ public class KillEffect extends Module {
|
||||||
CustomSoundEngine.get().play(ORTHODOX_SOUND, soundVolume.get(), 1.0, false, true);
|
CustomSoundEngine.get().play(ORTHODOX_SOUND, soundVolume.get(), 1.0, false, true);
|
||||||
}
|
}
|
||||||
} else if ("FallingLava".equals(current)) {
|
} else if ("FallingLava".equals(current)) {
|
||||||
spawnFallingLava(entity);
|
FallingLavaHelper.spawn(entity);
|
||||||
} else if ("EmbersArc".equals(current)) {
|
} else if ("EmbersArc".equals(current)) {
|
||||||
emberHelper.spawn(pos);
|
emberHelper.spawn(pos);
|
||||||
} else if ("Lightning".equals(current)) {
|
} else if ("Lightning".equals(current)) {
|
||||||
|
|
@ -135,6 +138,9 @@ public class KillEffect extends Module {
|
||||||
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.PORTAL);
|
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.PORTAL);
|
||||||
} else if ("Tornado".equals(current)) {
|
} else if ("Tornado".equals(current)) {
|
||||||
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.TORNADO);
|
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)) {
|
} else if ("Blood".equals(current)) {
|
||||||
bloodHelper.spawn(new Vec3(pos.x, pos.y + entity.getBbHeight() * 0.6, pos.z),
|
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),
|
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();
|
lightningHelper.clear();
|
||||||
beamHelper.clear();
|
beamHelper.clear();
|
||||||
bloodHelper.clear();
|
bloodHelper.clear();
|
||||||
|
deathHelper.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -167,28 +174,16 @@ public class KillEffect extends Module {
|
||||||
} else if ("EmbersArc".equals(current)) {
|
} else if ("EmbersArc".equals(current)) {
|
||||||
emberHelper.render(renderer, tickDelta);
|
emberHelper.render(renderer, tickDelta);
|
||||||
}
|
}
|
||||||
|
deathHelper.render(renderer, tickDelta);
|
||||||
beamHelper.render(renderer, mc, beamIntensity.get(), beamSize.get());
|
beamHelper.render(renderer, mc, beamIntensity.get(), beamSize.get());
|
||||||
bloodHelper.render(renderer, mc.gameRenderer.mainCamera().position(), tickDelta, bloodTime.get());
|
bloodHelper.render(renderer, mc.gameRenderer.mainCamera().position(), tickDelta, bloodTime.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnFallingLava(LivingEntity entity) {
|
private int blastColor() {
|
||||||
Minecraft mc = Minecraft.getInstance();
|
return switch (blastColorMode.get()) {
|
||||||
if (mc.level == null) {
|
case "Client" -> Theme.theme().accent() | 0xFF000000;
|
||||||
return;
|
case "Custom" -> color.getArgb();
|
||||||
}
|
default -> 0xFFFFA030;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<DeathPhysics.Part> 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<DeathPhysics.Part> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Part> parts;
|
||||||
|
final List<Spark> 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<Part> 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<Corpse> 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<Part> 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<Corpse> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Integer> duration = num("projectileTrailsDuration", "duration", 500, 100, 2000);
|
||||||
|
private final NumberValue<Float> 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<Float> 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<Integer> 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<TrailStore.Track> 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<TrailStore.Track> 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<Point> pts = new ArrayList<>(track.points);
|
||||||
|
if (pts.size() >= 2) draw(pts, now, lifeMs, width, fillMesh, lineMesh, cam, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void draw(List<Point> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Point> points = new ArrayDeque<>();
|
||||||
|
long lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<Integer, Track> tracks = new HashMap<>();
|
||||||
|
|
||||||
|
Map<Integer, Track> 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<Integer> alive) {
|
||||||
|
for (Iterator<Map.Entry<Integer, Track>> it = tracks.entrySet().iterator(); it.hasNext(); ) {
|
||||||
|
Map.Entry<Integer, Track> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1042,6 +1042,8 @@
|
||||||
"setting.predictions.aim_trajectory": "Aim Trajectory",
|
"setting.predictions.aim_trajectory": "Aim Trajectory",
|
||||||
"setting.predictions.aim_indicator": "Aim Indicator",
|
"setting.predictions.aim_indicator": "Aim Indicator",
|
||||||
"setting.predictions.tnt_timer": "TNT Timer",
|
"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.tnt_color": "TNT Color",
|
||||||
"setting.predictions.show_plate": "Show Impact Plate",
|
"setting.predictions.show_plate": "Show Impact Plate",
|
||||||
"setting.reimaginedvisual.night_mode.option.NIGHT": "Dark night",
|
"setting.reimaginedvisual.night_mode.option.NIGHT": "Dark night",
|
||||||
|
|
@ -3632,5 +3634,50 @@
|
||||||
"module.rain.description": "Customizes rain visuals and sounds.",
|
"module.rain.description": "Customizes rain visuals and sounds.",
|
||||||
"module.spatialgui.description": "Renders the interface as a floating panel in 3D space.",
|
"module.spatialgui.description": "Renders the interface as a floating panel in 3D space.",
|
||||||
"module.trinket.description": "Attaches decorative trinkets to your character.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -976,6 +976,8 @@
|
||||||
"setting.predictions.aim_trajectory": "Траектория для прицеливания",
|
"setting.predictions.aim_trajectory": "Траектория для прицеливания",
|
||||||
"setting.predictions.aim_indicator": "Индикатор прицеливания",
|
"setting.predictions.aim_indicator": "Индикатор прицеливания",
|
||||||
"setting.predictions.tnt_timer": "Таймер TNT",
|
"setting.predictions.tnt_timer": "Таймер TNT",
|
||||||
|
"setting.predictions.tnt_icon": "Иконка TNT",
|
||||||
|
"setting.predictions.tnt_fuse_bar": "Полоска фитиля TNT",
|
||||||
"setting.predictions.tnt_color": "Цвет TNT",
|
"setting.predictions.tnt_color": "Цвет TNT",
|
||||||
"setting.predictions.show_plate": "Показывать точку приземления",
|
"setting.predictions.show_plate": "Показывать точку приземления",
|
||||||
"setting.reimaginedvisual.night_mode.option.NIGHT": "Темная ночь",
|
"setting.reimaginedvisual.night_mode.option.NIGHT": "Темная ночь",
|
||||||
|
|
@ -3423,5 +3425,50 @@
|
||||||
"module.rain.description": "Настраивает вид и звук дождя.",
|
"module.rain.description": "Настраивает вид и звук дождя.",
|
||||||
"module.spatialgui.description": "Рисует интерфейс плавающей панелью в 3D-пространстве.",
|
"module.spatialgui.description": "Рисует интерфейс плавающей панелью в 3D-пространстве.",
|
||||||
"module.trinket.description": "Крепит декоративные безделушки к вашему персонажу.",
|
"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": "Свой"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue