feat: frontend perf and a11y pass, plus mod visuals, hero main menu, Simple Voice Chat and vanilla chat autocomplete
Frontend: - replace the i18next stack with a small typed i18n module - hero clouds removed, llms.txt, HUD widget ARIA fixes and tests Mod: - TargetESP constellation mode; tracker and colors extracted - JumpCircles: 7 new shapes, shared animation and color pipeline - KillEffect blood mode with Burst/Fountain/Ring/Spray styles - main menu: menuLayout setting (orbs/hero) and hero screen - Simple Voice Chat soft integration: VoiceChat module and HUD panel - renderer/quick: static Draw facade with glass outline - vanilla chat: Baritone-style autocomplete popup for the % command prefix
This commit is contained in:
parent
167405aed0
commit
e00ea8eb11
204 changed files with 6469 additions and 833 deletions
146
mod/TODO.md
146
mod/TODO.md
|
|
@ -591,3 +591,149 @@
|
|||
### Итог: ничего из 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: `hand_relative_pose`, аддоны (`*_addon.lua`), подмена ванильного equip/swing скриптом,
|
||||
обе руки всегда, ItemStack-поля. Не проверено вживую: направление осей M:rotate на реальной руке.
|
||||
- [ ] 1b. HMI хвост (пункты 9-16): счётчики свинга и события, обе руки, ItemStack-поля, use_duration/using_item,
|
||||
аниматор частей модели (`item_model.lua`), камера из Lua, Lua-частицы, клавиша осмотра.
|
||||
- [ ] 2. Wings: режим «Модель» (Bedrock geo.json + animation.json, 14 видов, общий `BedrockGeoParser` + бейк + сэмплер).
|
||||
- [ ] 3. Модуль Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person.
|
||||
- [ ] 4. Быстрые модули: TNTTimer, AutoSprint, TotemCounter, PlaceAnimation, EnchantGlint, ArmorColor, GodRays, ProjectileTrails.
|
||||
- [ ] 5. DeathEffects, Blizzard, ElytraTrails.
|
||||
|
||||
### Не проверено вживую / открытые вопросы
|
||||
|
||||
- [ ] Проверить в игре автокомплит `%`: ввод `%`, `%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 файлов >200 строк), по мере касания.
|
||||
- [ ] Приёмка серверного плагина (Фаза 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/`.
|
||||
- Лимиты проекта: ≤200 строк на файл, ≤4 файла на папку (`checkFolderLimit` падает при нарушении),
|
||||
без длинного тире в текстах сайта и ответах.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ repositories {
|
|||
// ViaBedrock snapshots (alt repo used by Via* stack)
|
||||
url = 'https://maven.lenni0451.net/snapshots'
|
||||
}
|
||||
maven {
|
||||
// Simple Voice Chat API (optional compat)
|
||||
url = 'https://maven.maxhenkel.de/repository/public'
|
||||
}
|
||||
maven {
|
||||
// JitPack (for com.github.* artifacts like CubeConverter)
|
||||
url = 'https://jitpack.io'
|
||||
|
|
@ -130,6 +134,9 @@ dependencies {
|
|||
|
||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||
// Loader 0.19.4 ships MixinExtras 0.5.4, which crashes on @Redirect(at) arrays with this Mixin; 0.5.5 handles them.
|
||||
implementation "io.github.llamalad7:mixinextras-fabric:${project.mixinextras_version}"
|
||||
include "io.github.llamalad7:mixinextras-fabric:${project.mixinextras_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||
|
||||
|
||||
|
|
@ -155,6 +162,9 @@ dependencies {
|
|||
runtimeOnly "com.viaversion:viafabricplus:${project.viafabricplus_version}"
|
||||
}
|
||||
|
||||
// Simple Voice Chat (optional): client plugin API for speaking indicators; the mod provides it at runtime.
|
||||
compileOnly "de.maxhenkel.voicechat:voicechat-api:${project.voicechat_api_version}"
|
||||
|
||||
// Entity Culling (optional): typed compile-time API for shader ESP visibility compat.
|
||||
compileOnly "maven.modrinth:entityculling:${project.entityculling_version}"
|
||||
|
||||
|
|
@ -175,6 +185,10 @@ dependencies {
|
|||
// Javet core only. Native V8 binaries are copied into resources/javet/natives.
|
||||
bundledLibrary "com.caoccao.javet:javet:${javetVersion}"
|
||||
|
||||
// LuaJ: scripted hand/item poses for HoldMyItems (MIT, pure Java interpreter).
|
||||
bundledLibrary "org.luaj:luaj-jse:3.0.1"
|
||||
testImplementation "org.luaj:luaj-jse:3.0.1"
|
||||
|
||||
// Javet in minimal V8 mode only: no Node runtime and no ARM native payloads.
|
||||
// ARM builds can be added later behind a separate opt-in distribution profile.
|
||||
javetNativeBundle "com.caoccao.javet:javet-v8-windows-x86_64:${javetVersion}"
|
||||
|
|
|
|||
18
mod/config/lovisual/hud/draggable/voicechat.json
Normal file
18
mod/config/lovisual/hud/draggable/voicechat.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"enabled": false,
|
||||
"layout": {
|
||||
"pos": [
|
||||
16.0,
|
||||
150.0
|
||||
],
|
||||
"space": "logical"
|
||||
},
|
||||
"voicechat_scale": 1.0,
|
||||
"voicechat_layout": "Split Header",
|
||||
"voicechat_max_rows": 6.0,
|
||||
"voicechat_hold_ms": 350.0,
|
||||
"voicechat_show_self": true,
|
||||
"voicechat_bg_alpha": 131.0,
|
||||
"voicechat_bg_effect": "Auto",
|
||||
"voicechat_blur_alpha": 255.0
|
||||
}
|
||||
|
|
@ -1,20 +1,12 @@
|
|||
{
|
||||
'debug': 'off',
|
||||
'forcePvp': false,
|
||||
'menuBg': 'off',
|
||||
'menuLayout': 'orbs',
|
||||
'menuClockShowSeconds': false,
|
||||
'menuBgUseTheme': false,
|
||||
'msaa3d': 'off',
|
||||
'clickGuiModulesHints': true,
|
||||
'clickGuiHudEditorHints': true,
|
||||
'inventorySwapPolicy': 'none',
|
||||
'inventorySwapScope': 'full',
|
||||
'inventorySwapVisibility': 'silent',
|
||||
'inventorySwapRestore': true,
|
||||
'inventorySwapPreferHotbar': true,
|
||||
'inventorySwapLegitWaitTicks': 2.0,
|
||||
'inventorySwapStrictInventoryWaitTicks': 1.0,
|
||||
'inventorySwapStrictMovementLockTicks': 2.0,
|
||||
'disableNarrator': true,
|
||||
'backdoorProtection': true,
|
||||
'backdoorTranslationFilter': true,
|
||||
|
|
|
|||
411
mod/docs/EVO_REPORT.md
Normal file
411
mod/docs/EVO_REPORT.md
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
# Отчёт по клиенту EvoVis (evo) для LoVisual
|
||||
|
||||
Дата: 2026-10-01. Режим: только чтение, в проекте ничего не менялось, кроме этого файла.
|
||||
|
||||
Источники (все пути абсолютные):
|
||||
|
||||
- Деобфусцированный мод evo: `/storage/project/jvm/LoVisual/ref/evo/mod/jar-src-named/evovis/` (читаемые имена классов, 1328 штук) и `/storage/project/jvm/LoVisual/ref/evo/mod/jar-src-plain/sources/defpackage/` (оригинальные короткие имена `afe.java` и т.п.). Исходники ещё с control-flow flattening (`switch (i + i2)`), логику читать можно, но вокруг много шума.
|
||||
- Ресурсы evo (распакованный jar): `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/` (пространства имён `evolution` и `minecraft`).
|
||||
- Каталоги модулей: `/storage/project/jvm/LoVisual/ref/evo/analysis/modules.txt` (57 модулей) и `lovisual_modules.txt` (88 наших).
|
||||
- Таблица соответствия имён: `/storage/project/jvm/LoVisual/ref/evo/analysis/class_map.tsv` (старое имя, новое имя, роль).
|
||||
- Чистый (не обфусцированный) референс HoldMyItems с Lua: `/storage/project/jvm/LoVisual/ref/clients/mercury-src/com/holdmylua/` и миксины `/storage/project/jvm/LoVisual/ref/clients/mercury-src/fun/mercury/mixin/hmi/` + `HeldItemRendererMixin.java`.
|
||||
- Наш мод: `/storage/project/jvm/LoVisual/mod/src/main/java/dev/loki/lovisual/`.
|
||||
|
||||
ВАЖНОЕ ЗАМЕЧАНИЕ О ЛИЦЕНЗИИ. `ref/evo/README.md` прямо говорит: код и ассеты EvoVis проприетарные, "в LoVisual ничего не переносим, смотрим подходы и идеи". Поэтому копировать модели/текстуры/звуки/Lua evo в релиз как есть нельзя без разрешения. Форматы там стандартные (Bedrock geo.json + animation.json из Blockbench, Lua-скрипты формата HoldMyItems), так что реализацию можно делать на своих ассетах, а пути ниже нужны как карта и как тестовый материал для локальной отладки. Ниже в разделах 2 и 3 "пути для копирования" означают пути источников, которые нужны на время отладки. Для публичного релиза нужны либо разрешение, либо свои модели в тех же форматах.
|
||||
|
||||
---
|
||||
|
||||
## 1. Модули evo и чего нет у нас
|
||||
|
||||
Всего у evo 57 модулей (`evovis/module/{builders,combat,cosmetics,misc,render}`), у нас 88. Список наших взят из `@ModuleInfo` в `features/module/modules/*`.
|
||||
|
||||
Обозначения: сложность S (до дня работы), M (несколько дней), L (неделя и больше или нужна новая инфраструктура). Приоритет P1 (делать первым), P2, P3. "LOC" это размер класса модуля в evo (грубая оценка объёма; сам модуль часто тянет ещё `evovis/internal/*`).
|
||||
|
||||
### 1.1 Что у нас УЖЕ есть (аналог найден, переносить не нужно)
|
||||
|
||||
| Модуль evo | Наш аналог |
|
||||
|---|---|
|
||||
| Crosshair | `visuals/player/Crosshair` |
|
||||
| HitBox | `visuals/visibility/Hitboxes` |
|
||||
| Predictions | `combat/prediction/Predictions` |
|
||||
| AspectRatio | `visuals/camera/AspectRatio` |
|
||||
| BetterMinecraft | `misc/vanilla/BetterMinecraft` |
|
||||
| BlockOverlay | `visuals/scene/BlockHighlight` (у evo есть ещё шейдерные режимы, см. 1.3) |
|
||||
| Chams | `visuals/scene/visual/Chams` |
|
||||
| HMI | `visuals/items/held/HoldMyItems` (урезанный аналог, раздел 4) |
|
||||
| JumpCircles | `visuals/player/JumpCircles` |
|
||||
| MotionBlur | `visuals/fx/MotionBlur` |
|
||||
| NameTags | `visuals/nametags/NameTags` |
|
||||
| TargetESP | `combat/prediction/TargetESP` |
|
||||
| Trails | `visuals/effects/ambient/Trails` |
|
||||
| WorldLyrics | `visuals/lyrics/KineticLyrics` |
|
||||
| WorldTweaks | `visuals/weather/WorldTweaks` |
|
||||
| Zoom | `visuals/fx/Zoom` |
|
||||
| ItemScroller | `misc/items/ItemScroller` |
|
||||
| NoRender | `visuals/visibility/NoRender` |
|
||||
| Menu / Interface | `misc/ClickGui` и HUD-система `features/gui/hud/*` |
|
||||
| BetterRain | `visuals/weather/precip/Rain`, `WetWorld` |
|
||||
| FireFly | `visuals/particles/AmbientParticles` (там есть firefly) |
|
||||
| Particles (на удар, тотем) | `visuals/effects/hits/HitEffect`, `visuals/fx/TotemFX` |
|
||||
| Waypoints | частично: `visuals/world/Gps` + Xaero-совместимость `compat/xaero/*` |
|
||||
|
||||
### 1.2 Чего у нас НЕТ (полный список, 29 модулей)
|
||||
|
||||
| Категория | Модуль evo | Суть | LOC | Сложность | Приоритет |
|
||||
|---|---|---|---|---|---|
|
||||
| BUILDERS | Chunk Cache | держит посещённые чанки загруженными за пределами server view distance | 314 | M/L (клиентский ChunkCache, совместимость с Sodium) | P3 |
|
||||
| BUILDERS | Schematics | призрачный предпросмотр и список материалов для litematic/schem/nbt | 1258 | L | P3 |
|
||||
| BUILDERS | Showcase | находит постройку, летит кинематографичным путём вокруг и сохраняет клип | 882 | L (камера-путь это M, запись клипа это L) | P2 |
|
||||
| COMBAT | HitFix | атаки по хитбоксу, который реально видишь, а не на тик вперёд | 665 | M (трогает сетевую логику атаки, риск античита) | P3 |
|
||||
| COMBAT | TNTTimer | остаток фитиля у зажжённого TNT | 324 | S | P1 |
|
||||
| COSMETICS | Cosmetics | 3D-косметика и плащи на игроках (крылья, рюкзаки, скины-модели, плащ) | 46 + `internal/Cosmetic*` | L в целом, M только для крыльев (раздел 2) | P1 |
|
||||
| COSMETICS | Melee Skins | анимированные скины (ножи) на удерживаемых мечах | 860 | L (раздел 3) | P1 |
|
||||
| COSMETICS | Camera | клиентская камера и штатив: кадр, подбег, фото | 1082 | M/L | P3 |
|
||||
| COSMETICS | Graffiti | зажать клавишу и наспреить граффити на блок | 1010 | M | P3 |
|
||||
| COSMETICS | PatPat | пустая рука на мобе, зажать клавишу, погладить | 645 | M | P2 |
|
||||
| COSMETICS | Skin Wheel | поднять запястный циферблат (Omnitrix), крутить скины со вспышкой | 341 | M (нужны полные модели скинов, `cosmetic/skins`) | P3 |
|
||||
| MISC | AutoSprint | держит спринт, когда разрешено | 19 | S (у нас уже есть `SprintControlEvent`) | P1 |
|
||||
| MISC | TotemCounter | счётчик сломанных тотемов и своих | 255 | S | P1 |
|
||||
| MISC | TotemSound | свой звук при срабатывании тотема | 257 | S | P2 |
|
||||
| MISC | Sounds | звук на каждое клиентское действие | 195 | S | P2 |
|
||||
| MISC | InventorySort | сортировка инвентаря по пресету | 390 | S/M | P2 |
|
||||
| MISC | StorageTracker | запоминает содержимое контейнеров, ищет предметы | 678 | M | P2 |
|
||||
| MISC | Discord RPC | статус в Discord | 56 | S (но нужна IPC-библиотека, зависимость) | P3 |
|
||||
| MISC | Replay | хранит последние секунды геймплея, пишет клипы, тримминг | 526 + jcodec, `Evoclip` | L | P3 |
|
||||
| MISC | Trainer | тренировочные дрели: прицел, тотем, якорь | 402 | M | P3 |
|
||||
| RENDER | ArmorColor | красит надетую броню по остаточной прочности | 353 | S/M (у нас есть `EquipmentLayerRendererMixin`) | P2 |
|
||||
| RENDER | Blizzard | высотная метель: ветровые снежинки, белая пелена, обмерзание краёв | 1290 | M/L | P2 |
|
||||
| RENDER | ConsumeESP | круг с кольцом прогресса, что цель ест или пьёт | 671 | M | P2 |
|
||||
| RENDER | CustomHand | пост-процесс шейдер руки | 373 | M (у нас есть `shaders/hand_glass.frag`, частично) | P2 |
|
||||
| RENDER | DeathEffects | игроки рассыпаются на части тела при смерти | 2789 | L | P1 (одна из самых эффектных) |
|
||||
| RENDER | DynamicLights | факелы и лампы в руке светят по-настоящему | 553 | M (проверить, что наш `Light` это не оно) | P2 |
|
||||
| RENDER | ElytraTrails | светящиеся шлейфы с концов крыльев элитры | 2208 | M (переиспользовать наш `Trails`) | P2 |
|
||||
| RENDER | EnchantGlint | замена блеска зачарования стилями | 274 | S/M | P1 |
|
||||
| RENDER | FoodInfo | сытость, восстановление, подсказка оптимального порядка еды на полосе голода | 889 | M | P2 |
|
||||
| RENDER | GodRays | лучи солнца сквозь листву и рельеф | 33 + шейдер `shaders/core/godrays` | M | P1 |
|
||||
| RENDER | GuiPresence | показывает меню Evolution/YouTube, которое держат другие пользователи | 1577 | L (нужен бэкенд evo) | пропустить |
|
||||
| RENDER | PlaceAnimation | поставленные блоки плавно скользят и наклоняются на место | 195 | S/M | P1 |
|
||||
| RENDER | ProjectileTrails | гаснущие шлейфы за стрелами, жемчугом и прочими снарядами | 1401 | M (переиспользовать `Trails`) | P1 |
|
||||
|
||||
Итого отсутствует 29 из 57. Из них 3 из BUILDERS, 6 из COSMETICS, 12 из MISC (включая S-модули), остальное из RENDER и COMBAT.
|
||||
|
||||
### 1.3 Самое крутое (рекомендуемый порядок)
|
||||
|
||||
1. **Melee Skins (ножи)**: 33 скина, полные анимации draw/inspect/атаки/бег, звуки CS2-стиля, отдельный шейдер "holy". Раздел 3.
|
||||
2. **Крылья из моделей** (14 видов, часть с анимацией). У нас уже есть модуль Wings, нужно добавить режим моделей. Раздел 2.
|
||||
3. **DeathEffects**: распад тела на части (в evo есть шейдер `assets/evolution/shaders/core/death_blast`).
|
||||
4. **GodRays**: готовые референс-шейдеры в `assets/evolution/shaders/core/godrays/`.
|
||||
5. **Blizzard** и **ElytraTrails/ProjectileTrails**: хорошо ложатся на наш рендер-движок (`Renderer3D`, `MeshBuilder`).
|
||||
6. **PlaceAnimation**, **EnchantGlint**, **ArmorColor**: маленькие, дают заметный визуальный эффект.
|
||||
7. **Showcase** + **Camera** (кинематографика), **PatPat**, **Skin Wheel**, **Graffiti** (социальные косметики).
|
||||
8. Дополнительно для существующего BlockHighlight: шейдерные режимы оверлея блока у evo (`assets/evolution/shaders/core/blockoverlay/{starfield,holy,plasma,nebula,cobweb}`).
|
||||
|
||||
Прочие ресурсы evo, которые могут пригодиться как референс: `assets/evolution/shaders/core/*` (liquid_glass, kawase_up/down blur, motionblur, godrays, hand, hand_smoke, rain, sky_*, customsky, chams_*, wetworld, trail_glass), `assets/evolution/fonts/msdf/`, `assets/evolution/textures/{jumpcircles,targetesp,mob_effect,particle}`, `assets/evolution/videos/menu_bg.mp4`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Крылья (wings)
|
||||
|
||||
### 2.1 Где лежат и в каком формате
|
||||
|
||||
Формат: Bedrock Blockbench. Модель это `*.geo.json` (корень `minecraft:geometry`, `format_version` 1.12.0, у `easter` 1.21.0), анимация это `*.animation.json` (Bedrock animation, линейные keyframe, без Molang). Форматов obj/bbmodel/Geckolib у крыльев нет, свой парсер.
|
||||
|
||||
Каталоги (источник):
|
||||
|
||||
- Модели и анимации: `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/cosmetic/wings/` (13 geo, 10 animation, всего 340 КБ).
|
||||
- Текстуры: `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/textures/cosmetic/wings/` (14 png, 88 КБ).
|
||||
|
||||
Точные файлы:
|
||||
|
||||
geo.json: `angel`, `blue_dragon`, `dark`, `demon`, `dragon`, `easter`, `ghoul`, `pulse`, `rocker`, `spider`, `trident`, `vulcano`, `wota`.
|
||||
|
||||
animation.json: `blue_dragon`, `dark`, `easter`, `ghoul`, `pulse`, `rocker`, `spider`, `trident`, `vulcano`, `wota`. У `angel`, `demon`, `dragon` анимации в файлах нет, они процедурные в коде (см. 2.3).
|
||||
|
||||
png: `angel_white`, `baby_dragon_white`, `blue_dragon`, `dark`, `demon_black`, `demon_red`, `easter`, `ghoul`, `pulse`, `rocker`, `spider`, `trident`, `vulcano`, `wota`.
|
||||
|
||||
### 2.2 Каталог: 14 видов
|
||||
|
||||
Каталог задан статически в `evovis/hud/WingsHud.java` (оригинал `defpackage/afe.java`), блок `static { WINGS.add(new Cosmetic5(key, name, DragonMode)) }`. Параметры привязки заданы в `evovis/internal/DragonMode.java` (оригинал `afc.java`): scale, offsetY, offsetZ. Для режимов без явных значений: scale 1.0, offsetY 0.45, offsetZ 0.0.
|
||||
|
||||
| key (id) | Название | geo | Текстура (размер png) | Анимация | scale / offsetY / offsetZ |
|
||||
|---|---|---|---|---|---|
|
||||
| baby_dragon_white | Baby Dragon (White) | dragon | baby_dragon_white.png (64x64) | процедурная `dragon_wings` | 1.0 / 0.45 / 0 |
|
||||
| angel_white | Angel (White) | angel | angel_white.png (516x774) | процедурная `angel_wings` | 1.0 / 0.45 / 0 |
|
||||
| demon_red | Demon (Red) | demon | demon_red.png (516x774) | процедурная `demon_wings` | 1.0 / 0.45 / 0 |
|
||||
| demon_black | Demon (Black) | demon | demon_black.png (516x774) | процедурная `demon_wings` | 1.0 / 0.45 / 0 |
|
||||
| pulse | Pulse Wings | pulse | pulse.png (32x32) | pulse.animation.json (idle, 3 с, 4 кости) | 1.0 / -1.2 / 0 |
|
||||
| dark | Dark Wings | dark | dark.png (64x64) | dark.animation.json (idle, 4 с, 6 костей) | 0.9 / -1.05 / 0.05 |
|
||||
| ghoul | Ghoul Wings | ghoul | ghoul.png (64x64) | ghoul.animation.json (Idle, 3 с, 21 кость) | 0.6 / -0.65 / 0 |
|
||||
| rocker | Rocker Wings | rocker | rocker.png (128x128) | rocker.animation.json (main, 2.4 с, 25 костей) | 0.9 / -1.05 / 0 |
|
||||
| wota | Wota Wings | wota | wota.png (64x640, 10 кадров по 64) | wota.animation.json (idle, 3 с, 8 костей) | 0.9 / -1.05 / 0 |
|
||||
| trident | Trident Wings | trident | trident.png (32x32) | trident.animation.json (idle, 3.2 с, 1 кость) | 0.9 / -1.05 / 0.2 |
|
||||
| easter | Easter Wings | easter | easter.png (128x128) | easter.animation.json (idle, 3 с, 19 костей) | 0.9 / -1.05 / 0 |
|
||||
| blue_dragon | Dragon Wings (Blue) | blue_dragon | blue_dragon.png (128x128) | blue_dragon.animation.json (idle, 1.8 с, 5 костей) | 0.9 / -1.05 / 0 |
|
||||
| spider | Spider Wings | spider | spider.png (256x256) | spider.animation.json (idle, 2.4 с, 5 костей) | 1.0 / 0 / 0 |
|
||||
| vulcano | Vulcano Wings | vulcano | vulcano.png (64x64) | vulcano.animation.json (idle, 1.54 с, 4 кости) | 1.0 / 0 / 0 |
|
||||
|
||||
Размер png не равен `texture_width/height` в geo (текстуры в 2x выше). UV в geo задаются в пикселях геометрии, поэтому нормировать надо делением на `description.texture_width/texture_height` из geo, а не на размер png.
|
||||
|
||||
Число костей/кубов по geo: angel 15/12, blue_dragon 8/52, dark 9/14, demon 7/14, dragon 5/12, easter 45/158, ghoul 25/28, pulse 7/12, rocker 58/186, spider 11/94, trident 1/5, vulcano 4/14, wota 21/50.
|
||||
|
||||
Особый случай `wota`: анимированная текстура. `wota.png` это 10 кадров по 64 px высотой (64x640), смена кадра каждые 150 мс (`WOTA_FRAME_HEIGHT = 64`, `WOTA_FRAME_MILLIS = 150`, регистрация через `Animated.register` в `evovis/internal/Animated.java`, `Cosmetic5.getTexture()` дёргает `Animated.tick()`). У нас проще: брать смещение V = frame * (64/640).
|
||||
|
||||
Запасные режимы `AVIAN` и `INSECTOID` в `DragonMode` в каталог не попали, это встроенные JSON-анимации-заглушки в `WingsHud` (`AVIAN_ANIM`, `INSECTOID_ANIM`) для кастомных моделей. Нам не нужны.
|
||||
|
||||
### 2.3 Как рендерятся
|
||||
|
||||
Цепочка в evo:
|
||||
|
||||
1. Каталог и загрузка JSON: `evovis/hud/WingsHud.java` (`afe`). `geometry(mode)` читает `/assets/evolution/cosmetic/wings/<geometry>.geo.json` из classpath, `bundledAnimation(mode)` читает `<geometry>.animation.json`. Для `DRACONIC/ANGELIC/DEMONIC` вместо файла подставляется `{"procedural":"dragon_wings"}` и т.п.
|
||||
2. Экипировка: `WingsHud.applyKey(playerName, key)` собирает JSON "косметики" (`{"name":"wing_<key>","id":..., "attachment":1,"scale":..,"offset":[0,offsetY,offsetZ],"rotation":[0,0,0],"animation":..,"model":<geo>}`) и регистрирует через `Internal34.registerWithTexture(json, textureId)`. `attachment: 1` это `AttachmentMode.BODY` (`evovis/internal/AttachmentMode.java`: FREE, BODY, HEAD, ABOVE_HEAD, RIGHT_ARM, LEFT_ARM, RIGHT_LEG, LEFT_LEG, FULL_BODY). Менеджер косметики: `evovis/internal/Internal34.java` (`adq`), дескриптор: `Internal35.java` (`adr`).
|
||||
3. Слой рендера игрока: `evovis/internal/Internal33.java` (`adp`), `RenderLayer<AvatarRenderState, PlayerModel>`. Для каждой косметики вызывает `Controller.render(...)`, пропускает невидимых игроков.
|
||||
4. Позиционирование: `evovis/internal/Controller.java`. Порядок: `bind(poseStack)`, `push`, привязка к части тела `applyAttachment` (для BODY: translate+rotate по `playerModel.body`, поэтому крылья следуют за наклоном при присяде; возвращает сдвиг -0.3), затем `rotateZ(180)`, `translate(offX, offY + attachmentShift, offZ + backOffset)`, `rotateY/X/Z(косметика)`, `scale(scale)`. `backOffset` отодвигает крылья от спины при надетой броне: 0.078125 для нагрудника, 0.1375 для крыльев поверх нагрудника (`WINGS_BACK_OFFSET`). Крылья скрываются при глайдере (элитре) на груди (`DataComponents.GLIDER`).
|
||||
5. Геометрия: `evovis/hud/ModelRendererHud.java` (`adv`). `render()` вызывает `submitNodeCollector.submitCustomGeometry(poseStack, RenderTypes.entityTranslucent(texture), ...)`, рекурсивно `renderPart(bone)`: translate offset, translate pivot, rotate, scale, translate pivot back, затем кубы (`renderCube`: на каждую из 6 граней по 4 вершины с позицией, UV, нормалью, `OverlayTexture.NO_OVERLAY`, light игрока).
|
||||
6. Парсер геометрии: `evovis/internal/Rotation2.java` (`aid`), `Definition.java` (`aif`: textureWidth/Height, roots), `Parent.java` (`aig`: кость, pivot, rot, offset, scale, cubes, children).
|
||||
|
||||
Правила парсинга Bedrock geo в `Rotation2` (их надо воспроизвести):
|
||||
|
||||
- Кость: `pivot` читается как `(-x, y, z)`. `rotation` как `(rad(-rx), rad(-ry), rad(rz))`. Кубы внутри кости: `origin`, `size`, `pivot` (по умолчанию = origin), `rotation`, `inflate`, `mirror`; тот же поворот знаков.
|
||||
- UV куба: либо box-UV массив `[u,v]` (стандартная раскладка Bedrock), либо per-face объект `{north:{uv,uv_size,uv_rotation},...}`; `uv_rotation` допустим только 0/90/180/270. Есть эвристика `collectFrontRects` для плоских кубов нулевой толщины (dedupe прямоугольников "south" против "north").
|
||||
- Размеры в пикселях, масштаб 1/16 блока.
|
||||
|
||||
Анимации:
|
||||
|
||||
- Файловые (`*.animation.json`): корень либо `{"animations":{...}}`, либо плоский `{"idle":{...}}` (так у dark, pulse, trident, vulcano, wota; а у blue_dragon, easter, spider корень плоский, но имя клипа `animation.xxx.idle`). Берётся первый клип. Каналы: `rotation`, `position`, `scale`, значения `time -> [x,y,z]`, интерполяция линейная (`ModelRendererHud.interpolate`). Время: `(System.currentTimeMillis() - start) / 1000 % animation_length`. Применение (`ModelRendererHud.applyAnimation`): `rotX = base + rad(-x)`, `rotY = base + rad(-y)`, `rotZ = base + rad(z)`; `offset = base + position`; `scale = base * scale`. Базовые позы костей снимаются один раз (`snapshotBasePose`) и перед каждым кадром восстанавливаются (`resetPart`).
|
||||
- Процедурные (angel, dragon, demon): `ModelRendererHud.applyProceduralAnimation` выбирает по имени: `angel_wings` это `Internal29` (`defpackage/adg.java`), `dragon_wings` это `Internal42` (`aei.java`), `demon_wings` это `Internal41` (`aeh.java`). Время `lifeTime = ((tickCount + partialTick) / 20) % 360`. Внутри обычные `cos/sin` от градусов на костях: angel (Left_Wing, Right_Wing, bone2..bone11; корневой масштаб 0.6), demon (Wing2, Wing3, bone2, bone3, bone8, bone9; множитель времени 200), dragon (left_wing, right_wing, left_tip, right_tip; частоты 120/300/600/2200). Формулы лежат в этих трёх классах в явном виде.
|
||||
|
||||
Вывод: у evo крылья не зависят от позы игрока (нет режима полёта/присяда, кроме следования за телом). Анимация общая по часам, а не по игроку.
|
||||
|
||||
### 2.4 Наш модуль Wings и как добавить режим "модель из evo"
|
||||
|
||||
Наш модуль: `/storage/project/jvm/LoVisual/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/`
|
||||
|
||||
- `Wings.java` (`@ModuleInfo(id="wings", category=ACCESSORIES)`): настройки `wingsType` (ModeValue из `WingShapes.TYPES`: Angelic, Dragon, Butterfly, Phoenix, Crystal, Mechanical, Fairy, Demon), target (Self/Friends/All Players), scale, flapping, flapStrength, flapSpeed, throughWalls, glow, outline, ribs, color. Фаза `WorldPhase.END_MAIN`, метод `onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta)`. Свой игрок показывается только не от первого лица и без элитры.
|
||||
- `WingsRenderer.java`: рисует мировые вееры треугольников через `Renderer3D.batch(...)` и joml-матрицу `translate(player) · rotY(180-bodyYaw) · pre-translate · rotX(pitch) · rotZ(roll) · translate(anchor) · scale · [сторона]...`. Есть `resolvePose` (idle/sneak/glide).
|
||||
- `WingShapes.java`: данные 8 силуэтов и пресеты поз (`POSE_IDLE`, `POSE_SNEAK`, `glidePose`).
|
||||
- Тесты: `src/test/java/.../wings/WingShapesTest.java`, `WingsRendererTransformTest.java`.
|
||||
|
||||
Текстурный рендер в нашем движке уже есть и ровно подходит: `Renderer3D.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_DEPTH | WORLD_TEXTURED, Identifier texture, Renderer3D.DepthMode.MAIN | NONE)` и запись вершин `mesh.vec3(x,y,z).vec2(u,v).color(r,g,b,a).next()`, затем `mesh.quad(i1,i2,i3,i4)`. Пример: `features/module/modules/visuals/trollface/TrollfaceMask.java` строки 125-132. Текстуры берутся по `Identifier` (ресурс-пак `assets/lovisual/textures/...`). Значит миксин на слой игрока (как `Internal33` у evo) не нужен: можно трансформировать вершины на CPU так же, как это делает `WingsRenderer.put()` и `transformPosition()`.
|
||||
|
||||
План (режим "модель"):
|
||||
|
||||
1. Ассеты. Положить свои/разрешённые файлы в:
|
||||
- `src/main/resources/assets/lovisual/wings/<key>.geo.json` и `<key>.animation.json`;
|
||||
- `src/main/resources/assets/lovisual/textures/wings/<key>.png`.
|
||||
Для отладки можно скопировать (только локально) из:
|
||||
`/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/cosmetic/wings/{angel,blue_dragon,dark,demon,dragon,easter,ghoul,pulse,rocker,spider,trident,vulcano,wota}.geo.json`,
|
||||
`.../cosmetic/wings/{blue_dragon,dark,easter,ghoul,pulse,rocker,spider,trident,vulcano,wota}.animation.json`,
|
||||
`.../textures/cosmetic/wings/{angel_white,baby_dragon_white,blue_dragon,dark,demon_black,demon_red,easter,ghoul,pulse,rocker,spider,trident,vulcano,wota}.png`.
|
||||
2. Новые классы в пакете `wings`:
|
||||
- `WingModelDef` (record: key, displayName, geoKey, texture Identifier, animKey или procedural id, scale, offsetY, offsetZ). Таблица 2.2 переносится в статический каталог.
|
||||
- `BedrockGeoParser` (аналог `Rotation2`/`Definition`/`Parent`): читает `minecraft:geometry[0]`, строит дерево костей `Bone{name,parent,pivot,rot,cubes}`. Знаки pivot/rotation как в 2.3. Запекает кубы в массивы позиций и UV (нормировка на `texture_width/height` из description).
|
||||
- `BedrockAnimation` (аналог `ModelRendererHud.applyAnimation` и `interpolate`): разбор обоих корней (`animations` и плоский), линейные keyframe, `loop`.
|
||||
- `ProceduralWingAnims` (angel/demon/dragon): три функции `apply(Map<String,BoneState>, float timeSeconds)` с формулами из `Internal29/41/42`.
|
||||
- Реестр с кэшем, ленивая загрузка через `Minecraft.getInstance().getResourceManager().getResource(Identifier.fromNamespaceAndPath("lovisual", "wings/x.geo.json"))`.
|
||||
3. Рендер. В `WingsRenderer` добавить `renderModel(renderer, player, tickDelta, bodyYaw, def, style)`:
|
||||
- Якорь: вызывать ту же цепочку матриц, что `drawWingSide` (translate player, rotY(180-bodyYaw), pre-translate/pitch от `resolvePose`), затем масштаб `def.scale`. По evo: начало координат модели лежит на `1.5 (шея) + offsetY - 0.3` блока над ногами (у режимов с offsetY около -1.05 это почти уровень ног, потому что координаты geo заданы от ног; у angel/dragon/demon с offsetY 0.45 это уровень шеи, кости ниже pivot). Z-сдвиг: `offsetZ + backOffset` (0.078125 при нагруднике, 0.1375 при крыльях и нагруднике) в сторону от спины. Знак по Z и направление взгляда модели подобрать на практике одним тестом (в тестах `WingsRendererTransformTest` есть пример проверки матриц).
|
||||
- Обход дерева костей: матрица кости = `translate(offset) · translate(pivot) · rotZYX(rot) · scale · translate(-pivot)`, в блоках (деление на 16). Куб рисовать 6 гранями, вершины через `transformPosition` и `batchTextured(WORLD_TEXTURED_DEPTH, texture, DepthMode.MAIN)`; для throughWalls брать `WORLD_TEXTURED` с `DepthMode.NONE`.
|
||||
- Время анимации: лучше `(player.tickCount + tickDelta) / 20` (у evo wall-clock, одинаково у всех игроков).
|
||||
- Свет: наш мировой пайплайн не знает lightmap. Умножить цвет вершины на яркость позиции игрока (`level.getBrightness`) или оставить `fullbright` как опцию, чтобы ночью крылья не светились.
|
||||
- Анимированная текстура wota: смещение V кадра.
|
||||
- Для режима glide и sneak взять существующую `WingShapes.glidePose/POSE_SNEAK` (повернуть всю модель по pitch, как сейчас), у evo этого нет, это наша добавка.
|
||||
4. Настройки в `Wings.java`: добавить `ModeValue wingSource` ("Procedural" / "Model") и второй `ModeValue wingModel` (14 ключей из 2.2) с `visibleWhen(..., () -> "Model".equals(wingSource.get()))`. Прежний `wingType` показывать только для "Procedural". Цвет и glow для режима "модель" можно применять как tint (`color`) и дополнительным аддитивным проходом через `WORLD_TEXTURED_ADDITIVE`. Локализация: `assets/lovisual/lang/{en_us,ru_ru}.json` (в проекте есть `DescriptionCoverageTest`, ключи описаний обязательны).
|
||||
5. Тесты: парсер на мини-geo, проверка знаков pivot/rotation, нормировки UV, интерполяции keyframe (по образцу `WingShapesTest`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Ножи (knife, модуль Melee Skins)
|
||||
|
||||
### 3.1 Что это в evo
|
||||
|
||||
Модуль `evovis/module/cosmetics/MeleeSkinsModule.java` (`sz.java`, `@ModuleInfo(name="Melee Skins")`). Подменяет любой меч в руке (`ItemTags.SWORDS`, условие `appliesTo`) на выбранный "скин" (на самом деле полную 3D-модель ножа с анимированными руками). Настройки: `firstPersonSetting` (в своём виде), `thirdPersonSetting` (на теле), `inspectSetting` (клавиша осмотра, `OpenKeySetting`). Выбор скина хранится через `Melee3.equip/applyKey` и сохраняется в профиль косметики (`Cosmetic.persistIfLocal`), в нашем случае достаточно локального конфига.
|
||||
|
||||
### 3.2 Ресурсы (источник)
|
||||
|
||||
- Каталог моделей/анимаций: `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/cosmetic/melee/` (5.7 МБ).
|
||||
- `index.json` каталог скинов (33 записи, 16.5 КБ).
|
||||
- `<key>.geo.json` модель (Bedrock geo, `format_version` 1.12.0) и `<key>.anim.json` анимации (Bedrock animation `format_version` 1.8.0) на 21 геометрию (скины-варианты используют общую геометрию через поле `model`).
|
||||
- Геометрии: `annihil, bayonet, butterfly, cobra_vanguard, css, executor, karambit, m9, mercy, null_annihil, polaris, push, quenching, shadowkiller, silencefd, skeleton, stiletto, tactical, talon, thieve_sea, treasure_kn`.
|
||||
- Текстуры: `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/textures/cosmetic/melee/<key>.png` (3.5 МБ; размеры от 128 до 2048 px; нормировка UV по `texture_width/height` из geo) и иконки слотов `.../textures/cosmetic/melee/slot/<key>.png` (33 штуки). Служебные `m9.png.ppm` и `m9amethyst.png.ppm` (786 КБ каждый) в мод не нужны.
|
||||
- Звуки: `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/sounds/melee/` (94 ogg, 7.6 МБ), подпапки `annihil, cobra_vanguard, cs2, executor, mercy, polaris, quenching, shadowkiller, silencefd, thieve_sea, treasure_kn` (`val` пустая). Регистрация звуков: `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/sounds.json`, ключи `melee.<папка>.<имя>` (94 события; `melee.cs2.knife_slash1` -> `evolution:melee/cs2/knife_slash1`). Событие в анимации задаётся в `sound_effects` как `{"0.0":{"effect":"melee.cs2.knife_deploy1"}}`.
|
||||
- Шейдер "holy": `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/shaders/core/melee_holy.vsh` и `melee_holy.fsh` (GLSL 330, шумовое поле fbm, дрейф, градиент; формат вершин ENTITY, Sampler0).
|
||||
|
||||
### 3.3 Все скины (из `index.json`)
|
||||
|
||||
Базовые (21 геометрия, `key` = имя файла): annihil "Dark Star" (варианты angel/devil, 16 клипов), cobra_vanguard "Cobra Vanguard", executor "Executor", mercy "Mercy", null_annihil "\"Free Dark Star\"", polaris "Desmoulins' Edge", quenching "Dragon's Fang", shadowkiller "Fiery Owl", silencefd "TAC Dagger", thieve_sea "Sea Gleam", treasure_kn "Creed", bayonet "Bayonet", butterfly "Butterfly", css "Classic Knife", karambit "Karambit", m9 "M9 Bayonet", push "Push", skeleton "Skeleton", stiletto "Stiletto", tactical "Huntsman", talon "Talon".
|
||||
|
||||
Перекраски (общая геометрия, поле `model` в index.json): bayoauto "Bayonet | Autotronic" (bayonet), buemerald "Butterfly | Emerald" и busnowfall "Butterfly | Snowfall" (butterfly), karobsidian "Karambit | Obsidian" (karambit), m9auto, m9amethyst, m9emerald, m9sapphire, m9holy (m9; `holyBones:["blade"]`), stilettosapphire, stilettott "Tiger tooth", stilettoholy (stiletto; `holyBones:["b"]`).
|
||||
|
||||
Итого 33 записи (21 базовая + 12 перекрасок). Текстуры у каждой своя `textures/cosmetic/melee/<key>.png`. Поля записи index.json: `key`, `name`, `model` (общая геометрия, опц.), `clips` (список клипов), `transforms` (`thirdperson_righthand`, `thirdperson_lefthand`, `ground`, `fixed`, `gui`, `head`: `rotation/translation/scale` для разных display-контекстов), `variants` (только annihil: `[{suffix:"",hidden:["angel"]},{suffix:"_angel",hidden:["devil"]}]`, скрытие костей и суффикс клипов), `holyBones` (кости, к которым применяется шейдер holy).
|
||||
|
||||
### 3.4 Структура модели и клипов
|
||||
|
||||
Кости, общие для всех ножей (пример karambit, m9, bayonet): `root > bone > knife_and_right > <knife> > weapon > ...`, `righthand`, `righthand_pos`, `lefthand`, `lefthand_pos`, `constraint`, а также служебные: `camera`, `view`, `refit_view`, `iron_view`, `idle_view`, `positioning > {ground, thirdperson_hand, fixed}`. Служебные имена, которые читает код (`evovis/internal/Model2.java`): `righthand_pos`, `lefthand_pos` (куда ставить руки игрока), `camera` (покачивание камеры), `idle_view` (якорь вида первого лица), `thirdperson_hand` (якорь в третьем лице).
|
||||
|
||||
Клипы (набор варьируется): `draw`, `draw_1` (случайный выбор варианта), `inspect`, `inspect_1`, `melee_1`, `melee_2`, `melee_3`, `static_idle`, `idle`, `run`, `run_start`, `run_end` (не у всех). У annihil суффикс `_angel`. Длительности: draw 0.96 до 1.12 с, melee 1.12 с, inspect от 6 до 13 с.
|
||||
|
||||
Формат клипа: `{"loop":true,"animation_length":..,"override_previous_animation":..,"bones":{<bone>:{"rotation":{"0.0":[x,y,z],...},"position":{...},"scale":{...}}},"sound_effects":{"t":{"effect":"melee..."}}}`. Ключи времени: строка секунд (шаг около 0.04 с).
|
||||
|
||||
### 3.5 Анимация: осмотр, доставание, удар, бег
|
||||
|
||||
Класс состояния: `evovis/internal/Inspect.java` (`defpackage/ahl.java`). Два экземпляра: `Inspect.getInstance()` (главная рука) и `Inspect.offHand()`. Состояние:
|
||||
|
||||
- Три трека (`BaseTrack`, `evovis/internal/BaseTrack.java`): `baseTrack` (вечная `static_idle`, режим `Mode6.LOOP`), `mainTrack` (одноразовые: draw, inspect, melee), `movementTrack` (`run_start` > `run` > `run_end`). Режимы `Mode6`: `LOOP`, `ONCE_STOP`, `ONCE_HOLD`.
|
||||
- Конечный автомат: `MainStateMode.START` > при привязке модели проигрывает `draw` или (50% если есть) `draw_1` > `IDLE`. `MovementStateMode` для бега.
|
||||
- Осмотр: `onInspect()` (вызывается по клавише из `MeleeSkinsModule.onKeyPressed`) работает только в `IDLE`; с 50% шансом играет `inspect_1` (если клип есть), иначе `inspect`. Переход 0.2 с.
|
||||
- Атаки: `onAttack(boolean primary)`. ЛКМ чередует `melee_1` и `melee_2` (`PRIMARY_ATTACK_VARIANTS = 2`), ПКМ играет `melee_3`. Переходы 0.2 с (`TRANSITION_ACTION`). Хуки: `evovis/mixin/MeleeInputMixin.java` (`Minecraft.startAttack` HEAD, `Minecraft.startUseItem` RETURN, если не используется оффхенд).
|
||||
- Бег: только при `player.isSprinting()`; `run_start` (0.2 с) > `run` (цикл по пройденной дистанции, `RUN_CYCLE_DISTANCE = 2.0` блока) > `run_end` (0.3 с). Не у всех ножей есть run-клипы (нет у bayonet/karambit/m9 и др.).
|
||||
- Смена предмета/слота: `heldChanged` перепривязывает модель (`bind`), случайный вариант (`Variant`) выбирается из `variants`, скрытые кости помечаются `Model2.markHidden`.
|
||||
- Композиция: `compose()` смешивает статичную позу (static fade) и аддитивную (additive fade) в `float[] composedPose` (на кость 9 float: rot xyz, pos xyz, scale xyz). Время считается по `System.nanoTime`, максимальный шаг 0.25 с.
|
||||
- Интерполяция клипа: `evovis/internal/Internal68.java` (`CHANNEL_STRIDE=3`, `BONE_STRIDE=9`, Catmull-Rom сплайн; константы `CATMULL_HALF/TWO/FIVE`) и `Tracks.java` (`sample(time, float[] out)` по каналам rotation/position/scale).
|
||||
- Звуки: `evovis/internal/Melee5.java` (`aia`): события `melee.*` из sounds.json, `play(name)` через `SimpleSoundInstance` в позиции игрока (SoundSource.PLAYERS, volume/pitch 1.0). `BaseTrack` ведёт курсор `sound_effects` (capacity 4) и останавливает звук при смене клипа.
|
||||
|
||||
### 3.6 Рендер
|
||||
|
||||
Хуки (все в `evovis/mixin/`):
|
||||
|
||||
- `MeleeItemInHandMixin` (на `ItemInHandRenderer`): `submitHandsWithItems` HEAD вызывает `Thirdperson.beginHands()`, RETURN вызывает `flushHandItem(...)` (отложенно рисует предмет в левой руке модели ножа); `submitArmWithItem` HEAD cancellable: если `MeleeSkinsModule.rendersFirstPerson(item)` то `Thirdperson.renderFirstPerson(player, hand, matrices, queue, light, retireProgress)` и отмена ванильного рендера; пустую главную руку скрывает `hidesEmptyMainHand`; не-меч в другой руке откладывается `deferHandItem`.
|
||||
- `MeleePlayerItemInHandLayerMixin` (на `PlayerItemInHandLayer`): `submitArmWithItem` HEAD cancellable для третьего лица: `renderer.getModel().translateToHand(state, arm, matrices)`, `rotation XP -90`, `YP 180`, затем `Thirdperson.renderThirdPerson(...)` (только для своего игрока, `player.getId() == state.id`).
|
||||
- `MeleeInputMixin` (на `Minecraft`): атака, см. выше.
|
||||
|
||||
Класс рендера: `evovis/internal/Thirdperson.java` (`defpackage/...`, на деле рендер первого и третьего лица). Основные приёмы:
|
||||
|
||||
- `buildWorld(PrimaryBones, Model2, pose[], hidden[])` считает мировые матрицы костей из позы клипа (`PrimaryBones`: `world`, `combined`, `normals`, `visible`).
|
||||
- `applyCamera(model, inspect, poseStack)` применяет кость `camera` (покачивание камеры при осмотре и атаке). `applyViewAnchor` якорит модель по кости `idle_view`.
|
||||
- Руки: `submitArm(...)` рендерит руки игрока (скин через аватар-рендер) в точках `righthand_pos` и `lefthand_pos` модели, `hideHands` скрывает нерелевантные. Левая рука зеркалится при левшах (`handMirrored`, `scale(-1,1,1)`).
|
||||
- Геометрия ножа: `submitGeometry(...)` отправляет меш каждой кости через `submitCustomGeometry` с `RenderType` entityTranslucent и текстурой скина. Для `holyBones` используется `Melee.holy(texture)` (`evovis/internal/Melee.java`, `ahr.java`): `RenderPipeline "pipeline/melee_holy"` из `evolution:core/melee_holy`, формат вершин `DefaultVertexFormat.ENTITY`, translucent blend, без cull.
|
||||
- Запечённая модель: `evovis/internal/Model2.java`: массивы `boneNames/boneParents/bonePivots/boneRestRotation`, `boneMeshes[i]` (float[]: на вершину 8 float: pos(3), uv(2), normal(3); `VERTEX_STRIDE = 8`, 4 вершины на грань), `clips: Map<String, Internal68>`, индексы `rightHandBone/leftHandBone/cameraBone/viewAnchorBone`. Бейк `Model2.bake(Definition, key)` из того же `Rotation2`-парсера, что и крылья.
|
||||
- Загрузка и кэш: `evovis/internal/Melee4.java` (`ahx`): фоновый поток "evo-melee-loader", `ConcurrentHashMap<String, Model2>` кэш, `LOADING` set, вытеснение (`evict`). Модель читается из `/assets/evolution/cosmetic/melee/<geo>.geo.json` и `<geo>.anim.json` (`Melee2.modelResource()/animationResource()`).
|
||||
- Третье лицо и инвентарь: кость `thirdperson_hand`, в `index.json` блок `transforms.thirdperson_righthand/lefthand` задаёт rotation/translation на скин, `ground/fixed/gui/head` для других display-контекстов (в evo на них отдельные поля не используются в коде рендера кроме third-person, но в данных есть).
|
||||
|
||||
### 3.7 Что нужно у нас для отдельного модуля Knife
|
||||
|
||||
Новый модуль: `features/module/modules/visuals/items/knife/Knife.java` (`@ModuleInfo(id="knife", displayName="Knife", category=ModuleCategory.VISUALS` или `ACCESSORIES`). Классы:
|
||||
|
||||
| Роль | Предлагаемое имя | Прототип в evo |
|
||||
|---|---|---|
|
||||
| Модуль и настройки | `Knife` (скин, firstPerson, thirdPerson, inspectKey, scale, offset, звук) | `MeleeSkinsModule` |
|
||||
| Каталог и дескриптор скина | `KnifeCatalog`, `KnifeSkin` (record: key, geoKey, name, variants, holyBones, thirdPerson transforms, texture, icon) | `Melee3`, `Melee2`, `Variant`, `ThirdPersonLeftHand` |
|
||||
| Парсер geo и бейк | `BedrockGeoParser` (общий с крыльями), `KnifeModel` | `Rotation2`, `Definition`, `Parent`, `Model2` |
|
||||
| Клипы и сэмплинг | `KnifeClip`, `KnifeClipTrack` (LOOP/ONCE_STOP/ONCE_HOLD), `KnifeAnimationState` | `Internal68`, `Tracks`, `BaseTrack`, `Mode6`, `Inspect` |
|
||||
| Рендер первого лица | `KnifeFirstPersonRenderer` | `Thirdperson.renderFirstPerson`, `buildWorld`, `applyCamera`, `applyViewAnchor`, `submitArm`, `submitGeometry` |
|
||||
| Рендер третьего лица | `KnifeThirdPersonRenderer` | `Thirdperson.renderThirdPerson` |
|
||||
| Шейдер holy | `KnifeRenderTypes` + шейдер `assets/lovisual/shaders/knife_holy.{vsh,fsh}` (писать по образцу `melee_holy`, регистрация pipeline в `LoVisualRenderPipelines`) | `Melee` + `melee_holy.vsh/fsh` |
|
||||
| Звуки | `KnifeSounds` + `sounds.json` в `assets/lovisual/` | `Melee5` + `evolution/sounds.json` |
|
||||
| Загрузка | `KnifeModelCache` (асинхронная, кэш, вытеснение) | `Melee4` |
|
||||
| Миксины | `KnifeInputMixin` (на `Minecraft`), `KnifeItemInHandMixin` (на `ItemInHandRenderer`), `KnifePlayerItemInHandLayerMixin` (на `PlayerItemInHandLayer`) | `MeleeInputMixin`, `MeleeItemInHandMixin`, `MeleePlayerItemInHandLayerMixin` |
|
||||
|
||||
Ресурсы к копированию (источник, для отладки; см. предупреждение о лицензии): весь каталог `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/evolution/cosmetic/melee/` (`index.json`, 21 `.geo.json`, 21 `.anim.json`), все png из `.../textures/cosmetic/melee/` и `.../textures/cosmetic/melee/slot/` (кроме `.ppm`), каталог `.../sounds/melee/` + выбранные ключи из `.../sounds.json`, шейдеры `.../shaders/core/melee_holy.{vsh,fsh}`. Целевые пути у нас: `assets/lovisual/knife/*.json`, `assets/lovisual/textures/knife/*.png`, `assets/lovisual/sounds/knife/*.ogg`.
|
||||
|
||||
Что у нас уже есть и можно переиспользовать: `ItemInHandRendererMixin` (`mixins/render/item/ItemInHandRendererMixin.java`, уже перехватывает `submitArmWithItem` и `submitHandsWithItems`-путь), `HoldMyItems`/`ViewModel` (смещения руки), `EquipmentLayerRendererMixin`, текстуры через `Identifier`, `LoVisualRenderPipelines`. Чего нет и надо сделать с нуля: парсер Bedrock geo, система клипов, пайплайн и шейдер holy, миксин на `PlayerItemInHandLayer`, миксин ввода (атака/ПКМ), каталог скинов, UI выбора (иконки слотов).
|
||||
|
||||
Порядок работы: 1) общий `BedrockGeoParser` + бейк (он же нужен для крыльев), 2) клипы + `KnifeAnimationState` (draw, static_idle, inspect, melee_1..3 без бега), 3) рендер первого лица без рук (только нож), 4) руки игрока в `righthand_pos`/`lefthand_pos`, 5) третье лицо, 6) звуки, 7) бег, варианты annihil, holy-шейдер, 8) UI и сохранение выбора.
|
||||
|
||||
---
|
||||
|
||||
## 4. HoldMyItems и Lua ("нода на Lua")
|
||||
|
||||
### 4.1 Как это устроено в evo
|
||||
|
||||
evo бандлит оригинальный мод HoldMyItems целиком (миксин-конфиг `holdmyitems.mixins.json`, лист `"package":"evovis.mixin"`) и обёртку-модуль `HmiModule` (`evovis/module/render/HmiModule.java`: только флаг `active()`). Движок скриптов: LuaJ 3.0.1 (`/storage/project/jvm/LoVisual/ref/evo/mod/jar/META-INF/jars/luaj-jse-3.0.1.jar`, исходники `evovis`-дерева `org/luaj/vm2`).
|
||||
|
||||
Скрипты (источник): `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/minecraft/holdmyitems/`
|
||||
|
||||
| Файл | Размер | Назначение |
|
||||
|---|---|---|
|
||||
| `hand_pose.lua` | 66 КБ | поза рук "сцены" (экипировка, использование: bow, crossbow, trident, spear, eat/drink, brush, block, подмена свинга для мечей и т.д.) |
|
||||
| `hand_relative_pose.lua` | 1.6 КБ | поза руки относительно сцены (пустая рука, смена предмета, бросаемое) |
|
||||
| `item_pose.lua` | 45 КБ | поза самого предмета в руке |
|
||||
| `item_model.lua` | 20 КБ | анимация частей (квадов) модели предмета: вёдра, лодки, пакеты |
|
||||
| `hand_pose.lua.rpo`, `item_pose.lua.rpo` | 157 Б | опции Respackopts (`hmi.xOffset/yOffset/zOffset/inspectKeybind`) |
|
||||
|
||||
Необязательные "аддоны" подхватываются как `holdmyitems/hand_addon.lua`, `hand_relative_addon.lua`, `item_addon.lua`, `item_model_addon.lua` (со всех ресурс-паков, `getAllResources`).
|
||||
|
||||
Ресурсы вокруг скриптов (источник `/storage/project/jvm/LoVisual/ref/evo/mod/jar/assets/minecraft/`): `items/*.json` (35, лодки, вёдра, end_crystal и др.), `models/item/*.json` (48 3D/2D моделей, `eatinganimation/` 17 моделей), `textures/item/` (58), `textures/particle/` (36, пламя и glow для Lua-частиц), `optifine/`.
|
||||
|
||||
Классы evo (читаемые имена; оригинал в скобках):
|
||||
|
||||
- Загрузчик: `evovis/internal/Holdmyitems.java` (`SimpleSynchronousResourceReloadListener`, id `holdmyitems:lua_animation_loader`). Грузит 4 основных + 4 вида аддонов при перезагрузке ресурсов. Препроцессор `preprocessScript`: строки `global.x = v;` превращаются в `local x = registry:getOrDefault('x', v)` и в конце скрипта добавляется `registry:put('x', x)` (персистентные между кадрами переменные); плейсхолдеры `${name}` заменяются на `0`, если нет мода respackopts.
|
||||
- Песочница и глобалы: `evovis/internal/Internal82.java` (`LuaScriptManager`): `Globals` с библиотеками Base, Package, Bit32, Table, String, Coroutine, JseMath, `LoadState.install`, `LuaC.install`.
|
||||
- Кэш скрипта руки/предмета: `evovis/internal/HandScriptCache.java` (аналог `LuaScriptCache`): регистрирует в Lua глобалы `M, I, Texture, Items, Tags, P, Easings, KeyBindManager, S, C, particleManager, swingSpeed, registry, renderAsBlock, translateItem, itemSwingSpeed, animator, useDuration, usingItem, debugger, applyBlockRotation, context` и вызывает `chunk.invoke()` на каждый кадр. При ошибке Lua: тост "HMI Lua Runtime error!" и `canRun=false` до перезагрузки.
|
||||
- Кэш скрипта модели: `evovis/internal/ItemModelCache.java` (глобал `data` вместо `context`), `executeModel`.
|
||||
- Контекст кадра: `Context` (поля `mainHand, hand, bl (правая ли рука), swingProgress, mainHandSwingProgress, offHandSwingProgress, equipProgress, matrices (PoseStack), item, player, deltaTime, mainHandSwitchEvent, offHandSwitchEvent, swingMHand, swingOHand, interact, blockBreaking, particles`).
|
||||
- Опасная часть: `evovis/mixin/JavaMethodMixin.java` патчит `org.luaj.vm2.lib.jse.JavaMethod.invokeMethod` и возвращает NIL для любого Java-метода без аннотации `@Safe` (кроме `getOrDefault`/`put`): так защищают от того, что Lua дёргает произвольные Java-методы.
|
||||
- Обёртки API (Lua-видимые): `MInstance` (`M:`: moveX/Y/Z, rotateX/Y/Z (с pivot), scale, translate, shear, push/pop, sin/cos/abs/round/floor/ceil/pow/clamp/lerp), `IInstance` (`I:`: isOf, isIn, isEmpty, isBlock, isThrowable, isLantern, isChargedCrossbow, isEnchanted, getUseAction, getName, getAttackDamage, getSpearData, setSwingSpeed, setRenderAsBlock, setTranslate, shouldRenderAsBlock, shouldTranslateItem, isCustomTranslate, setMainStack/setOffStack, getComponents, copyAppearanceComponents, getDefaultStack), `PInstance` (`P:`: getSpeed, getXSpeed/YSpeed/ZSpeed, getYaw/Pitch, getX/Y/Z, getAge, getHealth, isOnGround, isSneaking, isSwimming, isCrawling, isClimbing, isUsingItem, isUsingRiptide, isUsingSpyglass, isTouchingWater, isSubmergedInWater, hasVehicle, isItemCoolingDown, getActiveHand, getMainItem, getOffhandItem, getSwingCount, getBlockBelow/Above/Standing), `EasingsInstance` (30 easing функций: easeInOutBack, easeOutElastic, easeOutBounce, cubicEase и др.), `JsItemsInstance` (`Items:get("minecraft:...")`), `JsTagsInstance` (`Tags:getVanillaTag/getFabricTag`), `KeyBindManagerInstance` (`isKeyPressed`), `SInstance` (`S:playSound`), `CInstance` (`C:setCamRot/setCamPos`, камера), `TextureInstance`, `ParticleManagerInstance` (`particleManager:addParticle(...)` с Lua-колбэком на тик; сама система частиц `Particle`, `ParticleManager`, рендер-типы в мире и в руке).
|
||||
- Анимация частей модели предмета: `ModelPartAnimator` (`animator:rotateX/Y/Z(from, to, deg, pivotX, pivotY, pivotZ)`, `moveX/Y/Z`, `scale` по диапазону индексов частей `from..to`, применяется при рендере квадов через `ItemRendererMixin`/`BlockRenderManagerMixin`).
|
||||
|
||||
Чистый, не обфусцированный референс тех же классов: `/storage/project/jvm/LoVisual/ref/clients/mercury-src/com/holdmylua/source/`:
|
||||
`lua_runtime/{LuaScriptManager,LuaScriptCache,ModelScriptCache,ScriptHolder}.java`, `lua_runtime/resource_controller/LuaAnimationResourceLoader.java`, `global/{LuaContext,GlobalsStorage,DispatcherStorage}.java`, `global/item_model/*`, `scripting/script_wrappers/{M,P,IInstance,Easings,C,S,JSItems,JSTags}.java`, `scripting/custom_api/*`, `model/{ModelPartAnimator,PoseX/Y/Z,RotationX/Y/Z,ScalePose}.java`, `patricles/*` (Particle 450 строк, ParticleManager), `annotation/Safe.java`, `access/*` (аксессоры), `SwordAttacks.java`, `data_structures/SpearData.java`, `compat/IrisCompat.java`.
|
||||
Миксины: `/storage/project/jvm/LoVisual/ref/clients/mercury-src/fun/mercury/mixin/hmi/*` (BlockRenderManagerMixin, HandRenderTypeMixin, HmiCameraMixin, HmiGameRendererMixin (deltaTime по GLFW, не более 0.05), HmiLivingEntityMixin (счётчики свинга), HmiMinecraftClientMixin (doAttack, doItemUse), ItemRendererMixin, ItemRenderStateMixin, ItemStackMixin (поля transform, swingSpeed, renderAsBlock), JavaMethodMixin, UseDurationMixin, UsingItemMixin) и главный `HeldItemRendererMixin.java` (905 строк; точки вызова скриптов: `hmi$itemPose` > `item_pose`, `hmi$mainHandPose` > `hand_relative_pose`, `hmi$scenePoseMain` > `hand_pose`; `hmi$applyArmMatrices` копирует ванильное построение руки; `wrapApplyEquipOffset`/`wrapSwingArm` отключают ванильную экипировку и свинг; `hmi$renderOverhaul` переопределяет `renderItem`).
|
||||
|
||||
Другие Lua-системы в ref (для идей "нод"/скриптов модулей, не руки):
|
||||
|
||||
- `/storage/project/jvm/LoVisual/ref/clients/sacura-1.21.4/src/main/java/su/sacura/util/impl/lua/`: `LuaManager` (папка `<run>/sacura/lua`, вызывает глобальные `onEnable/onDisable/onUpdate/onRender` у каждого скрипта-модуля), `LuaScript` (скрипт как `Module`), API `ClientApi, GlobalApi, ModulesApi, PlayerApi, Render3DApi, RenderApi, SettingsApi, Vec2`, `render/LuaFontManager`. Зависимость: `org.luaj:luaj-jse:3.0.1` (в `build.gradle` как `include`).
|
||||
- `/storage/project/jvm/LoVisual/ref/clients/newcode-1.21/` (`FiguraAvatarInstaller`, `figura_avatars/*/script.lua`): Figura-аватары на Lua (`test_luaj.gradle`).
|
||||
- `/storage/project/jvm/LoVisual/ref/clients/mercury-src/` (HoldMyLua, выше).
|
||||
- `/storage/project/jvm/LoVisual/ref/README.md` упоминает `hmi` (HoldMyItems event API) и `mercury-src` (HoldMyLua), но каталога `ref/clients/hmi` на диске сейчас нет; в `mod/TODO.md` строки 546-560 описан разбор HMI.zip в `/tmp/lv_refs/hmi/` (Delta HMI, обёртка над сторонним `holdmyitems-5.1.1+26.2.jar`).
|
||||
|
||||
### 4.2 Что есть у нас
|
||||
|
||||
Только чистая Java-динамика, без Lua и без скриптов:
|
||||
|
||||
- `features/module/modules/visuals/items/held/HoldMyItems.java` (`id="holdmyitems"`, displayName "Item Holder"): настройки `sway_intensity`, `ease_strength`, `offhand`, `walk_bob`/`bob_strength`, `use_drift`/`drift_strength`, `join_appear`, `grip_enabled` + 6 слайдеров grip (`gripExtend/Slide/Out/Forward/Tilt/Turn/Roll`). Метод `onFrame` вызывает `HandAnimState.beginFrame`. Метод `shapeEquipProgress`.
|
||||
- `util/animation/hand/HandDynamics.java` (применяет позу в `submitArmWithItem` HEAD через `pushPose`), `HandPoseMath` (sway, bob, drift, joinDrop, grip), `HandAnimState`, `SwayState`, `BobMath`, `HeldAlongFist`.
|
||||
- `util/animation/EquipProgressComposer.java` + `EaseCurves` (кривая equip).
|
||||
- `mixins/render/item/ItemInHandRendererMixin.java`: инъекции `submitArmWithItem` HEAD/RETURN (динамика руки), HEAD (ViewModel offsets), `@ModifyVariable` equipProgress (ordinal 3), `renderItem` (мини-предметы), `swingArm` (кастомные свинги ViewModel `Hammer/Chop/Arc/Double/Shake/Shove/Slash/Thrust` из `visuals/camera/swing/*`).
|
||||
- Родственные: `visuals/camera/ViewModel.java`, `visuals/items/held/AlwaysVisibleHand.java`, `mixins/render/item/ItemInHandAlwaysVisibleMixin.java`.
|
||||
|
||||
Решение по Lua в `mod/TODO.md` (строки 546-558): полный HMI не портировали из-за правила "без стороннего jar в сборке" и KISS; взята только идея grip. Скриптового движка для рук нет. В проекте есть только Javet (V8) для UI/HUD-скриптов: `render/engine/renderer/ui/runtime/script/engine/{UiScriptEngine,JavetUiScriptEngine,UiScriptEngineProvider}.java` (подключение в `build.gradle`: `bundledLibrary "com.caoccao.javet:javet:5.0.9"`, строка 186). Lua/LuaJ в проекте отсутствует (в зависимостях нет `luaj`).
|
||||
|
||||
### 4.3 Чего не хватает до полного функционала HMI (список)
|
||||
|
||||
Движок и инфраструктура:
|
||||
|
||||
1. Зависимость LuaJ: `bundledLibrary 'org.luaj:luaj-jse:3.0.1'` в `mod/build.gradle` рядом со строкой Javet. Версия та же, что у evo, mercury и sacura. Лицензия LuaJ (MIT) допускает бандл, но это "сторонняя библиотека в сборке", как Javet, а не сторонний мод.
|
||||
2. Пакет `features/module/modules/visuals/items/held/lua/`: `LuaHandRuntime` (общие `Globals`, как `Internal82`), `HandScript` (аналог `HandScriptCache`: 3 основных скрипта руки/предмета + аддоны), `ItemModelScript` (аналог `ItemModelCache`), `HandScriptContext` (поля из `Context`), `ScriptHolder`, `LuaScriptPreprocessor` (регэкспы `global.x = v;` > `registry`, `${name}` > `0`), `HmiResourceReloadListener` (загрузка `holdmyitems/*.lua` из ресурс-паков, плюс из `config/lovisual/holdmyitems/` для пользовательских скриптов).
|
||||
3. Песочница. Evo и mercury используют миксин на `JavaMethod` + `@Safe`. Лучше без миксина на сторонний класс: не отдавать Lua `CoerceJavaToLua.coerce(javaObject)` (даёт доступ ко всем public-методам, включая `getClass()`), а собирать `LuaTable` с `LibFunction`-обёртками только нужных методов (M, I, P, Easings, Items, Tags, S, C, KeyBindManager, particleManager, registry). `Globals` без `JseBaseLib.loadfile/dofile` и без `luajava`, `io`, `os`. Это закрывает вопрос безопасности скриптов из чужих ресурс-паков.
|
||||
4. Реестр персистентных переменных `registry` (HashMap) со сбросом при перезагрузке ресурсов.
|
||||
5. Дельта времени кадра: по GLFW (`HmiGameRendererMixin`: `min(0.05, dt)`, 0 при паузе). У нас есть `render/helpers/util/TickDelta`.
|
||||
6. Тосты/лог ошибок Lua (`canRun=false` до reload).
|
||||
|
||||
Хуки рендера (нашего `ItemInHandRendererMixin` для этого мало):
|
||||
|
||||
7. Подмена всей цепочки руки: обернуть/перенаправить вызов построения руки и предмета (аналог `hmi$renderOverhaul`, `wrapApplyEquipOffset`, `wrapSwingArm`), чтобы Lua управлял `equipProgress` и свингом вместо ванили. Сейчас у нас только `ModifyVariable` equipProgress, `swingArm` cancel для ViewModel и pushPose/popPose.
|
||||
8. Раздельные фазы скриптов: `hand_pose` (сцена), `hand_relative_pose` (рука относительно), `item_pose` (предмет), плюс вызов `item_model` при рендере модели.
|
||||
9. Счётчики свинга и события (`mainHandSwingProgress/offHandSwingProgress`, `swingMHand/swingOHand`, `interact`, `blockBreaking`, `mainHandSwitchEvent/offHandSwitchEvent`): миксины `HmiLivingEntityMixin`, `HmiMinecraftClientMixin` (doAttack, doItemUse), `getSwingCount`.
|
||||
10. Рендер обеих рук всегда (`HandRenderTypeMixin`: `renderMainHand/renderOffHand = true`).
|
||||
11. Подмена параметров ItemStack на лету: `ItemStackMixin` (поля transform, swingSpeed, renderAsBlock) и `setMainStack/setOffStack`, `copyAppearanceComponents`.
|
||||
12. Подмена свойств item-моделей `use_duration` и `using_item` (`UseDurationMixin`, `UsingItemMixin`) для анимаций еды/лука/арбалета.
|
||||
13. Аниматор частей модели (`ModelPartAnimator` + хуки на рендер квадов `ItemRendererMixin`, `BlockRenderManagerMixin`, `ItemRenderStateMixin` сброс диспетчера) для `item_model.lua` (вёдра с рыбой, лодки, пакеты).
|
||||
14. Камера из Lua (`C:setCamRot/setCamPos`, `HmiCameraMixin`) для crawl/climb/riptide.
|
||||
15. Lua-частицы: `Particle`, `ParticleManager`, рендер-типы (мир/рука), `particleManager:addParticle` с Lua-колбэком (пламя, glow).
|
||||
16. Привязка клавиши осмотра (`KeyBindManager`, `hmi.inspectKeybind`, `S:playSound`). У нас есть настройки кнопки в ClickGUI (`OpenKeySetting`-аналог), надо пробросить в Lua.
|
||||
|
||||
Контент:
|
||||
|
||||
17. Сами скрипты: `hand_pose.lua`, `hand_relative_pose.lua`, `item_pose.lua`, `item_model.lua` (источник: каталог из 4.1). Для релиза свои или с разрешением автора HMI/evo. Скрипты завязаны на ресурсы `assets/minecraft/items/*.json`, `models/item/*` (3D-вёдра, лодки, eatinganimation), `textures/particle/*`.
|
||||
18. Настройки в ClickGUI: выбор набора скриптов, переключатели per-feature (inspect, eating, bow, crossbow, trident, spear, sword attacks), горячая перезагрузка скриптов (`.reload`-команда или кнопка).
|
||||
19. Тесты: препроцессор `global.x`, песочница (скрипт не вызывает `os/io`, не достаёт `getClass`), математика `M:` и `Easings:` (по образцу `HandPoseMath`-тестов).
|
||||
|
||||
Что у нас лучше evo: готовые Java-режимы (sway/bob/drift/grip), настраиваемые слайдеры в ClickGUI, ViewModel-свинги, нет зависимости от Lua-интерпретатора и риска чужих скриптов. Рекомендация: сначала добавить пункты 1-8 как режим "Scripted" внутри существующего `HoldMyItems` (переключатель `Dynamics (Java)` / `Scripts (Lua)`), собственные короткие примеры скриптов, и лишь затем наращивать 9-16.
|
||||
|
||||
### 4.4 Минимальный план "нода на Lua" (если нужна именно скриптуемая нода)
|
||||
|
||||
Идея аналогична sacura `LuaManager`/`LuaScript`: Lua-файл = модуль/узел, который объявляет `onEnable`, `onDisable`, `onUpdate`, `onRender`. Минимум:
|
||||
|
||||
1. `LuaScriptManager` + `LuaScript` (по образцу `sacura/util/impl/lua`), папка `<run>/lovisual/lua`, регистрация скрипта как `Module` в нашем реестре модулей (`features/module/lifecycle/Modules`).
|
||||
2. Сначала только безопасные API: `Player`, `Render3D` (линии, квады через `Renderer3D`), `Settings` (создание num/bool/mode значений как у `Module.num/bool/modeSetting`), `Client` (chat print).
|
||||
3. Для hold-items узла: отдельный тип скрипта `held`, вызываемый из `ItemInHandRendererMixin` с `HandScriptContext`, `matrices`, глобалами `M, I, P, Easings` из 4.1.
|
||||
|
||||
Альтернатива Lua: у нас уже есть Javet/V8 (`UiScriptEngine`). Если совместимость с готовыми HMI-паками не нужна, то скрипты рук можно писать на JS и не тянуть второй интерпретатор. Но экосистема HMI (готовые `hand_pose.lua` и т.д.) только на Lua, поэтому для "полного функционала HMI" нужен именно LuaJ.
|
||||
|
||||
---
|
||||
|
||||
## 5. Итоговый приоритет работ
|
||||
|
||||
1. Общий `BedrockGeoParser` + бейк + сэмплер анимации (нужен и крыльям, и ножам). Затем режим "модель" в Wings (раздел 2.4): ближайший заметный результат при умеренной сложности (M).
|
||||
2. Модуль Knife (раздел 3.7): самая эффектная фича, сложность L, строить этапами.
|
||||
3. Быстрые модули: TNTTimer, AutoSprint, TotemCounter, PlaceAnimation, EnchantGlint, ArmorColor, GodRays, ProjectileTrails.
|
||||
4. DeathEffects, Blizzard, ElytraTrails.
|
||||
5. LuaJ-движок и скриптовый режим HoldMyItems (раздел 4.3, пункты 1-8), потом остальное.
|
||||
6. Лицензионный вопрос по ассетам evo решить до начала копирования любых файлов.
|
||||
|
|
@ -18,3 +18,5 @@ xaero_minimap_version=fabric-26.2-26.1.3
|
|||
xaero_worldmap_version=fabric-26.2-1.41.1
|
||||
viafabricplus_version=4.6.0
|
||||
entityculling_version=1.10.5
|
||||
voicechat_api_version=2.6.24
|
||||
mixinextras_version=0.5.5
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package dev.loki.lovisual.compat.voicechat;
|
||||
|
||||
import de.maxhenkel.voicechat.api.VoicechatApi;
|
||||
import de.maxhenkel.voicechat.api.VoicechatClientApi;
|
||||
import de.maxhenkel.voicechat.api.VoicechatPlugin;
|
||||
import de.maxhenkel.voicechat.api.events.ClientReceiveSoundEvent;
|
||||
import de.maxhenkel.voicechat.api.events.ClientSoundEvent;
|
||||
import de.maxhenkel.voicechat.api.events.ClientVoicechatConnectionEvent;
|
||||
import de.maxhenkel.voicechat.api.events.EventRegistration;
|
||||
import de.maxhenkel.voicechat.api.events.MicrophoneMuteEvent;
|
||||
|
||||
/**
|
||||
* Simple Voice Chat plugin entrypoint ({@code "voicechat"} in fabric.mod.json). Only loaded
|
||||
* by the voice chat mod itself, so nothing here is ever resolved when it is not installed.
|
||||
*/
|
||||
public final class LoVisualVoicechatPlugin implements VoicechatPlugin {
|
||||
|
||||
@Override
|
||||
public String getPluginId() {
|
||||
return "lovisual";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(VoicechatApi api) {
|
||||
if (api instanceof VoicechatClientApi client) {
|
||||
VoiceChatBridge.attach(new ClientProbe(client));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEvents(EventRegistration registration) {
|
||||
VoiceState state = VoiceChatBridge.rawState();
|
||||
registration.registerEvent(ClientReceiveSoundEvent.EntitySound.class,
|
||||
e -> state.markSpeaking(e.getEntityId(), e.isWhispering()));
|
||||
registration.registerEvent(ClientSoundEvent.class, e -> state.markSelfSpeaking(e.isWhispering()));
|
||||
registration.registerEvent(MicrophoneMuteEvent.class, e -> state.setMuted(e.isDisabled()));
|
||||
registration.registerEvent(ClientVoicechatConnectionEvent.class, e -> state.setConnected(e.isConnected()));
|
||||
}
|
||||
|
||||
private record ClientProbe(VoicechatClientApi api) implements VoiceChatBridge.SelfProbe {
|
||||
@Override
|
||||
public boolean muted() {
|
||||
return api.isMuted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disabled() {
|
||||
return api.isDisabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean disconnected() {
|
||||
return api.isDisconnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pushToTalk() {
|
||||
return api.isPushToTalkKeyPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package dev.loki.lovisual.compat.voicechat;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
/**
|
||||
* Safe entry point to Simple Voice Chat data. Never touches the voice chat API itself, so
|
||||
* any class may use it whether or not the mod is installed; the plugin feeds it when present.
|
||||
*/
|
||||
public final class VoiceChatBridge {
|
||||
public static final String MOD_ID = "voicechat";
|
||||
|
||||
private static final VoiceState STATE = new VoiceState(System::currentTimeMillis);
|
||||
private static volatile SelfProbe probe;
|
||||
|
||||
private VoiceChatBridge() {
|
||||
}
|
||||
|
||||
/** Live reads of the local microphone, implemented by the plugin on top of the voice chat API. */
|
||||
public interface SelfProbe {
|
||||
boolean muted();
|
||||
|
||||
boolean disabled();
|
||||
|
||||
boolean disconnected();
|
||||
|
||||
boolean pushToTalk();
|
||||
}
|
||||
|
||||
public static boolean isInstalled() {
|
||||
return FabricLoader.getInstance().isModLoaded(MOD_ID);
|
||||
}
|
||||
|
||||
/** Current state with the local microphone flags refreshed; cheap enough to call every frame. */
|
||||
public static VoiceState state() {
|
||||
SelfProbe p = probe;
|
||||
if (p != null) {
|
||||
STATE.setMuted(p.muted());
|
||||
STATE.setDisabled(p.disabled());
|
||||
STATE.setPushToTalk(p.pushToTalk());
|
||||
STATE.setConnected(!p.disconnected());
|
||||
}
|
||||
return STATE;
|
||||
}
|
||||
|
||||
static VoiceState rawState() {
|
||||
return STATE;
|
||||
}
|
||||
|
||||
static void attach(SelfProbe selfProbe) {
|
||||
probe = selfProbe;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package dev.loki.lovisual.compat.voicechat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
/**
|
||||
* Thread-safe snapshot of who is talking in Simple Voice Chat. Written from the voice
|
||||
* threads by the plugin, read from the render thread by the module and the HUD. Holds no
|
||||
* Simple Voice Chat or Minecraft types, so it stays testable and safe to load without the mod.
|
||||
*/
|
||||
public final class VoiceState {
|
||||
|
||||
/** State of the local microphone, ordered by display priority. */
|
||||
public enum MicStatus { DISCONNECTED, DISABLED, MUTED, WHISPERING, TALKING, PUSH_TO_TALK, IDLE }
|
||||
|
||||
/** One remote speaker; {@code lastMs} is when audio from them was last received. */
|
||||
public record Speaker(UUID id, boolean whisper, long lastMs) { }
|
||||
|
||||
private record Voice(boolean whisper, long lastMs) { }
|
||||
|
||||
private static final Comparator<Speaker> NEWEST_FIRST = Comparator.comparingLong(Speaker::lastMs).reversed();
|
||||
|
||||
private final LongSupplier clock;
|
||||
private final Map<UUID, Voice> speakers = new ConcurrentHashMap<>();
|
||||
private volatile Voice self = new Voice(false, Long.MIN_VALUE);
|
||||
private volatile boolean connected;
|
||||
private volatile boolean muted;
|
||||
private volatile boolean disabled;
|
||||
private volatile boolean pushToTalk;
|
||||
|
||||
public VoiceState(LongSupplier clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public void markSpeaking(UUID id, boolean whisper) {
|
||||
if (id != null) speakers.put(id, new Voice(whisper, clock.getAsLong()));
|
||||
}
|
||||
|
||||
public void markSelfSpeaking(boolean whisper) {
|
||||
self = new Voice(whisper, clock.getAsLong());
|
||||
}
|
||||
|
||||
public void setConnected(boolean value) {
|
||||
connected = value;
|
||||
if (!value) speakers.clear();
|
||||
}
|
||||
|
||||
public void setMuted(boolean value) { muted = value; }
|
||||
|
||||
public void setDisabled(boolean value) { disabled = value; }
|
||||
|
||||
public void setPushToTalk(boolean value) { pushToTalk = value; }
|
||||
|
||||
public boolean isConnected() { return connected; }
|
||||
|
||||
public void clear() {
|
||||
speakers.clear();
|
||||
self = new Voice(false, Long.MIN_VALUE);
|
||||
}
|
||||
|
||||
public boolean isSpeaking(UUID id, long windowMs) {
|
||||
Voice v = speakers.get(id);
|
||||
return v != null && age(v) <= windowMs;
|
||||
}
|
||||
|
||||
public boolean isWhispering(UUID id, long windowMs) {
|
||||
Voice v = speakers.get(id);
|
||||
return v != null && v.whisper() && age(v) <= windowMs;
|
||||
}
|
||||
|
||||
/** Fade factor: 1 while audio is arriving, falling linearly to 0 over {@code fadeMs} after it stops. */
|
||||
public float level(UUID id, long fadeMs) {
|
||||
Voice v = speakers.get(id);
|
||||
return v == null ? 0f : fade(age(v), fadeMs);
|
||||
}
|
||||
|
||||
public float selfLevel(long fadeMs) {
|
||||
return fade(age(self), fadeMs);
|
||||
}
|
||||
|
||||
public boolean isSelfWhispering(long windowMs) {
|
||||
Voice v = self;
|
||||
return v.whisper() && age(v) <= windowMs;
|
||||
}
|
||||
|
||||
/** Remote speakers heard within {@code windowMs}, newest first; stale entries are dropped. */
|
||||
public List<Speaker> speakers(long windowMs, int max) {
|
||||
List<Speaker> out = new ArrayList<>();
|
||||
speakers.entrySet().removeIf(e -> age(e.getValue()) > Math.max(windowMs * 4, 10_000L));
|
||||
for (Map.Entry<UUID, Voice> e : speakers.entrySet()) {
|
||||
if (age(e.getValue()) <= windowMs) {
|
||||
out.add(new Speaker(e.getKey(), e.getValue().whisper(), e.getValue().lastMs()));
|
||||
}
|
||||
}
|
||||
out.sort(NEWEST_FIRST);
|
||||
return out.size() > max ? new ArrayList<>(out.subList(0, Math.max(0, max))) : out;
|
||||
}
|
||||
|
||||
public MicStatus micStatus(long windowMs) {
|
||||
if (!connected) return MicStatus.DISCONNECTED;
|
||||
if (disabled) return MicStatus.DISABLED;
|
||||
if (muted) return MicStatus.MUTED;
|
||||
if (age(self) <= windowMs) return self.whisper() ? MicStatus.WHISPERING : MicStatus.TALKING;
|
||||
return pushToTalk ? MicStatus.PUSH_TO_TALK : MicStatus.IDLE;
|
||||
}
|
||||
|
||||
private long age(Voice v) {
|
||||
return v.lastMs() == Long.MIN_VALUE ? Long.MAX_VALUE : clock.getAsLong() - v.lastMs();
|
||||
}
|
||||
|
||||
private static float fade(long ageMs, long fadeMs) {
|
||||
if (ageMs == Long.MAX_VALUE || ageMs > fadeMs) return 0f;
|
||||
return fadeMs <= 0 ? 1f : 1f - (float) ageMs / fadeMs;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,10 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
private final ModeValue menuBg =
|
||||
new ModeValue("menuBg", "waves", "off", "waves", "aurora");
|
||||
|
||||
//@CFGComment("Main menu layout: orbs / hero")
|
||||
private final ModeValue menuLayout =
|
||||
new ModeValue("menuLayout", "orbs", "orbs", "hero");
|
||||
|
||||
//@CFGComment("Use ClickGUI theme colors for menu background shader")
|
||||
private final BooleanValue menuBgUseTheme =
|
||||
new BooleanValue("menuBgUseTheme", true);
|
||||
|
|
@ -147,6 +151,10 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
return menuBg.get();
|
||||
}
|
||||
|
||||
public String getMenuLayout() {
|
||||
return menuLayout.get();
|
||||
}
|
||||
|
||||
public boolean isMenuBackgroundUseTheme() {
|
||||
return menuBgUseTheme.get();
|
||||
}
|
||||
|
|
@ -194,9 +202,16 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
List<SettingDef> list = new ArrayList<>();
|
||||
list.add(SettingDef.mode(msaa3d)
|
||||
.unavailableWhen(IrisRuntime::isShaderpackRendererActive, MainConfig::irisMsaaReason));
|
||||
return list;
|
||||
}
|
||||
|
||||
/** Everything about the main menu in one group: layout, background, theme colors, clock. */
|
||||
public List<SettingDef> getMainMenuSettingDefs() {
|
||||
List<SettingDef> list = new ArrayList<>();
|
||||
list.add(SettingDef.mode(menuLayout));
|
||||
list.add(SettingDef.mode(menuBg));
|
||||
list.add(SettingDef.bool(menuClockShowSeconds));
|
||||
list.add(SettingDef.bool(menuBgUseTheme));
|
||||
list.add(SettingDef.bool(menuBgUseTheme).visibleWhen(() -> !"off".equalsIgnoreCase(menuBg.get())));
|
||||
list.add(SettingDef.bool(menuClockShowSeconds).visibleWhen(() -> !"hero".equalsIgnoreCase(menuLayout.get())));
|
||||
return list;
|
||||
}
|
||||
|
||||
|
|
@ -274,6 +289,7 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
List<ConfigValue<?>> list = new ArrayList<>();
|
||||
list.add(debug);
|
||||
list.add(menuBg);
|
||||
list.add(menuLayout);
|
||||
list.add(menuClockShowSeconds);
|
||||
list.add(menuBgUseTheme);
|
||||
list.add(msaa3d);
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package dev.loki.lovisual.features.command.impl.world;
|
|||
import dev.loki.lovisual.features.command.core.CommandContext;
|
||||
import dev.loki.lovisual.features.command.core.CommandInfo;
|
||||
import dev.loki.lovisual.features.command.util.ClientCommand;
|
||||
import dev.loki.lovisual.features.command.impl.CommandUtils;
|
||||
import dev.loki.lovisual.features.command.util.CommandOutput;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.misc.Balancer;
|
||||
import dev.loki.lovisual.util.mctiers.MCTiersCache;
|
||||
import dev.loki.lovisual.util.mctiers.MCTiersTier;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
|
|
@ -23,6 +25,19 @@ import java.util.Locale;
|
|||
descriptionKey = "command.tier.description"
|
||||
)
|
||||
public final class TierCommand implements ClientCommand {
|
||||
private static final List<String> MODES = List.of("vanilla", "uhc", "pot", "nethop", "smp", "sword", "axe", "mace");
|
||||
|
||||
@Override
|
||||
public List<String> suggest(CommandContext ctx, int argIndex, String token) {
|
||||
String lower = token == null ? "" : token.toLowerCase(Locale.ROOT);
|
||||
if (argIndex == 1) {
|
||||
return CommandUtils.suggestPlayers(ctx.mc(), token);
|
||||
}
|
||||
if (argIndex == 2) {
|
||||
return MODES.stream().filter(mode -> mode.startsWith(lower)).toList();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(CommandContext ctx) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package dev.loki.lovisual.features.command.suggest;
|
||||
|
||||
import com.mojang.brigadier.context.StringRange;
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Splits raw command suggestions into pickable completions and a usage-hint placeholder
|
||||
* (an entry like "%toggle <module>"), so the vanilla chat popup lists only real choices
|
||||
* and shows the hint as ghost text instead of inserting it.
|
||||
*
|
||||
* @param suggestions pickable completions, all sharing the first one's range; null when none
|
||||
* @param ghostHint placeholder such as "<module>" to show after the cursor, or null
|
||||
*/
|
||||
public record SuggestionPlan(Suggestions suggestions, String ghostHint) {
|
||||
|
||||
public static SuggestionPlan of(List<Suggestion> raw) {
|
||||
List<Suggestion> real = new ArrayList<>();
|
||||
String hint = null;
|
||||
if (raw != null) {
|
||||
for (Suggestion s : raw) {
|
||||
String text = s.getText();
|
||||
int open = text.lastIndexOf('<');
|
||||
if (open >= 0 && text.endsWith(">")) {
|
||||
if (hint == null) hint = text.substring(open);
|
||||
} else {
|
||||
real.add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (real.isEmpty()) return new SuggestionPlan(null, hint);
|
||||
StringRange range = real.get(0).getRange();
|
||||
return new SuggestionPlan(new Suggestions(range, real), hint);
|
||||
}
|
||||
}
|
||||
|
|
@ -297,7 +297,8 @@ public final class ModulesMenuPanel {
|
|||
settingsSmoothScroll = 0.0f;
|
||||
}
|
||||
|
||||
record ModuleHit(String id, float x, float y, float w, float h, boolean hasSettings, boolean toggleable) {
|
||||
record ModuleHit(String id, String title, String description, float x, float y, float w, float h,
|
||||
boolean hasSettings, boolean toggleable) {
|
||||
}
|
||||
|
||||
record SettingHit(Setting setting, float x, float y, float w, float h) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public enum ModulesMenuResolver {
|
|||
out.add(new ModuleComponent.CardEntry(
|
||||
module.name(),
|
||||
module.getDisplayName(),
|
||||
locked ? I18n.get("lovisual.policy.locked", ServerPolicyState.reason()) : "",
|
||||
locked ? I18n.get("lovisual.policy.locked", ServerPolicyState.reason()) : module.getDescription(),
|
||||
bind == null ? "" : bind,
|
||||
!module.getSettings().isEmpty(),
|
||||
module.isEnabled(),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public final class ModulesMenuScreen {
|
|||
float scale = 1.0f; float screenAnim = 0.0f; boolean openTarget = false;
|
||||
float searchAnim = 0.0f; float searchX; float searchY; float searchW; float searchH;
|
||||
float searchBtnX; float searchBtnY; float searchBtnW; float searchBtnH; float searchBtnHoverAnim;
|
||||
float descAnim; String descTitle; String descText;
|
||||
|
||||
public ModulesMenuScreen() {
|
||||
for (ModulesMenuCategory c : ModulesMenuCategory.values()) panels.add(new ModulesMenuPanel(c, c.ordinal()));
|
||||
|
|
@ -80,6 +81,7 @@ public final class ModulesMenuScreen {
|
|||
}
|
||||
ModulesMenuChromeRenderer.renderSearchButton(this, mouseX, mouseY, screenAnim, scale);
|
||||
ModulesMenuChromeRenderer.renderSearch(this, screenAnim, scale);
|
||||
ModulesMenuDescriptionRenderer.render(this, mouseX, mouseY, screenAnim, scale);
|
||||
ModulesMenuChromeRenderer.renderHints(this, screenAnim, scale);
|
||||
ClickGuiRenderer.flushRenderer();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
|
||||
/** Word wrapping for the hover description plaque, independent of any renderer. */
|
||||
public final class DescriptionLines {
|
||||
private static final String ELLIPSIS = "...";
|
||||
|
||||
private DescriptionLines() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Greedy word wrap; a single word wider than {@code maxWidth} gets its own line, and when more
|
||||
* than {@code maxLines} are needed the last line is cut with an ellipsis.
|
||||
*/
|
||||
public static List<String> wrap(String text, double maxWidth, ToDoubleFunction<String> measure, int maxLines) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (text == null || text.isBlank() || maxLines < 1) return lines;
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (String word : text.trim().split("\\s+")) {
|
||||
String candidate = line.isEmpty() ? word : line + " " + word;
|
||||
if (!line.isEmpty() && measure.applyAsDouble(candidate) > maxWidth) {
|
||||
lines.add(line.toString());
|
||||
line.setLength(0);
|
||||
candidate = word;
|
||||
}
|
||||
line.setLength(0);
|
||||
line.append(candidate);
|
||||
}
|
||||
if (!line.isEmpty()) lines.add(line.toString());
|
||||
return lines.size() <= maxLines ? lines : truncate(lines, maxLines, maxWidth, measure);
|
||||
}
|
||||
|
||||
private static List<String> truncate(List<String> lines, int maxLines, double maxWidth, ToDoubleFunction<String> measure) {
|
||||
List<String> out = new ArrayList<>(lines.subList(0, maxLines));
|
||||
String last = out.get(maxLines - 1);
|
||||
while (last.length() > 1 && measure.applyAsDouble(last + ELLIPSIS) > maxWidth) {
|
||||
last = last.substring(0, last.length() - 1);
|
||||
}
|
||||
out.set(maxLines - 1, last + ELLIPSIS);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.DescriptionLines;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D;
|
||||
import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hover plaque above the module columns: shows the description of the module row under the
|
||||
* cursor and fades out when nothing is hovered.
|
||||
*/
|
||||
final class ModulesMenuDescriptionRenderer {
|
||||
private static final int MAX_LINES = 3;
|
||||
|
||||
private ModulesMenuDescriptionRenderer() {
|
||||
}
|
||||
|
||||
static void render(ModulesMenuScreen screen, float mouseX, float mouseY, float screenAnim, float scale) {
|
||||
ModulesMenuPanel.ModuleHit hit = hoveredHit(screen, mouseX, mouseY);
|
||||
boolean show = hit != null && !hit.description().isBlank();
|
||||
if (show) {
|
||||
screen.descTitle = hit.title();
|
||||
screen.descText = hit.description();
|
||||
}
|
||||
screen.descAnim = AnimationUtility.snap(
|
||||
AnimationUtility.approach(screen.descAnim, show ? 1.0f : 0.0f, AnimationUtility.deltaTime(), 9.0f),
|
||||
show ? 1.0f : 0.0f, 0.01f);
|
||||
float alpha = ModulesMenuLayout.easeOutCubic(screenAnim) * ModulesMenuLayout.easeOutCubic(screen.descAnim);
|
||||
if (alpha <= 0.001f || screen.descText == null) return;
|
||||
|
||||
ModulesMenuChromeRenderer.ensureFonts();
|
||||
TextRenderer titleFont = ModulesMenuChromeRenderer.medium;
|
||||
TextRenderer bodyFont = ModulesMenuChromeRenderer.regular;
|
||||
float titleSize = 9.0f * scale;
|
||||
float bodySize = 7.5f * scale;
|
||||
float pad = 9.0f * scale;
|
||||
float w = Math.min(screen.areaW * 0.6f, 300.0f * scale);
|
||||
List<String> lines = DescriptionLines.wrap(screen.descText, w - pad * 2.0f,
|
||||
s -> ClickGuiRenderer.textWidth(bodyFont, s, bodySize), MAX_LINES);
|
||||
float titleH = ModulesMenuLayout.textHeight(titleFont, titleSize);
|
||||
float lineH = ModulesMenuLayout.textHeight(bodyFont, bodySize) + 1.5f * scale;
|
||||
float h = pad * 2.0f + titleH + 3.0f * scale + lines.size() * lineH;
|
||||
float x = screen.areaX + (screen.areaW - w) * 0.5f;
|
||||
float y = Math.max(screen.areaY + 6.0f * scale, panelsTop(screen) - h - 10.0f * scale) + (1.0f - alpha) * 6.0f * scale;
|
||||
|
||||
float radius = 8.0f * scale;
|
||||
ModulesMenuGlassRenderer.drawLiquidGlass(x, y, w, h, radius, false, alpha >= 0.999f ? 1.0f : alpha, scale);
|
||||
int veil = ModulesMenuLayout.withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * 0.35f);
|
||||
LayoutRender2D.roundedQuad(x, y, w, h, radius, veil, veil, veil, veil);
|
||||
ClickGuiRenderer.drawText(titleFont, screen.descTitle, x + pad, y + pad, titleSize,
|
||||
ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha), false);
|
||||
float ty = y + pad + titleH + 3.0f * scale;
|
||||
int bodyColor = ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha);
|
||||
for (String line : lines) {
|
||||
ClickGuiRenderer.drawText(bodyFont, line, x + pad, ty, bodySize, bodyColor, false);
|
||||
ty += lineH;
|
||||
}
|
||||
}
|
||||
|
||||
private static ModulesMenuPanel.ModuleHit hoveredHit(ModulesMenuScreen screen, float mx, float my) {
|
||||
if (!ModulesMenuLayout.isInteractive(screen.screenAnim, screen.openTarget)) return null;
|
||||
for (ModulesMenuPanel panel : screen.panels) {
|
||||
if (panel.selected != null || !ModulesMenuLayout.inside(mx, my, panel.x, panel.y, panel.w, panel.h)) continue;
|
||||
for (ModulesMenuPanel.ModuleHit hit : panel.hits) {
|
||||
if (ModulesMenuLayout.inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) return hit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static float panelsTop(ModulesMenuScreen screen) {
|
||||
float top = Float.MAX_VALUE;
|
||||
for (ModulesMenuPanel panel : screen.panels) top = Math.min(top, panel.y);
|
||||
return top == Float.MAX_VALUE ? screen.areaY : top;
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +87,8 @@ final class ModulesMenuModuleRenderer {
|
|||
Renderer2D.COLOR.svg("check", checkX, checkY, check, check,
|
||||
SvgRenderOptions.overrideColor(ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha * (0.10f + 0.90f * enabled))));
|
||||
}
|
||||
panel.hits.add(new ModulesMenuPanel.ModuleHit(entry.getId(), x, y, rowW, h, entry.hasSettings(), entry.toggleable()));
|
||||
panel.hits.add(new ModulesMenuPanel.ModuleHit(entry.getId(), entry.title(), entry.description() == null ? "" : entry.description(),
|
||||
x, y, rowW, h, entry.hasSettings(), entry.toggleable()));
|
||||
}
|
||||
|
||||
static void renderModuleRowHover(ModulesMenuPanel panel, float x, float y, float w, float h,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings;
|
||||
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
import dev.loki.lovisual.config.io.ConfigSerializer;
|
||||
import dev.loki.lovisual.config.definition.SettingDef;
|
||||
import dev.loki.lovisual.config.definition.SettingOwner;
|
||||
|
|
@ -64,7 +65,7 @@ public enum MenuSettingsResolver {
|
|||
out.add(new ModuleComponent.CardEntry(
|
||||
widget.getId(),
|
||||
widget.getTitle(),
|
||||
"",
|
||||
widget.getDescription(),
|
||||
"",
|
||||
!widget.getSettingDefs().isEmpty(),
|
||||
widget.isEnabled(),
|
||||
|
|
@ -147,7 +148,7 @@ public enum MenuSettingsResolver {
|
|||
return new ModuleComponent.CardEntry(
|
||||
staticHud.getId(),
|
||||
title,
|
||||
"",
|
||||
staticHud.getDescription(),
|
||||
"",
|
||||
!staticHud.getSettingDefs().isEmpty(),
|
||||
staticHud.isEnabled(),
|
||||
|
|
@ -156,7 +157,7 @@ public enum MenuSettingsResolver {
|
|||
}
|
||||
|
||||
private static ModuleComponent.CardEntry uiReadOnlyCard(String id, String title) {
|
||||
return new ModuleComponent.CardEntry(id, title, "", "", true, false, false);
|
||||
return new ModuleComponent.CardEntry(id, title, I18n.get("hud." + id + ".description"), "", true, false, false);
|
||||
}
|
||||
|
||||
private static String mapUiAlias(String id) {
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ public final class MainSettingsComponent {
|
|||
int text = active ? palette.menuHeaderText() : LayoutRender2D.alpha(palette.panelMuted(), 0.72f + 0.20f * hoverAnim);
|
||||
ClickGuiRenderer.drawText(
|
||||
ClickGuiRenderer.getInterMedium(),
|
||||
category.title,
|
||||
category.label(),
|
||||
x + 13f * scale,
|
||||
rowY + 6f * scale,
|
||||
7f * scale,
|
||||
|
|
@ -210,7 +210,7 @@ public final class MainSettingsComponent {
|
|||
LayoutRender2D.alpha(palette.menuLineStrong(), 0.82f),
|
||||
LayoutRender2D.alpha(palette.menuLineLow(), 0.55f)
|
||||
);
|
||||
ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), selected.title, rightX + pad, titleY, titleSize, text, false);
|
||||
ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), selected.label(), rightX + pad, titleY, titleSize, text, false);
|
||||
|
||||
contentX = rightX + pad;
|
||||
contentY = rightY + 27f * scale;
|
||||
|
|
@ -287,9 +287,10 @@ public final class MainSettingsComponent {
|
|||
settingsByCategory.clear();
|
||||
MainConfig config = MainConfig.get();
|
||||
settingsByCategory.put(Category.IMAGE, buildSettings(config.getImageSettingDefs(), config));
|
||||
settingsByCategory.put(Category.MAIN_MENU, buildSettings(config.getMainMenuSettingDefs(), config));
|
||||
settingsByCategory.put(Category.MISCELLANIOUS, buildSettings(config.getMiscellaneousSettingDefs(), config));
|
||||
settingsByCategory.put(Category.SECURITY, buildSettings(config.getSecuritySettingDefs(), config));
|
||||
List<Setting> utility = buildSettings(config.getUtilitySettingDefs(), config);
|
||||
List<Setting> utility = new java.util.ArrayList<>(buildSettings(config.getUtilitySettingDefs(), config));
|
||||
utility.addAll(buildSettings(StyleConfig.get().getSettingDefs(), StyleConfig.get()));
|
||||
utility.addAll(buildSettings(PlatformConfig.get().getSettingDefs(), PlatformConfig.get()));
|
||||
settingsByCategory.put(Category.UTILITY, utility);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.main;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting;
|
||||
|
||||
/**
|
||||
|
|
@ -9,6 +11,7 @@ import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting;
|
|||
*/
|
||||
enum Category {
|
||||
IMAGE("Image"),
|
||||
MAIN_MENU("Main menu"),
|
||||
SECURITY("Security"),
|
||||
UTILITY("Utility"),
|
||||
MISCELLANIOUS("Miscellanious");
|
||||
|
|
@ -18,6 +21,11 @@ enum Category {
|
|||
Category(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/** Localized label; the English title is the fallback. */
|
||||
String label() {
|
||||
return ClickGuiI18n.tr("clickgui.settings.category." + name().toLowerCase(), title);
|
||||
}
|
||||
}
|
||||
|
||||
record SettingRow(Setting setting, float anim, float height, float gap) {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ public abstract class BaseHudElement implements JsonConfigObject, ConfigNameProv
|
|||
|
||||
public final String getId() { return id; }
|
||||
public final String getTitle() { return HudElementNamer.translateTitle(title, getTranslationKeyPrefix(), id); }
|
||||
/** Short settings-card description from {@code hud.<id>.description}; empty when none is translated. */
|
||||
public final String getDescription() { return HudElementNamer.translateDescription(id); }
|
||||
public boolean isEnabled() { return enabled.get() || previewEnabled; }
|
||||
public void setRuntimeEnabledTransient(boolean value) { enabled.set(value); }
|
||||
public void setPreviewEnabled(boolean value) { previewEnabled = value; }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ final class HudElementNamer {
|
|||
return fallback == null ? "" : fallback;
|
||||
}
|
||||
|
||||
/** Translated {@code hud.<id>.description}, or an empty string when the key is missing. */
|
||||
static String translateDescription(String id) {
|
||||
if (id == null || id.isBlank()) return "";
|
||||
String key = "hud." + id + ".description";
|
||||
String translated = I18n.get(key);
|
||||
return translated.equals(key) ? "" : translated;
|
||||
}
|
||||
|
||||
private static String stripOwnerPrefix(String value, String owner) {
|
||||
if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value;
|
||||
String normalizedOwner = normalizePrefix(owner);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ public final class ScriptedListHudPanel {
|
|||
public static final Variant KEYBINDS = new Variant(
|
||||
"keybinds", "Keybinds", "M",
|
||||
102.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.95f, "Icons", 1.18f);
|
||||
public static final Variant VOICECHAT = new Variant(
|
||||
"voicechat", "Voice chat", "K",
|
||||
96.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.95f);
|
||||
public static final float HEADER_HEIGHT = 15.5f;
|
||||
public static final float BODY_Y_OFFSET = 18.5f;
|
||||
public static final float CONTENT_START_Y = 25.0f;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package dev.loki.lovisual.features.gui.hud.draggable.impl.voice;
|
||||
|
||||
import dev.loki.lovisual.config.definition.SettingDef;
|
||||
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.gui.hud.config.HudBgStyles;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel;
|
||||
import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Draggable Simple Voice Chat panel: who is talking right now plus the state of your own microphone. */
|
||||
@HudElementRegister(order = 52)
|
||||
public final class VoiceChatHud extends DraggableHudElement {
|
||||
|
||||
{
|
||||
defaultLayout(16.0f, 150.0f);
|
||||
}
|
||||
|
||||
final HudGlobalConfig hud = HudGlobalConfig.get();
|
||||
final ScriptedListHudPanel panel = new ScriptedListHudPanel();
|
||||
final NumberValue<Double> scale = new NumberValue<>("voicechat_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX);
|
||||
final ModeValue layoutMode = new ModeValue("voicechat_layout", "Split Header", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER);
|
||||
final ModeValue bgEffect = new ModeValue("voicechat_bg_effect", HudBgStyles.AUTO, HudBgStyles.NONE, HudBgStyles.BLUR, HudBgStyles.AUTO);
|
||||
final NumberValue<Integer> bgAlpha = new NumberValue<>("voicechat_bg_alpha", 131, 0, 255);
|
||||
final NumberValue<Integer> blurAlpha = new NumberValue<>("voicechat_blur_alpha", 255, 0, 255);
|
||||
final NumberValue<Integer> maxRows = new NumberValue<>("voicechat_max_rows", 6, 1, 12);
|
||||
final NumberValue<Integer> holdMs = new NumberValue<>("voicechat_hold_ms", 350, 100, 1500);
|
||||
final BooleanValue showSelf = new BooleanValue("voicechat_show_self", true);
|
||||
|
||||
float displayWidth = -1.0f;
|
||||
float displayHeight = -1.0f;
|
||||
float visibilityAnim = 0.0f;
|
||||
|
||||
public VoiceChatHud() {
|
||||
super("voicechat", "VoiceChat", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void defineSettings(List<SettingDef> defs) {
|
||||
defs.add(SettingDef.number(scale));
|
||||
defs.add(SettingDef.mode(layoutMode));
|
||||
defs.add(SettingDef.number(maxRows));
|
||||
defs.add(SettingDef.number(holdMs));
|
||||
defs.add(SettingDef.bool(showSelf));
|
||||
defs.add(SettingDef.number(bgAlpha));
|
||||
defs.add(SettingDef.mode(bgEffect));
|
||||
defs.add(SettingDef.number(blurAlpha).visibleWhen(() -> HudBgStyles.hasEffect(bgEffect.get())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyDefaultPosition(int screenW, int screenH) {
|
||||
this.x = 16.0f;
|
||||
this.y = 150.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usesEngineRenderer() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx,
|
||||
float tickDelta, int screenW, int screenH) {
|
||||
VoiceChatHudRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH);
|
||||
}
|
||||
|
||||
void setSize(float w, float h) {
|
||||
setBounds(getX(), getY(), w, h);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package dev.loki.lovisual.features.gui.hud.draggable.impl.voice;
|
||||
|
||||
import dev.loki.lovisual.compat.voicechat.VoiceChatBridge;
|
||||
import dev.loki.lovisual.compat.voicechat.VoiceState;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudBgStyles;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Palette;
|
||||
import dev.loki.lovisual.render.engine.math.HudScale;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.font.Fonts;
|
||||
import dev.loki.lovisual.render.engine.text.font.info.FontInfo;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import dev.loki.lovisual.util.player.health.PlayerSkinResolver;
|
||||
import dev.loki.lovisual.util.screen.ClientScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.ChatScreen;
|
||||
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||
|
||||
/** Builds the rows of {@link VoiceChatHud} and feeds them to the shared scripted list panel. */
|
||||
final class VoiceChatHudRenderer {
|
||||
private static final int TALKING = 0xFF55FF77;
|
||||
private static final int WHISPER = 0xFFFFD255;
|
||||
private static final int SILENT = 0xFFFF5555;
|
||||
private static final float WIDTH_PAD = 30.0f;
|
||||
|
||||
private VoiceChatHudRenderer() {
|
||||
}
|
||||
|
||||
private record Row(String key, Identifier skin, String name, String status, int statusColor, float alpha) { }
|
||||
|
||||
static void render(VoiceChatHud host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx,
|
||||
float tickDelta, int screenW, int screenH) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
boolean preview = DraggableHudElementRegistry.isForceVisible() || ClientScreen.current() instanceof ChatScreen;
|
||||
if (mc == null || (mc.player == null && !preview)) { host.setSize(0f, 0f); return; }
|
||||
List<Row> rows = collectRows(host, mc, preview);
|
||||
host.visibilityAnim = HudRenderUtil.animateVisibility(host.visibilityAnim, !rows.isEmpty());
|
||||
if (rows.isEmpty() && host.visibilityAnim <= 0.0f) { host.setSize(0f, 0f); return; }
|
||||
|
||||
TextRenderer header = font("Inter", FontInfo.Type.Bold, textRenderer);
|
||||
TextRenderer body = font("OnestMedium", FontInfo.Type.Regular, textRenderer);
|
||||
TextRenderer icons = font("IconsNur", FontInfo.Type.Regular, textRenderer);
|
||||
float base = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(host.scale);
|
||||
float fontScale = 0.98f * (host.hud.getFontSize() / 18.0f);
|
||||
float iconH = height(icons, fontScale);
|
||||
float titleH = height(header, fontScale);
|
||||
float rowH = height(body, fontScale);
|
||||
float titleW = width(header, I18n.get("hud.voicechat.title"), fontScale);
|
||||
float labelW = width(body, "Active:", fontScale * 0.92f);
|
||||
float countW = width(body, Integer.toString(rows.size()), fontScale * 0.92f);
|
||||
float maxW = Math.max(ScriptedListHudPanel.VOICECHAT.minWidth() * base,
|
||||
(ScriptedListHudPanel.VOICECHAT.titleTextX() * base) + titleW + labelW + countW + 34.0f * base);
|
||||
for (Row r : rows) {
|
||||
maxW = Math.max(maxW, width(body, r.name(), fontScale) + width(body, r.status(), fontScale) + WIDTH_PAD * base);
|
||||
}
|
||||
float targetH = ScriptedListHudPanel.CONTENT_START_Y * base + rows.size() * ScriptedListHudPanel.ROW_STEP * base;
|
||||
host.displayWidth = HudRenderUtil.animateDimension(host.displayWidth, maxW);
|
||||
host.displayHeight = HudRenderUtil.animateDimension(host.displayHeight, rows.isEmpty() ? 0f : targetH);
|
||||
host.setSize(host.displayWidth, host.displayHeight);
|
||||
|
||||
float s = Math.max(0.0f, HudRenderUtil.visibilityScale(host.visibilityAnim));
|
||||
float w = host.displayWidth * s;
|
||||
float h = host.displayHeight * s;
|
||||
if (w <= 0.0f || h <= 0.0f) return;
|
||||
Palette palette = palette(host.bgAlpha.get());
|
||||
boolean blur = HudBgStyles.hasEffect(host.bgEffect.get());
|
||||
host.panel.render(renderer, textRenderer, ctx, tickDelta, new ScriptedListHudPanel.Panel(
|
||||
ScriptedListHudPanel.VOICECHAT, palette,
|
||||
host.getX() + (host.displayWidth - w) * 0.5f, host.getY() + (host.displayHeight - h) * 0.5f, w, h,
|
||||
s, base * s, fontScale * s, iconH * s, titleH * s, rowH * s, labelW * s, countW * s, rows.size(),
|
||||
blur, Math.min(1.0f, host.blurAlpha.get() / 255.0f * 1.15f), palette.counter(),
|
||||
HudPanelLayoutModes.current(host.layoutMode), toPanelRows(rows, palette)));
|
||||
}
|
||||
|
||||
private static List<Row> collectRows(VoiceChatHud host, Minecraft mc, boolean preview) {
|
||||
if (!VoiceChatBridge.isInstalled()) {
|
||||
return List.of(new Row("missing", null, I18n.get("hud.voicechat.not_found"), "", SILENT, 1.0f));
|
||||
}
|
||||
VoiceState state = VoiceChatBridge.state();
|
||||
long hold = host.holdMs.get();
|
||||
List<Row> rows = new java.util.ArrayList<>();
|
||||
if (host.showSelf.get() && mc.player != null) {
|
||||
VoiceState.MicStatus mic = state.micStatus(hold);
|
||||
rows.add(new Row("self", PlayerSkinResolver.resolveProfileSkin(mc.player.getGameProfile()),
|
||||
mc.player.getName().getString(), I18n.get("hud.voicechat.status." + mic.name().toLowerCase()),
|
||||
statusColor(mic), 1.0f));
|
||||
}
|
||||
for (VoiceState.Speaker sp : state.speakers(hold, host.maxRows.get())) {
|
||||
PlayerInfo info = mc.getConnection() != null ? mc.getConnection().getPlayerInfo(sp.id()) : null;
|
||||
String name = info != null && info.getProfile() != null ? info.getProfile().name() : shortId(sp.id());
|
||||
Identifier skin = info != null ? PlayerSkinResolver.resolveProfileSkin(info.getProfile()) : null;
|
||||
rows.add(new Row(sp.id().toString(), skin, name, sp.whisper() ? I18n.get("hud.voicechat.whisper") : "",
|
||||
sp.whisper() ? WHISPER : TALKING, state.level(sp.id(), hold) * 0.6f + 0.4f));
|
||||
}
|
||||
if (rows.isEmpty() && preview && mc.player != null) {
|
||||
rows.add(new Row("example", PlayerSkinResolver.resolveProfileSkin(mc.player.getGameProfile()),
|
||||
"Example Player", "", TALKING, 1.0f));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<LinkedHashMap<String, Object>> toPanelRows(List<Row> rows, Palette p) {
|
||||
List<LinkedHashMap<String, Object>> out = ScriptedListHudPanel.rows();
|
||||
for (Row r : rows) {
|
||||
int alpha = Math.round(255.0f * r.alpha());
|
||||
out.add(ScriptedListHudPanel.row(r.key(), r.skin() != null ? "head" : "", ScriptedListHudPanel.idString(r.skin()),
|
||||
List.of(ScriptedListHudPanel.textPart(r.name(), withAlpha(p.text(), alpha), 0.0f)),
|
||||
r.status(), withAlpha(r.statusColor(), alpha), withAlpha(p.muted(), Math.max(26, alpha - 120)), r.alpha()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int statusColor(VoiceState.MicStatus mic) {
|
||||
return switch (mic) {
|
||||
case TALKING -> TALKING;
|
||||
case WHISPERING -> WHISPER;
|
||||
case MUTED, DISABLED, DISCONNECTED -> SILENT;
|
||||
default -> theme().textMuted();
|
||||
};
|
||||
}
|
||||
|
||||
/** Theme-driven palette, the same recipe the other list panels use in theme mode. */
|
||||
private static Palette palette(int alpha) {
|
||||
int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha);
|
||||
int header = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), Math.min(255, alpha + 14));
|
||||
int surface = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), Math.min(255, alpha + 6));
|
||||
int deep = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), Math.min(255, alpha + 18));
|
||||
int headerRight = HudRenderUtil.mixColor(surface, header, 0.52f);
|
||||
int bodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f);
|
||||
int muted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f);
|
||||
return new Palette(HudRenderUtil.mixColor(header, window, 0.22f), headerRight,
|
||||
HudRenderUtil.mixColor(window, surface, 0.24f), bodyRight,
|
||||
HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), Math.min(255, Math.max(182, alpha + 36))),
|
||||
theme().textPrimary(), muted, HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f),
|
||||
HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f), HudRenderUtil.scaleAlpha(muted, 0.68f),
|
||||
HudRenderUtil.mixColor(HudRenderUtil.mixColor(headerRight, bodyRight, 0.5f), theme().accent(), 0.14f));
|
||||
}
|
||||
|
||||
private static int withAlpha(int rgb, int alpha) {
|
||||
return (Math.max(0, Math.min(255, alpha)) << 24) | (rgb & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
private static String shortId(UUID id) {
|
||||
return id.toString().substring(0, 8);
|
||||
}
|
||||
|
||||
private static TextRenderer font(String family, FontInfo.Type type, TextRenderer fallback) {
|
||||
TextRenderer r = Fonts.renderer(family, type, TextRenderer.get());
|
||||
return r != null ? r : fallback;
|
||||
}
|
||||
|
||||
private static float height(TextRenderer r, float size) {
|
||||
r.begin(size, true, false);
|
||||
float v = (float) r.getHeight(false);
|
||||
r.end();
|
||||
return v;
|
||||
}
|
||||
|
||||
private static float width(TextRenderer r, String text, float size) {
|
||||
r.begin(size, true, false);
|
||||
float v = (float) r.getWidth(text, false);
|
||||
r.end();
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ public final class ButtonQueue {
|
|||
public static void captureTooltip(AbstractWidget widget, int mouseX, int mouseY) {
|
||||
if (widget == null) return;
|
||||
BetterTooltips tooltips = BetterTooltips.get();
|
||||
if (tooltips == null || !tooltips.useCustomGuiTooltips()) {
|
||||
if (tooltips == null || !tooltips.useButtonTooltips()) {
|
||||
return;
|
||||
}
|
||||
WidgetTooltipHolder state = ((ClickableWidgetAccessor) widget).lovisual$getTooltipState();
|
||||
|
|
|
|||
|
|
@ -128,10 +128,14 @@ public final class BetterTooltips extends AbstractHudElement {
|
|||
for (ConfigValue<?> v : vs) { SettingDef d = settingDef(v); if (d != null) defs.add(d); }
|
||||
}
|
||||
|
||||
public boolean useCustomGuiTooltips() { BetterButtons b = BetterButtons.get(); if (b != null && b.useUiButtons()) return true; return isEnabled(); }
|
||||
public boolean useCustomItemTooltips() { BetterButtons b = BetterButtons.get(); if (b != null && b.useUiButtons()) return true; return itemTooltipEnabled.get(); }
|
||||
public boolean isItemInfoColorizeEnabled() { return itemInfoColorize.get(); }
|
||||
public boolean isTooltipAlphaEnabled() { return anyTooltipsEnabled(); }
|
||||
/** Every custom tooltip feature is gated by the module toggle: when it is off, tooltips are fully vanilla. */
|
||||
public boolean useCustomGuiTooltips() { return isEnabled(); }
|
||||
public boolean useCustomItemTooltips() { return isEnabled() && itemTooltipEnabled.get(); }
|
||||
/** BetterButtons draws its own button hints through this panel even while the module is off. */
|
||||
public boolean useButtonTooltips() { BetterButtons b = BetterButtons.get(); return (b != null && b.useUiButtons()) || isEnabled(); }
|
||||
public boolean isItemInfoColorizeEnabled() { return isEnabled() && itemInfoColorize.get(); }
|
||||
public boolean isTooltipAlphaEnabled() { return isEnabled(); }
|
||||
public boolean isShulkerPreviewActive() { return isEnabled() && shulkerPreviewEnabled.get(); }
|
||||
public int tooltipColor() { return (TOOLTIP_BG_ALPHA << 24) | 0x00101114; }
|
||||
public boolean isShulkerPreviewEnabled() { return shulkerPreviewEnabled.get(); }
|
||||
public void setShulkerPreviewEnabled(boolean v) { shulkerPreviewEnabled.set(v); saveConfig(); }
|
||||
|
|
@ -154,7 +158,6 @@ public final class BetterTooltips extends AbstractHudElement {
|
|||
public float shulkerPreviewSlotGap() { return SHULKER_SLOT_GAP; }
|
||||
public float shulkerPreviewPadding() { return SHULKER_PADDING; }
|
||||
|
||||
private boolean anyTooltipsEnabled() { if (useCustomGuiTooltips()) return true; if (itemTooltipEnabled.get()) return true; return shulkerPreviewEnabled.get(); }
|
||||
private String bindingName(KeyBindValue v) { return name() + ":" + v.getName(); }
|
||||
private void registerBind(KeyBindValue v) {
|
||||
String n = bindingName(v); KeyManager.unregisterAll(n);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ final class ShulkerPreviewCache {
|
|||
|
||||
static PreviewLayout buildLayout() {
|
||||
BetterTooltips self = BetterTooltips.get();
|
||||
if (self == null || !self.isShulkerPreviewEnabled() || !self.isShulkerPreviewHeld()) return null;
|
||||
if (self == null || !self.isShulkerPreviewActive() || !self.isShulkerPreviewHeld()) return null;
|
||||
ItemStack toolStack = TooltipState.getStack();
|
||||
GuiGraphicsExtractor ctx = TooltipState.getCtx();
|
||||
if (toolStack == null || toolStack.isEmpty() || !isShulkerBox(toolStack) || ctx == null) return null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips;
|
||||
|
||||
/** Keeps vanilla tooltip text readable on the themed panel. */
|
||||
final class TooltipColors {
|
||||
private static final int VANILLA_BLUE = 0x5555FF;
|
||||
private static final int VANILLA_DARK_BLUE = 0x0000AA;
|
||||
|
||||
private TooltipColors() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Vanilla paints creative-tab lines pure blue, which is hard to read on a dark themed panel;
|
||||
* those two shades are replaced by the theme accent, every other color is kept.
|
||||
*/
|
||||
static int readable(int argb, int accentArgb) {
|
||||
int rgb = argb & 0x00FFFFFF;
|
||||
if (rgb == VANILLA_BLUE || rgb == VANILLA_DARK_BLUE) {
|
||||
return 0xFF000000 | (accentArgb & 0x00FFFFFF);
|
||||
}
|
||||
return argb;
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +111,7 @@ final class TooltipState {
|
|||
static List<TooltipLine> convertOrdered(List<? extends FormattedCharSequence> src) {
|
||||
List<TooltipLine> out = new ArrayList<>();
|
||||
int fallback = Theme.theme().textPrimary();
|
||||
int accent = Theme.theme().accent();
|
||||
for (FormattedCharSequence line : src) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int[] color = new int[]{0};
|
||||
|
|
@ -122,7 +123,7 @@ final class TooltipState {
|
|||
return true;
|
||||
});
|
||||
if (sb.length() == 0) sb.append(' ');
|
||||
int c = color[0] != 0 ? color[0] : fallback;
|
||||
int c = color[0] != 0 ? TooltipColors.readable(color[0], accent) : fallback;
|
||||
out.add(new TooltipLine(sb.toString(), c));
|
||||
}
|
||||
return out;
|
||||
|
|
@ -149,7 +150,7 @@ final class TooltipState {
|
|||
private static boolean shouldShowHint(boolean isShulker) {
|
||||
if (!isShulker) return false;
|
||||
BetterTooltips b = BetterTooltips.get();
|
||||
if (b == null || !b.isShulkerPreviewEnabled()) return false;
|
||||
if (b == null || !b.isShulkerPreviewActive()) return false;
|
||||
if (!ShulkerPreviewCache.hasShulkerContents(stack)) return false;
|
||||
return !b.isShulkerPreviewHeld();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu;
|
||||
|
||||
import dev.loki.lovisual.config.i18n.MainConfig;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.LoVisualHeroMenuScreen;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import dev.loki.lovisual.util.screen.ClientScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
|
||||
/** Picks the main menu implementation from {@code menuLayout} and owns the vanilla fallback. */
|
||||
public final class MainMenus {
|
||||
private MainMenus() {
|
||||
}
|
||||
|
||||
public static Screen create() {
|
||||
MainConfig cfg = MainConfig.get();
|
||||
boolean hero = cfg != null && "hero".equalsIgnoreCase(cfg.getMenuLayout());
|
||||
return hero ? new LoVisualHeroMenuScreen() : new LoVisualMainMenuScreen();
|
||||
}
|
||||
|
||||
public static boolean isMainMenu(Screen screen) {
|
||||
return screen instanceof LoVisualMainMenuScreen || screen instanceof LoVisualHeroMenuScreen;
|
||||
}
|
||||
|
||||
public static boolean shouldUseVanillaTitleScreen() {
|
||||
return LoVisualMainMenuScreen.shouldUseVanillaTitleScreen();
|
||||
}
|
||||
|
||||
/** Shows the vanilla title screen; a non-null error also disables the custom menu for this session. */
|
||||
public static void fallbackToVanilla(Throwable error) {
|
||||
if (error != null) {
|
||||
LoVisualMainMenuScreen.forceVanillaTitleScreen = true;
|
||||
DebugLog.errorOnce("main_menu_vanilla_fallback", "Main menu failed, switching to vanilla title screen", error);
|
||||
}
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc != null && !(ClientScreen.current() instanceof TitleScreen)) {
|
||||
ClientScreen.show(mc, new TitleScreen(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero;
|
||||
|
||||
import dev.loki.lovisual.features.account.AccountConfig;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.LoVisualAddonManagerScreen;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenuTextUtil;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenus;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.alt.LoVisualAltManagerScreen;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout.Layout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroButtonPainter;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroMenuRenderer;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroPaint;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroWidgetPainter;
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
import dev.loki.lovisual.features.theme.Themes;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.math.HudScale;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.util.screen.ClientScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
|
||||
import net.minecraft.client.gui.screens.options.OptionsScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.Util;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
import static dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroPaint.easeOutCubic;
|
||||
|
||||
/**
|
||||
* "Hero" main-menu layout: greeting and big title on the left, account card and clock widget on
|
||||
* the top corners. Reuses the shared shader background, the alt manager and the global themes.
|
||||
*/
|
||||
public final class LoVisualHeroMenuScreen extends Screen {
|
||||
private static final long APPEAR_MS = 700L;
|
||||
private static final int THEME_DOTS = 6;
|
||||
|
||||
private final float[] hover = new float[HeroLayout.BUTTON_COUNT];
|
||||
private float accountHover;
|
||||
private int hoveredDot = -1;
|
||||
private int hoveredButton = -1;
|
||||
private boolean hoveredAccount;
|
||||
private boolean initialized;
|
||||
private long openTime;
|
||||
private long lastTime;
|
||||
private Layout layout;
|
||||
private float appear;
|
||||
|
||||
public LoVisualHeroMenuScreen() {
|
||||
super(Component.literal("LoVisual"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta) {
|
||||
Minecraft mc = minecraft;
|
||||
if (mc == null) return;
|
||||
if (MainMenus.shouldUseVanillaTitleScreen() || !HeroMenuRenderer.background(mc)) {
|
||||
MainMenus.fallbackToVanilla(null);
|
||||
return;
|
||||
}
|
||||
long now = Util.getMillis();
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
openTime = now;
|
||||
lastTime = now;
|
||||
}
|
||||
float dt = Mth.clamp((now - lastTime) / 1000f, 0f, 0.1f);
|
||||
lastTime = now;
|
||||
appear = easeOutCubic(Mth.clamp((now - openTime) / (float) APPEAR_MS, 0f, 1f));
|
||||
try {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
updateLayout(mc);
|
||||
updateHover(toFixed(mouseX, mc), toFixed(mouseY, mc), dt);
|
||||
Renderer2D.COLOR.begin();
|
||||
draw(ctx, mc);
|
||||
Renderer2D.COLOR.render();
|
||||
} catch (Throwable t) {
|
||||
MainMenus.fallbackToVanilla(t);
|
||||
} finally {
|
||||
ViewportContext.end(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLayout(Minecraft mc) {
|
||||
int fbw = mc.getWindow().getWidth();
|
||||
int fbh = mc.getWindow().getHeight();
|
||||
float rise = (1f - appear) * 18f * HeroLayout.scale(MainMenuTextUtil.virtualHeight(fbw, fbh));
|
||||
layout = HeroLayout.compute(MainMenuTextUtil.virtualWidth(fbw, fbh), MainMenuTextUtil.virtualHeight(fbw, fbh),
|
||||
dots().size(), rise, HeroPaint.metrics());
|
||||
}
|
||||
|
||||
private void updateHover(float mx, float my, float dt) {
|
||||
boolean live = appear > 0.8f;
|
||||
hoveredButton = live ? HeroLayout.buttonAt(layout, mx, my) : -1;
|
||||
hoveredDot = live ? HeroLayout.dotAt(layout, dots().size(), mx, my) : -1;
|
||||
hoveredAccount = live && layout.account().contains(mx, my);
|
||||
float step = Math.min(1f, dt * 12f);
|
||||
for (int i = 0; i < hover.length; i++) {
|
||||
hover[i] = Mth.lerp(step, hover[i], hoveredButton == i ? 1f : 0f);
|
||||
}
|
||||
accountHover = Mth.lerp(step, accountHover, hoveredAccount ? 1f : 0f);
|
||||
}
|
||||
|
||||
private void draw(GuiGraphicsExtractor ctx, Minecraft mc) {
|
||||
float w = MainMenuTextUtil.virtualWidth(mc.getWindow().getWidth(), mc.getWindow().getHeight());
|
||||
float h = MainMenuTextUtil.virtualHeight(mc.getWindow().getWidth(), mc.getWindow().getHeight());
|
||||
String name = accountName(mc);
|
||||
boolean needsAuth = AccountConfig.get().getMicrosoftAuthorizationRequiredAccount() != null;
|
||||
HeroMenuRenderer.vignette(w, h, appear);
|
||||
HeroMenuRenderer.account(ctx, layout, appear, accountHover, name, needsAuth);
|
||||
HeroWidgetPainter.draw(layout, appear, dots(), Theme.currentId(), hoveredDot, LocalTime.now());
|
||||
HeroMenuRenderer.hero(layout, appear, name);
|
||||
for (int i = 0; i < HeroLayout.BUTTON_COUNT; i++) {
|
||||
HeroButtonPainter.draw(layout, i, appear, hover[i], needsAuth && i == HeroLayout.ACCOUNTS);
|
||||
}
|
||||
HeroMenuRenderer.footer(layout, appear, mc.getLaunchedVersion());
|
||||
}
|
||||
|
||||
private static List<Themes.ThemeEntry> dots() {
|
||||
List<Themes.ThemeEntry> presets = Themes.presetEntries();
|
||||
return presets.subList(0, Math.min(THEME_DOTS, presets.size()));
|
||||
}
|
||||
|
||||
private static String accountName(Minecraft mc) {
|
||||
String active = AccountConfig.get().getActiveAccountName();
|
||||
if (active != null && !active.isBlank()) return active;
|
||||
return mc.getUser() != null ? mc.getUser().getName() : "LoVisual";
|
||||
}
|
||||
|
||||
private static float toFixed(float screen, Minecraft mc) {
|
||||
float scale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight());
|
||||
return scale <= 0f ? screen : screen * mc.getWindow().getGuiScale() / scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
|
||||
Minecraft mc = minecraft;
|
||||
if (click.button() != 0 || mc == null || layout == null || appear <= 0.8f) return true;
|
||||
float mx = toFixed((float) click.x(), mc);
|
||||
float my = toFixed((float) click.y(), mc);
|
||||
int dot = HeroLayout.dotAt(layout, dots().size(), mx, my);
|
||||
if (dot >= 0) {
|
||||
Theme.setTheme(dots().get(dot).id());
|
||||
} else if (layout.account().contains(mx, my)) {
|
||||
open(mc, HeroLayout.ACCOUNTS);
|
||||
} else {
|
||||
int button = HeroLayout.buttonAt(layout, mx, my);
|
||||
if (button >= 0) open(mc, button);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void open(Minecraft mc, int button) {
|
||||
switch (button) {
|
||||
case HeroLayout.MULTIPLAYER -> ClientScreen.show(mc, new JoinMultiplayerScreen(this));
|
||||
case HeroLayout.SINGLEPLAYER -> ClientScreen.show(mc, new SelectWorldScreen(this));
|
||||
case HeroLayout.ACCOUNTS -> ClientScreen.show(mc, new LoVisualAltManagerScreen(this,
|
||||
AccountConfig.get().getMicrosoftAuthorizationRequiredAccount() != null));
|
||||
case HeroLayout.ADDONS -> ClientScreen.show(mc, new LoVisualAddonManagerScreen(this));
|
||||
case HeroLayout.OPTIONS -> ClientScreen.show(mc, new OptionsScreen(this, mc.options, false));
|
||||
case HeroLayout.QUIT -> mc.stop();
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(double mouseX, double mouseY) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent input) {
|
||||
return input.key() == 256 || super.keyPressed(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldCloseOnEsc() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.logic;
|
||||
|
||||
/** Time-of-day helpers for the hero menu clock widget. */
|
||||
public final class DayCycle {
|
||||
public static final int MORNING = 0;
|
||||
public static final int DAY = 1;
|
||||
public static final int EVENING = 2;
|
||||
public static final int NIGHT = 3;
|
||||
|
||||
private DayCycle() {
|
||||
}
|
||||
|
||||
public static int period(int hour) {
|
||||
if (hour >= 5 && hour < 12) return MORNING;
|
||||
if (hour >= 12 && hour < 17) return DAY;
|
||||
if (hour >= 17 && hour < 22) return EVENING;
|
||||
return NIGHT;
|
||||
}
|
||||
|
||||
/** Fraction of the day elapsed, in [0, 1). */
|
||||
public static float progress(int hour, int minute) {
|
||||
return (hour * 60 + minute) / 1440.0f;
|
||||
}
|
||||
|
||||
/** Centre x of the bar thumb; the thumb never leaves the rounded bar ends. */
|
||||
public static float thumbX(float barX, float barW, float barH, float progress) {
|
||||
float r = barH * 0.5f;
|
||||
float p = Math.max(0f, Math.min(1f, progress));
|
||||
return barX + r + p * (barW - r * 2f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.logic;
|
||||
|
||||
/**
|
||||
* Geometry of the hero main menu, computed from the logical viewport and measured text heights,
|
||||
* so lines never overlap whatever the resolution or GUI scale. Free of Minecraft types.
|
||||
*/
|
||||
public final class HeroLayout {
|
||||
public static final int BUTTON_COUNT = 6;
|
||||
public static final int MULTIPLAYER = 0;
|
||||
public static final int SINGLEPLAYER = 1;
|
||||
public static final int ACCOUNTS = 2;
|
||||
public static final int ADDONS = 3;
|
||||
public static final int OPTIONS = 4;
|
||||
public static final int QUIT = 5;
|
||||
private static final float FONT_UNIT_PX = 12f;
|
||||
private static final float MIN_K = 0.45f;
|
||||
|
||||
/** Font sizes in logical pixels. */
|
||||
public record Sizes(float tag, float title, float sub, float label, float small, float footer, float time) {
|
||||
}
|
||||
|
||||
/** @param titleY1 first title line; @param titleY2 second title line */
|
||||
public record Layout(float k, Rect account, float avatarSize, Rect widget, float pad,
|
||||
float dotsStartX, float dotY, float dotSpacing, float dotRadius,
|
||||
Rect bar, Rect[] buttons, Sizes sizes,
|
||||
float heroX, float tagY, float titleY1, float titleY2, float subY,
|
||||
float footerY, float footerLine) {
|
||||
}
|
||||
|
||||
private HeroLayout() {
|
||||
}
|
||||
|
||||
public static float scale(float viewportHeight) {
|
||||
return Math.max(0.7f, viewportHeight / 1080f * 1.25f);
|
||||
}
|
||||
|
||||
/** Converts logical pixels to the text renderer's size units. */
|
||||
public static float font(float px) {
|
||||
return px / FONT_UNIT_PX;
|
||||
}
|
||||
|
||||
/** @param rise downward slide offset while fading in */
|
||||
public static Layout compute(float width, float height, int dotCount, float rise, TextMetrics m) {
|
||||
float k = scale(height);
|
||||
Layout l = build(width, height, dotCount, rise, m, k);
|
||||
for (int i = 0; i < 3 && !fits(l); i++) {
|
||||
k = Math.max(MIN_K, k * 0.92f);
|
||||
l = build(width, height, dotCount, rise, m, k);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
private static boolean fits(Layout l) {
|
||||
Rect last = l.buttons()[QUIT];
|
||||
return last.y() + last.h() + 14f * l.k() <= l.footerY() || l.k() <= MIN_K;
|
||||
}
|
||||
|
||||
private static Layout build(float width, float height, int dotCount, float rise, TextMetrics m, float k) {
|
||||
Sizes s = new Sizes(9.5f * k, 26f * k, 11f * k, 12.5f * k, 11.5f * k, 9f * k, 16f * k);
|
||||
float avatar = 30f * k;
|
||||
float topY = 24f * k + rise * 0.3f;
|
||||
Rect account = new Rect(32f * k, topY, avatar + 140f * k, avatar);
|
||||
|
||||
float pad = 10f * k;
|
||||
float ww = 215f * k;
|
||||
float timeH = m.height(s.time());
|
||||
float barH = 10f * k;
|
||||
float wh = pad + timeH + 12f * k + barH + pad;
|
||||
Rect widget = new Rect(width - ww - 24f * k, topY, ww, wh);
|
||||
float spacing = 13f * k;
|
||||
float dotsStart = widget.x() + ww - pad - dotCount * spacing + 6f * k;
|
||||
float dotY = widget.y() + pad + timeH * 0.5f;
|
||||
Rect bar = new Rect(widget.x() + pad, widget.y() + wh - pad - barH, ww - pad * 2f, barH);
|
||||
|
||||
float footerLine = m.height(s.footer()) + 3f * k;
|
||||
float footerY = height - 26f * k - footerLine * 3f;
|
||||
float tagH = m.height(s.tag());
|
||||
float titleH = m.height(s.title());
|
||||
float subH = m.height(s.sub());
|
||||
float buttonH = Math.max(38f * k, m.height(s.label()) + 22f * k);
|
||||
float gap = 8f * k;
|
||||
float tagGap = 10f * k;
|
||||
float subGap = 14f * k;
|
||||
float buttonsGap = 22f * k;
|
||||
float blockH = tagH + tagGap + titleH * 2f + 2f * k + subGap + subH + buttonsGap + buttonH * 5f + gap * 4f;
|
||||
|
||||
float top = account.y() + avatar + 30f * k;
|
||||
float room = footerY - 14f * k - top;
|
||||
float heroY = top + Math.max(0f, (room - blockH) * 0.42f) + rise;
|
||||
float heroX = width * 0.045f;
|
||||
float titleY1 = heroY + tagH + tagGap;
|
||||
float titleY2 = titleY1 + titleH + 2f * k;
|
||||
float subY = titleY2 + titleH + subGap;
|
||||
Rect[] buttons = buttons(heroX, subY + subH + buttonsGap, k, buttonH, gap);
|
||||
return new Layout(k, account, avatar, widget, pad, dotsStart, dotY, spacing, 4.2f * k, bar, buttons, s,
|
||||
heroX, heroY, titleY1, titleY2, subY, footerY, footerLine);
|
||||
}
|
||||
|
||||
private static Rect[] buttons(float x, float top, float k, float h, float gap) {
|
||||
float w = 290f * k;
|
||||
Rect[] out = new Rect[BUTTON_COUNT];
|
||||
for (int i = 0; i <= ADDONS; i++) {
|
||||
out[i] = new Rect(x, top + i * (h + gap), w, h);
|
||||
}
|
||||
float rowY = top + (ADDONS + 1) * (h + gap);
|
||||
float optionsW = (w - gap) * 0.55f;
|
||||
out[OPTIONS] = new Rect(x, rowY, optionsW, h);
|
||||
out[QUIT] = new Rect(x + optionsW + gap, rowY, w - gap - optionsW, h);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Index of the button under the cursor, or -1. */
|
||||
public static int buttonAt(Layout layout, float mx, float my) {
|
||||
for (int i = 0; i < layout.buttons().length; i++) {
|
||||
if (layout.buttons()[i].contains(mx, my)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Index of the theme dot under the cursor, or -1. */
|
||||
public static int dotAt(Layout layout, int dotCount, float mx, float my) {
|
||||
float r = layout.dotRadius() + 3f * layout.k();
|
||||
for (int i = 0; i < dotCount; i++) {
|
||||
float dx = mx - (layout.dotsStartX() + i * layout.dotSpacing());
|
||||
float dy = my - layout.dotY();
|
||||
if (dx * dx + dy * dy <= r * r) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.logic;
|
||||
|
||||
/** Axis-aligned rectangle in the fixed logical viewport. */
|
||||
public record Rect(float x, float y, float w, float h) {
|
||||
public boolean contains(float px, float py) {
|
||||
return px >= x && px <= x + w && py >= y && py <= y + h;
|
||||
}
|
||||
|
||||
public Rect offset(float dx, float dy) {
|
||||
return new Rect(x + dx, y + dy, w, h);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.logic;
|
||||
|
||||
/** Measures text in logical pixels so the layout can be built from real glyph sizes. */
|
||||
public interface TextMetrics {
|
||||
/** Line height of the bold UI font at {@code px} logical pixels. */
|
||||
float height(float px);
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.paint;
|
||||
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenuTextUtil;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout.Layout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.Rect;
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
||||
import static dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroPaint.*;
|
||||
|
||||
/** Hero menu buttons: accent primary, dark glass secondaries, a red-tinted quit, icon on the left. */
|
||||
public final class HeroButtonPainter {
|
||||
private static final String[] LABEL_KEYS = {"multiplayer", "singleplayer", "accounts", "addons", "options", "quit"};
|
||||
private static final String[] ICONS = {"swords", "tree-deciduous", "user", "package", "columns-3-cog", "cross"};
|
||||
|
||||
private HeroButtonPainter() {
|
||||
}
|
||||
|
||||
public static void draw(Layout l, int index, float a, float hover, boolean badge) {
|
||||
float k = l.k();
|
||||
Rect r = l.buttons()[index];
|
||||
float rad = 9f * k;
|
||||
boolean compact = index >= HeroLayout.OPTIONS;
|
||||
fill(r, rad, k, a, hover, index);
|
||||
|
||||
float iconSize = (compact ? 14f : 17f) * k;
|
||||
float iconX = r.x() + (compact ? 13f : 16f) * k + iconSize * 0.5f;
|
||||
float nudge = hover * 1.2f * k;
|
||||
icon(ICONS[index], iconX, r.y() + r.h() * 0.5f - nudge * 0.2f, iconSize + nudge, argb(0xFFFFFF, a * (0.85f + hover * 0.15f)));
|
||||
|
||||
TextRenderer bold = MainMenuTextUtil.interBold(TextRenderer.get());
|
||||
float px = (compact ? l.sizes().small() : l.sizes().label());
|
||||
float textX = iconX + iconSize * 0.5f + (compact ? 8f : 11f) * k;
|
||||
textCentered(bold, I18n.get("screen.lovisual.hero_menu." + LABEL_KEYS[index]), textX, r.y(), r.h(), px, argb(0xFFFFFF, a));
|
||||
if (badge) {
|
||||
Renderer2D.COLOR.circle(r.x() + r.w() - 14f * k, r.y() + r.h() * 0.5f, 3.6f * k, argb(0xFFB74D, a));
|
||||
}
|
||||
}
|
||||
|
||||
private static void fill(Rect r, float rad, float k, float a, float hover, int index) {
|
||||
int accent = Theme.theme().accent();
|
||||
if (index == HeroLayout.MULTIPLAYER) {
|
||||
Renderer2D.COLOR.roundedRectSoftShadow(r.x(), r.y() + 3f * k, r.w(), r.h(), rad, 18f * k, 0.05f + hover * 0.06f, argb(accent, a * 0.55f));
|
||||
int top = argb(mix(accent, 0xFFFFFF, 0.10f + hover * 0.12f), a);
|
||||
int bottom = argb(mix(accent, 0x000000, 0.10f - hover * 0.04f), a);
|
||||
Renderer2D.COLOR.roundedRectGradientQuad(r.x(), r.y(), r.w(), r.h(), rad, top, top, bottom, bottom);
|
||||
return;
|
||||
}
|
||||
boolean quit = index == HeroLayout.QUIT;
|
||||
int base = quit ? mix(0x1C1E26, 0xB0303C, hover * 0.8f) : mix(0x1C1E26, 0x2E3240, hover);
|
||||
Renderer2D.COLOR.roundedRectSoftShadow(r.x(), r.y() + 2f * k, r.w(), r.h(), rad, 10f * k, 0.03f, argb(0x000000, a * 0.4f));
|
||||
Renderer2D.COLOR.roundedRect(r.x(), r.y(), r.w(), r.h(), rad, argb(base, a * (0.76f + hover * 0.16f)));
|
||||
Renderer2D.COLOR.roundedRectStrokeGradient(r.x(), r.y(), r.w(), r.h(), rad, 1f,
|
||||
argb(0xFFFFFF, a * (0.10f + hover * 0.14f)), argb(0xFFFFFF, a * (0.03f + hover * 0.05f)), 90f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.paint;
|
||||
|
||||
import dev.loki.lovisual.config.i18n.MainConfig;
|
||||
import dev.loki.lovisual.features.account.SkinManager;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenuTextUtil;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout.Layout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.Rect;
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||
import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
||||
import static dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroPaint.*;
|
||||
|
||||
/** Draw pass of the hero menu: background, vignette, account card, hero text, buttons and footer. */
|
||||
public final class HeroMenuRenderer {
|
||||
private HeroMenuRenderer() {
|
||||
}
|
||||
|
||||
/** Draws the shader background; returns false when the custom menu is switched off. */
|
||||
public static boolean background(Minecraft mc) {
|
||||
MainConfig cfg = MainConfig.get();
|
||||
String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off";
|
||||
if (mode == null || mode.equalsIgnoreCase("off")) return false;
|
||||
MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora"));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void vignette(float w, float h, float a) {
|
||||
int dark = argb(0x0A0C10, a * 0.88f);
|
||||
int mid = argb(0x0A0C10, a * 0.45f);
|
||||
int clear = argb(0x0A0C10, 0f);
|
||||
Renderer2D.COLOR.quad(0, 0, w * 0.27f, h, dark, mid, mid, dark);
|
||||
Renderer2D.COLOR.quad(w * 0.27f, 0, w * 0.27f, h, mid, clear, clear, mid);
|
||||
}
|
||||
|
||||
public static void account(GuiGraphicsExtractor ctx, Layout l, float a, float hover, String name, boolean needsAuth) {
|
||||
float k = l.k();
|
||||
Rect r = l.account();
|
||||
int accent = Theme.theme().accent();
|
||||
Renderer2D.COLOR.roundedRect(r.x(), r.y(), l.avatarSize(), l.avatarSize(), 6f * k, argb(0xFFFFFF, a * (0.07f + hover * 0.07f)));
|
||||
PlayerHeadRenderer.drawRounded(ctx, r.x(), r.y(), l.avatarSize(), 6f * k, SkinManager.getSkin(name),
|
||||
new RenderColor(argb(0xFFFFFF, a)), true, new RenderColor(argb(0xFFFFFF, a * 0.14f)), 0.8f, false);
|
||||
TextRenderer bold = MainMenuTextUtil.interBold(TextRenderer.get());
|
||||
TextRenderer medium = MainMenuTextUtil.interMedium(bold);
|
||||
float tx = r.x() + l.avatarSize() + l.sizes().footer();
|
||||
String caption = needsAuth ? I18n.get("screen.lovisual.hero_menu.sign_in") : I18n.get("screen.lovisual.hero_menu.account");
|
||||
float capH = height(medium, 8.5f * k);
|
||||
float nameH = height(bold, 11f * k);
|
||||
float top = r.y() + (l.avatarSize() - capH - nameH - 2f * k) * 0.5f;
|
||||
text(medium, caption, tx, top, 8.5f * k, needsAuth ? argb(0xFFB74D, a) : argb(0xFFFFFF, a * 0.55f));
|
||||
text(bold, name, tx, top + capH + 2f * k, 11f * k, argb(0xFFFFFF, a));
|
||||
if (needsAuth) {
|
||||
Renderer2D.COLOR.circle(r.x() + l.avatarSize() - 2f * k, r.y() + 2f * k, 3.4f * k, argb(0xFFB74D, a));
|
||||
}
|
||||
if (hover > 0.01f) {
|
||||
Renderer2D.COLOR.roundedRectStrokeGradient(r.x() - 4f * k, r.y() - 4f * k, r.w() + 8f * k, r.h() + 8f * k, l.sizes().footer(), 1f,
|
||||
argb(accent, a * hover * 0.5f), argb(accent, a * hover * 0.2f), 90f);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hero(Layout l, float a, String name) {
|
||||
TextRenderer bold = MainMenuTextUtil.interBold(TextRenderer.get());
|
||||
TextRenderer medium = MainMenuTextUtil.interMedium(bold);
|
||||
HeroLayout.Sizes sz = l.sizes();
|
||||
text(bold, I18n.get("screen.lovisual.hero_menu.greeting", name.toUpperCase()), l.heroX(), l.tagY(), sz.tag(), argb(Theme.theme().accent(), a));
|
||||
text(bold, I18n.get("screen.lovisual.hero_menu.title1"), l.heroX(), l.titleY1(), sz.title(), argb(0xFFFFFF, a));
|
||||
text(bold, I18n.get("screen.lovisual.hero_menu.title2"), l.heroX(), l.titleY2(), sz.title(), argb(0xFFFFFF, a));
|
||||
text(medium, I18n.get("screen.lovisual.hero_menu.subtitle"), l.heroX(), l.subY(), sz.sub(), argb(0xFFFFFF, a * 0.65f));
|
||||
}
|
||||
|
||||
public static void footer(Layout l, float a, String version) {
|
||||
TextRenderer medium = MainMenuTextUtil.interMedium(MainMenuTextUtil.interBold(TextRenderer.get()));
|
||||
int dim = argb(0xFFFFFF, a * 0.45f);
|
||||
text(medium, I18n.get("screen.lovisual.hero_menu.disclaimer1"), l.heroX(), l.footerY(), l.sizes().footer(), dim);
|
||||
text(medium, I18n.get("screen.lovisual.hero_menu.disclaimer2"), l.heroX(), l.footerY() + l.footerLine(), l.sizes().footer(), dim);
|
||||
text(medium, I18n.get("screen.lovisual.hero_menu.version", version), l.heroX(), l.footerY() + l.footerLine() * 2f, l.sizes().footer(), argb(0xFFFFFF, a * 0.55f));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.paint;
|
||||
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenuTextUtil;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.TextMetrics;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/** Small colour and text helpers shared by the hero menu painters. */
|
||||
public final class HeroPaint {
|
||||
private HeroPaint() {
|
||||
}
|
||||
|
||||
public static int argb(int rgb, float alpha) {
|
||||
return (rgb & 0x00FFFFFF) | (Mth.clamp(Math.round(alpha * 255f), 0, 255) << 24);
|
||||
}
|
||||
|
||||
public static int mix(int a, int b, float t) {
|
||||
return HudRenderUtil.mixColor(a, b, t);
|
||||
}
|
||||
|
||||
public static float easeOutCubic(float x) {
|
||||
return 1f - (float) Math.pow(1f - Mth.clamp(x, 0f, 1f), 3f);
|
||||
}
|
||||
|
||||
/** Draws text sized in logical pixels with the given weight renderer. */
|
||||
public static void text(TextRenderer r, String text, float x, float y, float px, int argb) {
|
||||
MainMenuTextUtil.drawText(r, text, x, y, HeroLayout.font(px), argb);
|
||||
}
|
||||
|
||||
public static float width(TextRenderer r, String text, float px) {
|
||||
return MainMenuTextUtil.measureWidth(r, text, HeroLayout.font(px));
|
||||
}
|
||||
|
||||
public static float height(TextRenderer r, float px) {
|
||||
return MainMenuTextUtil.measureHeight(r, HeroLayout.font(px));
|
||||
}
|
||||
|
||||
/** Text vertically centred inside a box. */
|
||||
public static void textCentered(TextRenderer r, String text, float x, float y, float h, float px, int argb) {
|
||||
text(r, text, x, y + (h - height(r, px)) * 0.5f, px, argb);
|
||||
}
|
||||
|
||||
/** Real line heights of the bold UI font, so the layout adapts to the actual glyph metrics. */
|
||||
public static TextMetrics metrics() {
|
||||
TextRenderer bold = MainMenuTextUtil.interBold(TextRenderer.get());
|
||||
return px -> height(bold, px);
|
||||
}
|
||||
|
||||
/** Draws a tinted SVG icon centred on {@code (cx, cy)}. */
|
||||
public static void icon(String svg, float cx, float cy, float size, int argb) {
|
||||
Renderer2D.COLOR.svg(svg, cx - size * 0.5f, cy - size * 0.5f, size, size, SvgRenderOptions.overrideColor(argb));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu.hero.paint;
|
||||
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenuTextUtil;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.DayCycle;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.HeroLayout.Layout;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.hero.logic.Rect;
|
||||
import dev.loki.lovisual.features.theme.Themes;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
import static dev.loki.lovisual.features.gui.mainmenu.hero.paint.HeroPaint.*;
|
||||
|
||||
/** Top-right widget: clock, time-of-day label, theme dots and the daylight bar with its thumb. */
|
||||
public final class HeroWidgetPainter {
|
||||
private static final int[] BAR_STOPS = {0x283450, 0xDC8255, 0xEBA08C, 0x373C5F};
|
||||
|
||||
private HeroWidgetPainter() {
|
||||
}
|
||||
|
||||
public static void draw(Layout l, float a, List<Themes.ThemeEntry> dots, String currentThemeId, int hoveredDot, LocalTime now) {
|
||||
float k = l.k();
|
||||
Rect w = l.widget();
|
||||
float rad = 12f * k;
|
||||
Renderer2D.COLOR.roundedRectSoftShadow(w.x(), w.y() + 2f * k, w.w(), w.h(), rad, 14f * k, 0.05f, argb(0x060810, a * 0.5f));
|
||||
Renderer2D.COLOR.roundedRect(w.x(), w.y(), w.w(), w.h(), rad, argb(0x141820, a * 0.66f));
|
||||
Renderer2D.COLOR.roundedRectStrokeGradient(w.x(), w.y(), w.w(), w.h(), rad, 1f, argb(0xFFFFFF, a * 0.12f), argb(0xFFFFFF, a * 0.06f), 90f);
|
||||
|
||||
TextRenderer bold = MainMenuTextUtil.interBold(TextRenderer.get());
|
||||
TextRenderer medium = MainMenuTextUtil.interMedium(bold);
|
||||
String time = String.format("%02d:%02d", now.getHour(), now.getMinute());
|
||||
float cx = w.x() + l.pad() + 2f * k;
|
||||
float cy = w.y() + l.pad();
|
||||
text(bold, time, cx, cy, l.sizes().time(), argb(0xFFFFFF, a));
|
||||
String period = I18n.get("screen.lovisual.hero_menu.period." + DayCycle.period(now.getHour()));
|
||||
float periodY = cy + height(bold, l.sizes().time()) - height(medium, 8.5f * k) - 1.5f * k;
|
||||
text(medium, period, cx + width(bold, time, l.sizes().time()) + 6f * k, periodY, 8.5f * k, argb(0xFFFFFF, a * 0.55f));
|
||||
|
||||
drawDots(l, a, dots, currentThemeId, hoveredDot);
|
||||
drawBar(l, a, DayCycle.progress(now.getHour(), now.getMinute()));
|
||||
}
|
||||
|
||||
private static void drawDots(Layout l, float a, List<Themes.ThemeEntry> dots, String currentId, int hovered) {
|
||||
float r = l.dotRadius();
|
||||
for (int i = 0; i < dots.size(); i++) {
|
||||
Themes.ThemeEntry entry = dots.get(i);
|
||||
float x = l.dotsStartX() + i * l.dotSpacing();
|
||||
int color = entry.theme().accent();
|
||||
boolean selected = entry.id().equals(currentId);
|
||||
if (selected) {
|
||||
Renderer2D.COLOR.circle(x, l.dotY(), r + 2.8f * l.k(), argb(color, a * 0.30f));
|
||||
} else if (i == hovered) {
|
||||
Renderer2D.COLOR.circle(x, l.dotY(), r + 1.8f * l.k(), argb(0xFFFFFF, a * 0.55f));
|
||||
}
|
||||
Renderer2D.COLOR.circle(x, l.dotY(), r, argb(color, a));
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawBar(Layout l, float a, float progress) {
|
||||
Rect b = l.bar();
|
||||
float rad = b.h() * 0.5f;
|
||||
float seg = b.w() / 3f;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Renderer2D.COLOR.quad(b.x() + i * seg, b.y(), seg, b.h(),
|
||||
argb(BAR_STOPS[i], a * 0.9f), argb(BAR_STOPS[i + 1], a * 0.9f),
|
||||
argb(BAR_STOPS[i + 1], a * 0.9f), argb(BAR_STOPS[i], a * 0.9f));
|
||||
}
|
||||
Renderer2D.COLOR.roundedRectStrokeGradient(b.x(), b.y(), b.w(), b.h(), rad, 0.8f * l.k(),
|
||||
argb(0xFFFFFF, a * 0.14f), argb(0xFFFFFF, a * 0.14f), 0f);
|
||||
float tx = DayCycle.thumbX(b.x(), b.w(), b.h(), progress);
|
||||
float ty = b.y() + b.h() * 0.5f;
|
||||
float tr = 4f * l.k();
|
||||
Renderer2D.COLOR.circle(tx, ty, tr * 1.9f, argb(0xFFFFFF, a * 0.25f));
|
||||
Renderer2D.COLOR.circle(tx, ty, tr, argb(0xFFFFFF, a));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package dev.loki.lovisual.features.gui.mainmenu;
|
||||
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
||||
/** Caption pill that fades in under a hovered round main-menu button, so the glyph-only row stays readable. */
|
||||
final class MainMenuHoverLabel {
|
||||
private static final String[] KEYS = {"singleplayer", "multiplayer", "accounts", "addons", "options", "quit"};
|
||||
private static final float TEXT_SIZE = 0.66f;
|
||||
|
||||
private MainMenuHoverLabel() {
|
||||
}
|
||||
|
||||
/** @param buttonX left edge of button {@code index}; @param buttonY its top edge */
|
||||
static void draw(LoVisualMainMenuScreen owner, int index, float buttonX, float buttonY, float opacity) {
|
||||
float hover = owner.buttonHoverProgress[index];
|
||||
if (hover < 0.02f || opacity < 0.02f) return;
|
||||
float s = LoVisualMainMenuScreen.MENU_SCALE;
|
||||
TextRenderer font = MainMenuTextUtil.interMedium(MainMenuTextUtil.interBold(TextRenderer.get()));
|
||||
String text = I18n.get("screen.lovisual.hero_menu." + KEYS[index]);
|
||||
float size = TEXT_SIZE * s;
|
||||
float w = MainMenuTextUtil.measureWidth(font, text, size);
|
||||
float h = MainMenuTextUtil.measureHeight(font, size);
|
||||
float padX = 9f * s;
|
||||
float padY = 4f * s;
|
||||
float cx = buttonX + LoVisualMainMenuScreen.BUTTON_RADIUS;
|
||||
float y = buttonY + LoVisualMainMenuScreen.BUTTON_SIZE + (8f + (1f - hover) * 4f) * s;
|
||||
float alpha = hover * opacity;
|
||||
Renderer2D.COLOR.roundedRect(cx - w * 0.5f - padX, y, w + padX * 2f, h + padY * 2f, (h + padY * 2f) * 0.5f,
|
||||
MainMenuPainter.withAlpha(0x0C0F16, Math.round(alpha * 190f)));
|
||||
MainMenuTextUtil.drawText(font, text, cx - w * 0.5f, y + padY, size, MainMenuPainter.withAlpha(0xFFFFFF, Math.round(alpha * 235f)));
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,9 @@ final class MainMenuRenderer {
|
|||
if (eased <= 0f) continue;
|
||||
renderCircleButton(i, startX + i * (LoVisualMainMenuScreen.BUTTON_SIZE + LoVisualMainMenuScreen.BUTTON_SPACING), y, eased * opacity);
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
MainMenuHoverLabel.draw(owner, i, startX + i * (LoVisualMainMenuScreen.BUTTON_SIZE + LoVisualMainMenuScreen.BUTTON_SPACING), y, opacity);
|
||||
}
|
||||
}
|
||||
|
||||
/** Liquid-glass circle + shadow + glyph for one button; {@code quit} tints red on hover. */
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import dev.loki.lovisual.render.engine.color.RenderColor;
|
|||
* Lives in the same package as {@link LoVisualMainMenuScreen}; kept as a separate
|
||||
* class so the screen file stays within the two-hundred-line cap.
|
||||
*/
|
||||
final class MainMenuTextUtil {
|
||||
public final class MainMenuTextUtil {
|
||||
|
||||
private MainMenuTextUtil() { }
|
||||
|
||||
static float measureWidth(TextRenderer renderer, String text, float size) {
|
||||
public static float measureWidth(TextRenderer renderer, String text, float size) {
|
||||
if (renderer == null || text == null) return 0f;
|
||||
renderer.begin(size, true, false);
|
||||
float width = (float) renderer.getWidth(text, false);
|
||||
|
|
@ -24,7 +24,7 @@ final class MainMenuTextUtil {
|
|||
return width;
|
||||
}
|
||||
|
||||
static float measureHeight(TextRenderer renderer, float size) {
|
||||
public static float measureHeight(TextRenderer renderer, float size) {
|
||||
if (renderer == null) return 0f;
|
||||
renderer.begin(size, true, false);
|
||||
float height = (float) renderer.getHeight(false);
|
||||
|
|
@ -32,7 +32,7 @@ final class MainMenuTextUtil {
|
|||
return height;
|
||||
}
|
||||
|
||||
static void drawText(TextRenderer renderer, String text, float x, float y, float size, int argb) {
|
||||
public static void drawText(TextRenderer renderer, String text, float x, float y, float size, int argb) {
|
||||
if (renderer == null || text == null || text.isEmpty()) return;
|
||||
renderer.setAlpha(1.0);
|
||||
renderer.begin(size, false, false);
|
||||
|
|
@ -40,12 +40,12 @@ final class MainMenuTextUtil {
|
|||
renderer.end();
|
||||
}
|
||||
|
||||
static void drawCenteredText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) {
|
||||
public static void drawCenteredText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) {
|
||||
float x = centerX - measureWidth(renderer, text, size) * 0.5f;
|
||||
drawText(renderer, text, x, y, size, argb);
|
||||
}
|
||||
|
||||
static void drawLiquidGlassText(TextRenderer renderer, String text, float x, float y, float size, int argb) {
|
||||
public static void drawLiquidGlassText(TextRenderer renderer, String text, float x, float y, float size, int argb) {
|
||||
if (renderer == null || text == null || text.isEmpty()) return;
|
||||
renderer.setAlpha(1.0);
|
||||
renderer.begin(size, false, true);
|
||||
|
|
@ -53,13 +53,13 @@ final class MainMenuTextUtil {
|
|||
renderer.end();
|
||||
}
|
||||
|
||||
static void drawCenteredLiquidGlassText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) {
|
||||
public static void drawCenteredLiquidGlassText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) {
|
||||
float x = centerX - measureWidth(renderer, text, size) * 0.5f;
|
||||
drawLiquidGlassText(renderer, text, x, y, size, argb);
|
||||
}
|
||||
|
||||
/** Truncate {@code text} with a "..." suffix until it fits within {@code maxWidth} logical pixels. */
|
||||
static String ellipsize(TextRenderer renderer, String text, float size, float maxWidth) {
|
||||
public static String ellipsize(TextRenderer renderer, String text, float size, float maxWidth) {
|
||||
if (text == null || text.isEmpty()) return "";
|
||||
if (measureWidth(renderer, text, size) <= maxWidth) return text;
|
||||
String out = text;
|
||||
|
|
@ -70,11 +70,11 @@ final class MainMenuTextUtil {
|
|||
}
|
||||
|
||||
/** Resolve the Inter Bold or Inter Medium {@link TextRenderer} for main-menu text, with a fallback chain. */
|
||||
static TextRenderer interBold(TextRenderer fallback) { return Fonts.renderer("Inter", FontInfo.Type.Bold, fallback); }
|
||||
static TextRenderer interMedium(TextRenderer fallback) { return Fonts.renderer("InterMedium", FontInfo.Type.Regular, fallback); }
|
||||
static TextRenderer iconFont(TextRenderer fallback) { return Fonts.renderer("MainMenuIcons", FontInfo.Type.Regular, fallback); }
|
||||
public static TextRenderer interBold(TextRenderer fallback) { return Fonts.renderer("Inter", FontInfo.Type.Bold, fallback); }
|
||||
public static TextRenderer interMedium(TextRenderer fallback) { return Fonts.renderer("InterMedium", FontInfo.Type.Regular, fallback); }
|
||||
public static TextRenderer iconFont(TextRenderer fallback) { return Fonts.renderer("MainMenuIcons", FontInfo.Type.Regular, fallback); }
|
||||
|
||||
/** Recompute the fixed logical viewport from the framebuffer size, matching {@link HudScale}. */
|
||||
static float virtualWidth(int fbw, int fbh) { return Math.max(1f, HudScale.virtualWidth(fbw, fbh)); }
|
||||
static float virtualHeight(int fbw, int fbh) { return Math.max(1f, HudScale.virtualHeight(fbw, fbh)); }
|
||||
public static float virtualWidth(int fbw, int fbh) { return Math.max(1f, HudScale.virtualWidth(fbw, fbh)); }
|
||||
public static float virtualHeight(int fbw, int fbh) { return Math.max(1f, HudScale.virtualHeight(fbw, fbh)); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package dev.loki.lovisual.features.module.modules.combat.prediction;
|
|||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
|
|
@ -12,10 +11,10 @@ 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.features.module.modules.combat.targetesp.TargetEspEffects;
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes;
|
||||
import dev.loki.lovisual.features.relations.category.CategoryService;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.constellation.TargetEspConstellation;
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.state.TargetEspColors;
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.sword.TargetEspSwords;
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.state.TargetEspTracker;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3DCullOptions;
|
||||
|
|
@ -23,6 +22,7 @@ import dev.loki.lovisual.render.engine.renderer.Renderer3DCulling;
|
|||
import dev.loki.lovisual.util.target.TargetManager;
|
||||
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderBlackhole;
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderGhosts;
|
||||
|
|
@ -36,109 +36,92 @@ import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetE
|
|||
@ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.COMBAT)
|
||||
public class TargetESP extends Module {
|
||||
|
||||
private static final String SETTING_MODE = "mode";
|
||||
private static final String SETTING_COLOR = "color";
|
||||
private static final String SETTING_COLOR_MODE = "color_mode";
|
||||
private static final String SETTING_COLOR_SPEED = "color_speed";
|
||||
private static final String SETTING_COLOR_PHASE = "color_phase";
|
||||
private static final String SETTING_COLOR_SPREAD = "color_spread";
|
||||
private static final String SETTING_COLOR2 = "color2";
|
||||
private static final String SETTING_USE_RELATION_COLORS = "use_relation_colors";
|
||||
private static final String SETTING_RELATION_COLOR_MIX = "relation_color_mix";
|
||||
private static final String SETTING_CROSSHAIR_TARGETS = "crosshair_targets";
|
||||
private static final String SETTING_ALPHA_ANIMATION_SPEED = "alpha_animation_speed";
|
||||
private static final String SETTING_ESP_LENGTH = "esp_length";
|
||||
private static final String SETTING_ESP_FACTOR = "esp_factor";
|
||||
private static final String SETTING_ESP_SHAKING = "esp_shaking";
|
||||
private static final String SETTING_ESP_AMPLITUDE = "esp_amplitude";
|
||||
private static final String SETTING_CAPTURE_SPIN_SPEED = "capture_spin_speed";
|
||||
private static final String SETTING_CRYSTAL_HIT_RED = "crystal_hit_red";
|
||||
private static final String SETTING_BLACKHOLE_SIZE = "blackhole_size";
|
||||
private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed";
|
||||
private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style";
|
||||
private static final String SETTING_VORTEX_SPEED = "vortex_speed";
|
||||
private static final float ALPHA_TICK_DT = 1.0f / 20.0f;
|
||||
private static final float ALPHA_SNAP_EPSILON = 0.02f;
|
||||
private final ModeValue espMode = modeSetting(
|
||||
"targetEspMode",
|
||||
SETTING_MODE,
|
||||
"ghosts",
|
||||
"ghosts", "ring", "capture_mark", "crystals", "blackhole", "vortex"
|
||||
);
|
||||
private final RGBAColorValue colorValue = color("targetEspColor", SETTING_COLOR, "#FFFF5555");
|
||||
private final ModeValue colorMode = visibleWhen(modeSetting(
|
||||
"targetEspColorMode",
|
||||
SETTING_COLOR_MODE,
|
||||
"Static",
|
||||
"Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"
|
||||
), () -> true);
|
||||
private final ModeValue espMode = modeSetting("targetEspMode", "mode", "ghosts",
|
||||
"ghosts", "ring", "capture_mark", "crystals", "blackhole", "vortex", "constellation", "swords");
|
||||
private final RGBAColorValue colorValue = color("targetEspColor", "color", "#FFFF5555");
|
||||
private final ModeValue colorMode = visibleWhen(modeSetting("targetEspColorMode", "color_mode", "Static",
|
||||
"Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"), () -> true);
|
||||
private final NumberValue<Integer> colorSpeed =
|
||||
visibleWhen(num("targetEspColorSpeed", SETTING_COLOR_SPEED, 18, 2, 54), () -> true);
|
||||
private final NumberValue<Integer> colorPhase =
|
||||
num("targetEspColorPhase", SETTING_COLOR_PHASE, 0, 0, 360);
|
||||
private final NumberValue<Integer> colorSpread =
|
||||
num("targetEspColorSpread", SETTING_COLOR_SPREAD, 90, -720, 720);
|
||||
visibleWhen(num("targetEspColorSpeed", "color_speed", 18, 2, 54), () -> true);
|
||||
private final NumberValue<Integer> colorPhase = num("targetEspColorPhase", "color_phase", 0, 0, 360);
|
||||
private final NumberValue<Integer> colorSpread = num("targetEspColorSpread", "color_spread", 90, -720, 720);
|
||||
private final RGBAColorValue colorValue2 =
|
||||
visibleWhen(color("targetEspColor2", SETTING_COLOR2, "#FF55FFFF"), this::usesSecondaryColor);
|
||||
private final BooleanValue useRelationColors =
|
||||
boolCommon("targetEspUseRelationColors", SETTING_USE_RELATION_COLORS, CommonSettingSchemas.PLAYER_USE_RELATIONS, false);
|
||||
private final NumberValue<Float> relationColorMix =
|
||||
visibleWhen(num("targetEspRelationColorMix", SETTING_RELATION_COLOR_MIX, 1.0f, 0.0f, 1.0f), useRelationColors::get);
|
||||
private final BooleanValue crosshairTargets =
|
||||
bool("targetEspCrosshairTargets", SETTING_CROSSHAIR_TARGETS, true);
|
||||
visibleWhen(color("targetEspColor2", "color2", "#FF55FFFF"), this::usesSecondaryColor);
|
||||
private final BooleanValue useRelationColors = boolCommon("targetEspUseRelationColors", "use_relation_colors",
|
||||
CommonSettingSchemas.PLAYER_USE_RELATIONS, false);
|
||||
private final NumberValue<Float> relationColorMix = visibleWhen(
|
||||
num("targetEspRelationColorMix", "relation_color_mix", 1.0f, 0.0f, 1.0f), useRelationColors::get);
|
||||
private final BooleanValue crosshairTargets = bool("targetEspCrosshairTargets", "crosshair_targets", true);
|
||||
private final NumberValue<Float> alphaAnimationSpeed =
|
||||
num("targetEspAlphaAnimationSpeed", SETTING_ALPHA_ANIMATION_SPEED, 8.0f, 2.0f, 10.0f);
|
||||
num("targetEspAlphaAnimationSpeed", "alpha_animation_speed", 8.0f, 2.0f, 10.0f);
|
||||
private final NumberValue<Integer> espLength =
|
||||
visibleWhen(num("targetEspLength", SETTING_ESP_LENGTH, 14, 1, 40), this::isGhostMode);
|
||||
visibleWhen(num("targetEspLength", "esp_length", 14, 1, 40), onMode("ghosts"));
|
||||
private final NumberValue<Integer> espFactor =
|
||||
visibleWhen(num("targetEspFactor", SETTING_ESP_FACTOR, 8, 1, 20), this::isGhostMode);
|
||||
visibleWhen(num("targetEspFactor", "esp_factor", 8, 1, 20), onMode("ghosts"));
|
||||
private final NumberValue<Float> espShaking =
|
||||
visibleWhen(num("targetEspShaking", SETTING_ESP_SHAKING, 1.8f, 1.5f, 10f), this::isGhostMode);
|
||||
visibleWhen(num("targetEspShaking", "esp_shaking", 1.8f, 1.5f, 10f), onMode("ghosts"));
|
||||
private final NumberValue<Float> espAmplitude =
|
||||
visibleWhen(num("targetEspAmplitude", SETTING_ESP_AMPLITUDE, 3f, 0.1f, 8f), this::isGhostMode);
|
||||
private final NumberValue<Float> captureSpinSpeed =
|
||||
visibleWhen(num("targetEspCaptureSpinSpeed", SETTING_CAPTURE_SPIN_SPEED, 1.0f, 0.2f, 4.0f), this::isCaptureMarkMode);
|
||||
visibleWhen(num("targetEspAmplitude", "esp_amplitude", 3f, 0.1f, 8f), onMode("ghosts"));
|
||||
private final NumberValue<Float> captureSpinSpeed = visibleWhen(
|
||||
num("targetEspCaptureSpinSpeed", "capture_spin_speed", 1.0f, 0.2f, 4.0f), onMode("capture_mark"));
|
||||
private final BooleanValue crystalHitRed =
|
||||
visibleWhen(bool("targetEspCrystalHitRed", SETTING_CRYSTAL_HIT_RED, true), this::isCrystalsMode);
|
||||
private final NumberValue<Float> blackholeSize =
|
||||
visibleWhen(num("targetEspBlackholeSize", SETTING_BLACKHOLE_SIZE, 1.1f, 0.5f, 3.0f), this::isBlackholeMode);
|
||||
private final NumberValue<Float> blackholeSpinSpeed =
|
||||
visibleWhen(num("targetEspBlackholeSpinSpeed", SETTING_BLACKHOLE_SPIN_SPEED, 1.0f, 0.2f, 4.0f), this::isBlackholeMode);
|
||||
private final ModeValue blackholeStyle =
|
||||
visibleWhen(modeSetting("targetEspBlackholeStyle", SETTING_BLACKHOLE_STYLE, "Single", "Single", "Body"), this::isBlackholeMode);
|
||||
visibleWhen(bool("targetEspCrystalHitRed", "crystal_hit_red", true), onMode("crystals"));
|
||||
private final NumberValue<Float> blackholeSize = visibleWhen(
|
||||
num("targetEspBlackholeSize", "blackhole_size", 1.1f, 0.5f, 3.0f), onMode("blackhole"));
|
||||
private final NumberValue<Float> blackholeSpinSpeed = visibleWhen(
|
||||
num("targetEspBlackholeSpinSpeed", "blackhole_spin_speed", 1.0f, 0.2f, 4.0f), onMode("blackhole"));
|
||||
private final ModeValue blackholeStyle = visibleWhen(
|
||||
modeSetting("targetEspBlackholeStyle", "blackhole_style", "Single", "Single", "Body"), onMode("blackhole"));
|
||||
private final NumberValue<Float> vortexSpeed =
|
||||
visibleWhen(num("targetEspVortexSpeed", SETTING_VORTEX_SPEED, 1.2f, 0.2f, 4.0f), this::isVortexMode);
|
||||
visibleWhen(num("targetEspVortexSpeed", "vortex_speed", 1.2f, 0.2f, 4.0f), onMode("vortex"));
|
||||
private final NumberValue<Integer> constellationStars = visibleWhen(
|
||||
num("targetEspConstellationStars", "constellation_stars", 9, 3, 24), onMode("constellation"));
|
||||
private final NumberValue<Float> constellationSpeed = visibleWhen(
|
||||
num("targetEspConstellationSpeed", "constellation_speed", 1.0f, 0.2f, 4.0f), onMode("constellation"));
|
||||
private final NumberValue<Float> constellationOrbit = visibleWhen(
|
||||
num("targetEspConstellationOrbit", "constellation_orbit", 1.1f, 0.5f, 2.5f), onMode("constellation"));
|
||||
private final BooleanValue constellationLines = visibleWhen(
|
||||
bool("targetEspConstellationLines", "constellation_lines", true), onMode("constellation"));
|
||||
private final NumberValue<Float> constellationPointSize = visibleWhen(
|
||||
num("targetEspConstellationPointSize", "constellation_point_size", 0.12f, 0.05f, 0.4f), onMode("constellation"));
|
||||
private final NumberValue<Float> constellationGlow = visibleWhen(
|
||||
num("targetEspConstellationGlow", "constellation_glow", 1.0f, 0.0f, 2.0f), onMode("constellation"));
|
||||
private final NumberValue<Integer> swordsSpeed = visibleWhen(
|
||||
num("targetEspSwordsSpeed", "swords_speed", 2200, 600, 8000), onMode("swords"));
|
||||
private final NumberValue<Float> swordsRadius = visibleWhen(
|
||||
num("targetEspSwordsRadius", "swords_radius", 1.0f, 0.5f, 2.5f), onMode("swords"));
|
||||
private final NumberValue<Float> swordsSize = visibleWhen(
|
||||
num("targetEspSwordsSize", "swords_size", 1.0f, 0.3f, 2.5f), onMode("swords"));
|
||||
private final NumberValue<Float> swordsBob = visibleWhen(
|
||||
num("targetEspSwordsBob", "swords_bob", 0.06f, 0.0f, 0.4f), onMode("swords"));
|
||||
private final BooleanValue swordsGlow = visibleWhen(bool("targetEspSwordsGlow", "swords_glow", true), onMode("swords"));
|
||||
private final BooleanValue swordsPhysics =
|
||||
visibleWhen(bool("targetEspSwordsPhysics", "swords_physics", true), onMode("swords"));
|
||||
private final TargetEspColors colors = new TargetEspColors(colorMode, colorSpeed, colorPhase, colorSpread,
|
||||
colorValue, colorValue2, useRelationColors, relationColorMix);
|
||||
private final TargetEspTracker tracker = new TargetEspTracker();
|
||||
private final TargetEspCrystalRenderer crystalRenderer = new TargetEspCrystalRenderer();
|
||||
private LivingEntity target;
|
||||
private LivingEntity renderTarget;
|
||||
private float targetAlpha;
|
||||
private final TargetEspSwords swords = new TargetEspSwords();
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
target = null;
|
||||
renderTarget = null;
|
||||
targetAlpha = 0.0f;
|
||||
tracker.reset();
|
||||
crystalRenderer.clear();
|
||||
swords.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!isEnabled() || mc.level == null) {
|
||||
target = null;
|
||||
updateRenderTarget(null);
|
||||
crystalRenderer.clear();
|
||||
return;
|
||||
}
|
||||
target = TargetManager.getTarget(crosshairTargets.get());
|
||||
updateRenderTarget(target);
|
||||
if (renderTarget == null) {
|
||||
LivingEntity target = isEnabled() && mc.level != null ? TargetManager.getTarget(crosshairTargets.get()) : null;
|
||||
if (tracker.update(target, alphaAnimationSpeed.get()) || tracker.renderTarget() == null) {
|
||||
crystalRenderer.clear();
|
||||
swords.reset();
|
||||
}
|
||||
if (!isEnabled() || mc.level == null) return;
|
||||
|
||||
setCaptureSpeedScale(isCaptureMarkMode() ? captureSpinSpeed.get() : 1.0f);
|
||||
|
||||
if (renderTarget != null && targetAlpha > 0.0f) {
|
||||
setCaptureSpeedScale(espMode.get().equals("capture_mark") ? captureSpinSpeed.get() : 1.0f);
|
||||
if (tracker.isVisible()) {
|
||||
tickTargetEsp();
|
||||
}
|
||||
}
|
||||
|
|
@ -150,153 +133,46 @@ public class TargetESP extends Module {
|
|||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
if (!isEnabled() || renderTarget == null || targetAlpha <= 0.001f) return;
|
||||
Renderer3DCullOptions cull = resolveCullOptions(renderTarget);
|
||||
if (!Renderer3DCulling.shouldRender(renderTarget, tickDelta, cull)) return;
|
||||
LivingEntity target = tracker.renderTarget();
|
||||
if (!isEnabled() || !tracker.isVisible()) return;
|
||||
if (!Renderer3DCulling.shouldRender(target, tickDelta, resolveCullOptions(target))) return;
|
||||
|
||||
IntFunction<Integer> palette = index -> AnimatedRenderColors.scaleAlpha(getColorArgb(index), targetAlpha);
|
||||
|
||||
String mode = espMode.get();
|
||||
if ("ring".equals(mode)) {
|
||||
drawOldTargetEsp(renderer, renderTarget, tickDelta, palette);
|
||||
} else if ("ghosts".equals(mode)) {
|
||||
renderGhosts(
|
||||
renderer,
|
||||
espLength.get(),
|
||||
espFactor.get(),
|
||||
espShaking.get(),
|
||||
espAmplitude.get(),
|
||||
renderTarget,
|
||||
tickDelta,
|
||||
palette
|
||||
);
|
||||
} else if ("capture_mark".equals(mode)) {
|
||||
renderCaptureMark(renderer, renderTarget, tickDelta, palette);
|
||||
} else if ("crystals".equals(mode)) {
|
||||
crystalRenderer.render(renderer, renderTarget, tickDelta, crystalHitRed.get(), targetAlpha, this::getColorArgb);
|
||||
} else if ("blackhole".equals(mode)) {
|
||||
renderBlackhole(renderer, renderTarget, tickDelta, palette,
|
||||
blackholeSize.get(), blackholeSpinSpeed.get(), targetAlpha, blackholeStyle.get());
|
||||
} else if ("vortex".equals(mode)) {
|
||||
renderVortex(renderer, renderTarget, tickDelta, palette, vortexSpeed.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRenderTarget(LivingEntity next) {
|
||||
float dt = Math.max(AnimationUtility.deltaTime(), ALPHA_TICK_DT);
|
||||
float speed = alphaAnimationSpeed.get();
|
||||
if (next != null) {
|
||||
if (renderTarget == null) {
|
||||
renderTarget = next;
|
||||
targetAlpha = AnimationUtility.snap(
|
||||
AnimationUtility.approach(targetAlpha, 1.0f, dt, speed),
|
||||
1.0f,
|
||||
ALPHA_SNAP_EPSILON
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (renderTarget != next) {
|
||||
renderTarget = next;
|
||||
targetAlpha = 1.0f;
|
||||
crystalRenderer.clear();
|
||||
return;
|
||||
}
|
||||
targetAlpha = AnimationUtility.snap(
|
||||
AnimationUtility.approach(targetAlpha, 1.0f, dt, speed),
|
||||
1.0f,
|
||||
ALPHA_SNAP_EPSILON
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
targetAlpha = AnimationUtility.snap(
|
||||
AnimationUtility.approach(targetAlpha, 0.0f, dt, speed),
|
||||
0.0f,
|
||||
ALPHA_SNAP_EPSILON
|
||||
);
|
||||
if (targetAlpha <= 0.0f) {
|
||||
renderTarget = null;
|
||||
crystalRenderer.clear();
|
||||
float alpha = tracker.alpha();
|
||||
IntFunction<Integer> palette = index -> AnimatedRenderColors.scaleAlpha(getColorArgb(index), alpha);
|
||||
switch (espMode.get()) {
|
||||
case "ring" -> drawOldTargetEsp(renderer, target, tickDelta, palette);
|
||||
case "ghosts" -> renderGhosts(renderer, espLength.get(), espFactor.get(), espShaking.get(),
|
||||
espAmplitude.get(), target, tickDelta, palette);
|
||||
case "capture_mark" -> renderCaptureMark(renderer, target, tickDelta, palette);
|
||||
case "crystals" -> crystalRenderer.render(renderer, target, tickDelta, crystalHitRed.get(), alpha, this::getColorArgb);
|
||||
case "blackhole" -> renderBlackhole(renderer, target, tickDelta, palette,
|
||||
blackholeSize.get(), blackholeSpinSpeed.get(), alpha, blackholeStyle.get());
|
||||
case "vortex" -> renderVortex(renderer, target, tickDelta, palette, vortexSpeed.get());
|
||||
case "constellation" -> TargetEspConstellation.render(renderer, target, tickDelta, palette,
|
||||
constellationStars.get(), constellationSpeed.get(), constellationOrbit.get(),
|
||||
constellationLines.get(), constellationPointSize.get(), constellationGlow.get());
|
||||
case "swords" -> swords.render(renderer, target, tickDelta, palette, swordsSpeed.get(), swordsRadius.get(),
|
||||
swordsSize.get(), swordsBob.get(), swordsGlow.get(), swordsPhysics.get());
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
|
||||
// Raycast-selected targets may be behind walls: fall back to a look-based frustum check.
|
||||
private Renderer3DCullOptions resolveCullOptions(LivingEntity target) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (target == null || mc.player == null) {
|
||||
return Renderer3DCullOptions.SMART_TARGET;
|
||||
}
|
||||
// Allow rendering through walls if the current target is already selected,
|
||||
// so raycast-based targeting doesn't get culled by line-of-sight checks.
|
||||
if (!mc.player.hasLineOfSight(target)) {
|
||||
return Renderer3DCullOptions.FRUSTUM_LOOK;
|
||||
}
|
||||
return Renderer3DCullOptions.SMART_TARGET;
|
||||
boolean visible = mc.player == null || mc.player.hasLineOfSight(target);
|
||||
return visible ? Renderer3DCullOptions.SMART_TARGET : Renderer3DCullOptions.FRUSTUM_LOOK;
|
||||
}
|
||||
|
||||
private boolean isGhostMode() {
|
||||
return "ghosts".equals(espMode.get());
|
||||
}
|
||||
|
||||
private boolean isCaptureMarkMode() {
|
||||
return "capture_mark".equals(espMode.get());
|
||||
}
|
||||
|
||||
private boolean isCrystalsMode() {
|
||||
return "crystals".equals(espMode.get());
|
||||
}
|
||||
|
||||
private boolean isBlackholeMode() {
|
||||
return "blackhole".equals(espMode.get());
|
||||
}
|
||||
|
||||
private boolean isVortexMode() {
|
||||
return "vortex".equals(espMode.get());
|
||||
private Supplier<Boolean> onMode(String mode) {
|
||||
return () -> mode.equals(espMode.get());
|
||||
}
|
||||
|
||||
private boolean usesSecondaryColor() {
|
||||
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
||||
return colors.usesSecondary();
|
||||
}
|
||||
|
||||
private int getColorArgb(int count) {
|
||||
int phase = colorPhase.get() + Math.round(count * colorSpread.get() / 90.0f);
|
||||
int animated = AnimatedRenderColors.resolve(
|
||||
animatedColorMode(),
|
||||
colorSpeed.get(),
|
||||
phase,
|
||||
colorValue.getArgb(),
|
||||
colorValue2.getArgb(),
|
||||
true
|
||||
);
|
||||
if (!useRelationColors.get() || renderTarget == null) {
|
||||
return animated;
|
||||
}
|
||||
|
||||
int relation = relationColor(renderTarget, animated);
|
||||
relation = AnimatedRenderColors.withAlpha(relation, (animated >>> 24) & 0xFF);
|
||||
return AnimatedRenderColors.mixArgb(animated, relation, relationColorMix.get());
|
||||
}
|
||||
|
||||
private int relationColor(LivingEntity entity, int fallback) {
|
||||
if (entity instanceof Player player) {
|
||||
return CategoryService.getColor(player);
|
||||
}
|
||||
String name = entity.getName() != null ? entity.getName().getString() : null;
|
||||
if (name == null || name.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return CategoryService.getColor(name);
|
||||
}
|
||||
|
||||
private AnimatedRenderColors.Mode animatedColorMode() {
|
||||
return switch (colorMode.get()) {
|
||||
case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW;
|
||||
case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
|
||||
case "Sky" -> AnimatedRenderColors.Mode.SKY;
|
||||
case "Fade" -> AnimatedRenderColors.Mode.FADE;
|
||||
case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
|
||||
case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS;
|
||||
case "Theme" -> AnimatedRenderColors.Mode.THEME;
|
||||
default -> AnimatedRenderColors.Mode.STATIC;
|
||||
};
|
||||
return colors.argb(count, tracker.renderTarget());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.loki.lovisual.features.module.modules.combat.targetesp.constellation;
|
||||
|
||||
/**
|
||||
* Pure geometry of the "constellation" TargetESP mode: stars on a tilted,
|
||||
* slightly wobbling orbit around the target centre. Offsets are relative to
|
||||
* the target's feet position.
|
||||
*/
|
||||
public final class ConstellationLayout {
|
||||
private static final double TAU = Math.PI * 2.0;
|
||||
|
||||
private ConstellationLayout() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param out 3 * stars slots, filled with x, y, z per star
|
||||
* @param orbit orbit radius in target widths
|
||||
* @param time seconds
|
||||
* @param speed spin multiplier
|
||||
*/
|
||||
public static void positions(double[] out, int stars, float width, float height,
|
||||
float orbit, double time, float speed) {
|
||||
double spin = time * speed * 1.5;
|
||||
for (int s = 0; s < stars; s++) {
|
||||
double frac = (double) s / stars;
|
||||
double base = frac * TAU;
|
||||
double incl = Math.PI * 0.35 * Math.sin(frac * Math.PI * 1.5 + 0.5);
|
||||
double ang = spin + base;
|
||||
double r = orbit * width * (0.85 + 0.15 * Math.sin(base * 2.3));
|
||||
out[s * 3] = Math.cos(ang) * r;
|
||||
out[s * 3 + 1] = height / 2.0
|
||||
+ Math.sin(ang) * Math.sin(incl) * r * 0.7
|
||||
+ Math.sin(time * 1.2 + frac * TAU) * 0.2;
|
||||
out[s * 3 + 2] = Math.sin(ang) * Math.cos(incl) * r;
|
||||
}
|
||||
}
|
||||
|
||||
/** Star brightness pulse in [0.5, 1.0]. */
|
||||
public static float pulse(double time, int star, int stars) {
|
||||
double frac = (double) star / stars;
|
||||
return (float) (0.75 + 0.25 * Math.sin(time * 3.0 + frac * TAU * 1.7));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package dev.loki.lovisual.features.module.modules.combat.targetesp.constellation;
|
||||
|
||||
import dev.loki.lovisual.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.math.RenderMath;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*;
|
||||
|
||||
/** "Constellation" TargetESP mode: glowing stars orbiting the target, joined by thin lines. */
|
||||
public final class TargetEspConstellation {
|
||||
private static final float LINE_ALPHA = 120.0f / 255.0f;
|
||||
private static final float GLOW_SCALE = 3.5f;
|
||||
private static final float GLOW_ALPHA = 0.3f;
|
||||
|
||||
private TargetEspConstellation() {
|
||||
}
|
||||
|
||||
public static void render(Renderer3D renderer, LivingEntity target, float tickDelta,
|
||||
IntFunction<Integer> palette, int stars, float speed, float orbit,
|
||||
boolean lines, float pointSize, float glow) {
|
||||
if (renderer == null || target == null || stars < 2) return;
|
||||
|
||||
Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
|
||||
double time = (GameClock.millis() % 100000L) / 1000.0;
|
||||
double[] p = new double[stars * 3];
|
||||
ConstellationLayout.positions(p, stars, target.getBbWidth(), target.getBbHeight(), orbit, time, speed);
|
||||
|
||||
if (lines) {
|
||||
drawLines(renderer, pos, p, stars, palette);
|
||||
}
|
||||
drawStars(renderer, pos, p, stars, time, palette, pointSize, glow);
|
||||
}
|
||||
|
||||
private static void drawLines(Renderer3D renderer, Vec3 pos, double[] p, int stars,
|
||||
IntFunction<Integer> palette) {
|
||||
float prevWidth = RenderState.lineWidth;
|
||||
RenderState.lineWidth = 1.2f;
|
||||
MeshBuilder mesh;
|
||||
try {
|
||||
mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
|
||||
} finally {
|
||||
RenderState.lineWidth = prevWidth;
|
||||
}
|
||||
if (mesh == null) return;
|
||||
|
||||
for (int s = 0; s < stars; s++) {
|
||||
int n = (s + 1) % stars;
|
||||
int color = applyOpacity(palette.apply((int) (360.0 * s / stars)), LINE_ALPHA);
|
||||
mesh.ensureLineCapacity();
|
||||
int a = addVertex(mesh, pos.x + p[s * 3], pos.y + p[s * 3 + 1], pos.z + p[s * 3 + 2], color);
|
||||
int b = addVertex(mesh, pos.x + p[n * 3], pos.y + p[n * 3 + 1], pos.z + p[n * 3 + 2], color);
|
||||
mesh.line(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawStars(Renderer3D renderer, Vec3 pos, double[] p, int stars, double time,
|
||||
IntFunction<Integer> palette, float pointSize, float glow) {
|
||||
MeshBuilder core = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
|
||||
TextureStorage.PARTICLE_STARS, Renderer3D.DepthMode.MAIN);
|
||||
MeshBuilder halo = glow > 0.01f
|
||||
? renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
|
||||
TextureStorage.BLOOM, Renderer3D.DepthMode.MAIN)
|
||||
: null;
|
||||
if (core == null) return;
|
||||
|
||||
for (int s = 0; s < stars; s++) {
|
||||
float pulse = ConstellationLayout.pulse(time, s, stars);
|
||||
float alpha = 0.7f + 0.3f * pulse;
|
||||
int color = palette.apply((int) (360.0 * s / stars));
|
||||
double x = pos.x + p[s * 3];
|
||||
double y = pos.y + p[s * 3 + 1];
|
||||
double z = pos.z + p[s * 3 + 2];
|
||||
float size = pointSize * pulse;
|
||||
addBillboardQuad(core, x, y, z, size, applyOpacity(color, alpha));
|
||||
if (halo != null) {
|
||||
addBillboardQuad(halo, x, y, z, size * GLOW_SCALE, applyOpacity(color, alpha * glow * GLOW_ALPHA));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package dev.loki.lovisual.features.module.modules.combat.targetesp.state;
|
||||
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.features.relations.category.CategoryService;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/** Resolves TargetESP colors from the module's color settings, optionally mixed with relation colors. */
|
||||
public final class TargetEspColors {
|
||||
private final ModeValue mode;
|
||||
private final NumberValue<Integer> speed;
|
||||
private final NumberValue<Integer> phase;
|
||||
private final NumberValue<Integer> spread;
|
||||
private final RGBAColorValue primary;
|
||||
private final RGBAColorValue secondary;
|
||||
private final BooleanValue useRelations;
|
||||
private final NumberValue<Float> relationMix;
|
||||
|
||||
public TargetEspColors(ModeValue mode, NumberValue<Integer> speed, NumberValue<Integer> phase,
|
||||
NumberValue<Integer> spread, RGBAColorValue primary, RGBAColorValue secondary,
|
||||
BooleanValue useRelations, NumberValue<Float> relationMix) {
|
||||
this.mode = mode;
|
||||
this.speed = speed;
|
||||
this.phase = phase;
|
||||
this.spread = spread;
|
||||
this.primary = primary;
|
||||
this.secondary = secondary;
|
||||
this.useRelations = useRelations;
|
||||
this.relationMix = relationMix;
|
||||
}
|
||||
|
||||
public boolean usesSecondary() {
|
||||
return AnimatedRenderColors.usesSecondary(animatedMode());
|
||||
}
|
||||
|
||||
/** @param target entity whose relation color is mixed in, or null to skip relations */
|
||||
public int argb(int count, LivingEntity target) {
|
||||
int shifted = phase.get() + Math.round(count * spread.get() / 90.0f);
|
||||
int animated = AnimatedRenderColors.resolve(
|
||||
animatedMode(), speed.get(), shifted, primary.getArgb(), secondary.getArgb(), true);
|
||||
if (!useRelations.get() || target == null) {
|
||||
return animated;
|
||||
}
|
||||
int relation = AnimatedRenderColors.withAlpha(relationColor(target, animated), (animated >>> 24) & 0xFF);
|
||||
return AnimatedRenderColors.mixArgb(animated, relation, relationMix.get());
|
||||
}
|
||||
|
||||
private int relationColor(LivingEntity entity, int fallback) {
|
||||
if (entity instanceof Player player) {
|
||||
return CategoryService.getColor(player);
|
||||
}
|
||||
String name = entity.getName() != null ? entity.getName().getString() : null;
|
||||
if (name == null || name.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return CategoryService.getColor(name);
|
||||
}
|
||||
|
||||
private AnimatedRenderColors.Mode animatedMode() {
|
||||
return switch (mode.get()) {
|
||||
case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW;
|
||||
case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
|
||||
case "Sky" -> AnimatedRenderColors.Mode.SKY;
|
||||
case "Fade" -> AnimatedRenderColors.Mode.FADE;
|
||||
case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
|
||||
case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS;
|
||||
case "Theme" -> AnimatedRenderColors.Mode.THEME;
|
||||
default -> AnimatedRenderColors.Mode.STATIC;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.lovisual.features.module.modules.combat.targetesp.state;
|
||||
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
|
||||
/** Follows the current TargetESP target and fades its alpha in and out. */
|
||||
public final class TargetEspTracker {
|
||||
private static final float TICK_DT = 1.0f / 20.0f;
|
||||
private static final float SNAP_EPSILON = 0.02f;
|
||||
|
||||
private LivingEntity renderTarget;
|
||||
private float alpha;
|
||||
|
||||
public LivingEntity renderTarget() {
|
||||
return renderTarget;
|
||||
}
|
||||
|
||||
public float alpha() {
|
||||
return alpha;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return renderTarget != null && alpha > 0.001f;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
renderTarget = null;
|
||||
alpha = 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when the render target switched or vanished, so per-target
|
||||
* caches must be dropped
|
||||
*/
|
||||
public boolean update(LivingEntity next, float animationSpeed) {
|
||||
float dt = Math.max(AnimationUtility.deltaTime(), TICK_DT);
|
||||
if (next == null) {
|
||||
alpha = approach(0.0f, dt, animationSpeed);
|
||||
if (alpha > 0.0f) return false;
|
||||
boolean had = renderTarget != null;
|
||||
renderTarget = null;
|
||||
return had;
|
||||
}
|
||||
boolean switched = renderTarget != null && renderTarget != next;
|
||||
renderTarget = next;
|
||||
alpha = switched ? 1.0f : approach(1.0f, dt, animationSpeed);
|
||||
return switched;
|
||||
}
|
||||
|
||||
private float approach(float to, float dt, float speed) {
|
||||
return AnimationUtility.snap(AnimationUtility.approach(alpha, to, dt, speed), to, SNAP_EPSILON);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package dev.loki.lovisual.features.module.modules.combat.targetesp.sword;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Spring physics of the "swords" TargetESP mode: every sword is pulled towards its slot on an
|
||||
* orbit around the target, gets kicked outwards when the target is hurt, and leans into its
|
||||
* motion. Pure math, no Minecraft types.
|
||||
*/
|
||||
final class SwordPhysics {
|
||||
static final int COUNT = 8;
|
||||
private static final double SPRING = 0.22;
|
||||
private static final double DAMPING = 0.78;
|
||||
|
||||
private SwordPhysics() {
|
||||
}
|
||||
|
||||
static final class Sword {
|
||||
double x, y, z;
|
||||
double vx, vy, vz;
|
||||
float roll;
|
||||
float pitch;
|
||||
}
|
||||
|
||||
/** Frame inputs; angles in radians, {@code step} = frame time in 1/60 s units clamped to [0.2, 2]. */
|
||||
record Frame(double targetX, double targetZ, double centerY, double radius, double orbitAngle,
|
||||
double timeMs, double bobAmplitude, float step, boolean kick, boolean physics,
|
||||
double cameraYawRad) {
|
||||
}
|
||||
|
||||
/** Places every sword on its orbit slot at rest. */
|
||||
static void place(Sword[] swords, double targetX, double targetZ, double centerY, double radius, double orbitAngle) {
|
||||
for (int i = 0; i < swords.length; i++) {
|
||||
Sword s = swords[i];
|
||||
double a = orbitAngle + i * (Math.PI * 2.0 / swords.length);
|
||||
s.x = targetX + Math.cos(a) * radius;
|
||||
s.y = centerY;
|
||||
s.z = targetZ + Math.sin(a) * radius;
|
||||
s.vx = 0;
|
||||
s.vy = 0;
|
||||
s.vz = 0;
|
||||
s.roll = 0;
|
||||
s.pitch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void step(Sword[] swords, Frame f, Random rnd) {
|
||||
for (int i = 0; i < swords.length; i++) {
|
||||
Sword s = swords[i];
|
||||
double a = f.orbitAngle() + i * (Math.PI * 2.0 / swords.length);
|
||||
double bob = Math.sin(f.timeMs() * 0.0035 + i * (Math.PI / 4.0)) * f.bobAmplitude();
|
||||
double idealX = f.targetX() + Math.cos(a) * f.radius();
|
||||
double idealY = f.centerY() + bob;
|
||||
double idealZ = f.targetZ() + Math.sin(a) * f.radius();
|
||||
if (f.kick()) kick(s, f, rnd);
|
||||
if (!f.physics()) {
|
||||
s.x = idealX;
|
||||
s.y = idealY;
|
||||
s.z = idealZ;
|
||||
s.vx = 0;
|
||||
s.vy = 0;
|
||||
s.vz = 0;
|
||||
s.roll = 0;
|
||||
s.pitch = 0;
|
||||
continue;
|
||||
}
|
||||
double spring = SPRING * f.step();
|
||||
double damping = Math.pow(DAMPING, f.step());
|
||||
s.vx = (s.vx + (idealX - s.x) * spring) * damping;
|
||||
s.vy = (s.vy + (idealY - s.y) * spring) * damping;
|
||||
s.vz = (s.vz + (idealZ - s.z) * spring) * damping;
|
||||
s.x += s.vx * f.step();
|
||||
s.y += s.vy * f.step();
|
||||
s.z += s.vz * f.step();
|
||||
lean(s, f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void kick(Sword s, Frame f, Random rnd) {
|
||||
double dx = s.x - f.targetX();
|
||||
double dz = s.z - f.targetZ();
|
||||
double len = Math.hypot(dx, dz);
|
||||
if (len > 0.001) {
|
||||
double power = 0.16 + rnd.nextDouble() * 0.06;
|
||||
s.vx += dx / len * power;
|
||||
s.vz += dz / len * power;
|
||||
s.vy += 0.07;
|
||||
}
|
||||
s.roll += (float) ((rnd.nextDouble() - 0.5) * 28.0);
|
||||
}
|
||||
|
||||
/** Roll follows sideways speed as seen from the camera, pitch follows vertical speed. */
|
||||
private static void lean(Sword s, Frame f) {
|
||||
double sideSpeed = -s.vx * Math.cos(f.cameraYawRad()) - s.vz * Math.sin(f.cameraYawRad());
|
||||
float targetRoll = (float) Math.max(-42.0, Math.min(42.0, sideSpeed * 130.0));
|
||||
float targetPitch = (float) Math.max(-32.0, Math.min(32.0, -s.vy * 85.0));
|
||||
float k = Math.min(1f, 0.25f * f.step());
|
||||
s.roll += (targetRoll - s.roll) * k;
|
||||
s.pitch += (targetPitch - s.pitch) * k;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package dev.loki.lovisual.features.module.modules.combat.targetesp.sword;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.sword.SwordPhysics.Frame;
|
||||
import dev.loki.lovisual.features.module.modules.combat.targetesp.sword.SwordPhysics.Sword;
|
||||
import dev.loki.lovisual.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.math.RenderMath;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.addBillboardVertices;
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.applyOpacity;
|
||||
|
||||
/** "Swords" TargetESP mode: eight glowing swords orbit the target and recoil when it takes a hit. */
|
||||
public final class TargetEspSwords {
|
||||
private static final int HURT_COLOR = 0xFFFF4178;
|
||||
private static final float GLOW_SCALE = 1.35f;
|
||||
private static final float GLOW_ALPHA = 0.45f;
|
||||
|
||||
private final Sword[] swords = new Sword[SwordPhysics.COUNT];
|
||||
private final Random random = new Random();
|
||||
private LivingEntity lastTarget;
|
||||
private int lastHurt;
|
||||
private long lastMs;
|
||||
|
||||
public TargetEspSwords() {
|
||||
for (int i = 0; i < swords.length; i++) {
|
||||
swords[i] = new Sword();
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
lastTarget = null;
|
||||
lastHurt = 0;
|
||||
lastMs = 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param speedMs milliseconds per orbit
|
||||
* @param radius orbit radius multiplier
|
||||
* @param size sword size multiplier
|
||||
* @param bob vertical bobbing amplitude in blocks
|
||||
*/
|
||||
public void render(Renderer3D renderer, LivingEntity target, float tickDelta, IntFunction<Integer> palette,
|
||||
long speedMs, float radius, float size, float bob, boolean glow, boolean physics) {
|
||||
if (renderer == null || target == null) return;
|
||||
long now = GameClock.millis();
|
||||
float dt = lastMs == 0L ? 0f : Math.min(0.1f, (now - lastMs) / 1000f);
|
||||
lastMs = now;
|
||||
float step = Math.max(0.2f, Math.min(2.0f, dt * 60f));
|
||||
|
||||
Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
|
||||
double centerY = pos.y + target.getBbHeight() * 0.45;
|
||||
double orbitRadius = (target.getBbWidth() + 0.35) * radius;
|
||||
long period = Math.max(200L, speedMs);
|
||||
double orbit = (double) (now % period) / period * Math.PI * 2.0;
|
||||
|
||||
if (lastTarget != target) {
|
||||
lastTarget = target;
|
||||
lastHurt = target.hurtTime;
|
||||
SwordPhysics.place(swords, pos.x, pos.z, centerY, orbitRadius, orbit);
|
||||
}
|
||||
int hurt = target.hurtTime;
|
||||
boolean kick = hurt > lastHurt && hurt >= 9;
|
||||
lastHurt = hurt;
|
||||
float hurtPc = hurt > 0 ? (float) Math.sin(hurt * (Math.PI / 10.0)) : 0f;
|
||||
|
||||
float yawRad = (float) Math.toRadians(net.minecraft.client.Minecraft.getInstance().gameRenderer.mainCamera().yRot());
|
||||
SwordPhysics.step(swords, new Frame(pos.x, pos.z, centerY, orbitRadius, orbit, now, bob, step, kick,
|
||||
physics, yawRad), random);
|
||||
|
||||
float halfW = 0.20f * size;
|
||||
float halfH = 0.40f * size;
|
||||
if (glow) {
|
||||
MeshBuilder halo = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
|
||||
TextureStorage.BLOOM, Renderer3D.DepthMode.NONE);
|
||||
if (halo != null) draw(halo, palette, hurtPc, halfW * GLOW_SCALE, halfH * GLOW_SCALE, GLOW_ALPHA * (1f + hurtPc * 0.65f));
|
||||
}
|
||||
MeshBuilder core = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED, TextureStorage.SWORD,
|
||||
Renderer3D.DepthMode.NONE);
|
||||
if (core != null) draw(core, palette, hurtPc, halfW, halfH, 1f);
|
||||
}
|
||||
|
||||
private void draw(MeshBuilder mesh, IntFunction<Integer> palette, float hurtPc, float halfW, float halfH, float alpha) {
|
||||
for (int i = 0; i < swords.length; i++) {
|
||||
Sword s = swords[i];
|
||||
int base = AnimatedRenderColors.mixArgb(palette.apply(i * 45), HURT_COLOR, hurtPc);
|
||||
int color = applyOpacity(base, alpha);
|
||||
Quaternionf rot = new Quaternionf(RenderState.cameraRotation)
|
||||
.rotateZ((float) Math.toRadians(s.roll))
|
||||
.rotateX((float) Math.toRadians(s.pitch));
|
||||
Vector3f right = new Vector3f(1, 0, 0).rotate(rot).mul(halfW);
|
||||
Vector3f up = new Vector3f(0, 1, 0).rotate(rot).mul(halfH);
|
||||
addBillboardVertices(mesh, s.x, s.y, s.z, right, up, color, color, color, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package dev.loki.lovisual.features.module.modules.misc.voice;
|
||||
|
||||
import dev.loki.lovisual.compat.voicechat.VoiceChatBridge;
|
||||
import dev.loki.lovisual.compat.voicechat.VoiceState;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.render.engine.math.RenderMath;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Simple Voice Chat companion: pulsing indicators above players who are talking. */
|
||||
@ModuleInfo(id = "voicechat", displayName = "VoiceChat", category = ModuleCategory.MISC)
|
||||
public class VoiceChat extends Module {
|
||||
private static final float HEAD_OFFSET = 0.55f;
|
||||
private static final int MAX_SPEAKERS = 64;
|
||||
|
||||
private final NumberValue<Integer> holdMs = num("voiceChatHoldMs", "hold_ms", 350, 100, 1500);
|
||||
private final NumberValue<Float> maxDistance = num("voiceChatMaxDistance", "max_distance", 32.0f, 8.0f, 96.0f);
|
||||
private final NumberValue<Float> indicatorSize = num("voiceChatIndicatorSize", "indicator_size", 1.0f, 0.5f, 2.5f);
|
||||
private final BooleanValue pulse = bool("voiceChatPulse", "pulse", true);
|
||||
private final BooleanValue showSelf = bool("voiceChatShowSelf", "show_self", true);
|
||||
private final BooleanValue showMuted = visibleWhen(bool("voiceChatShowMuted", "show_muted", true), showSelf::get);
|
||||
private final RGBAColorValue color = color("voiceChatColor", "color", "#FF55FF77");
|
||||
private final RGBAColorValue whisperColor = color("voiceChatWhisperColor", "whisper_color", "#FFFFD255");
|
||||
private final RGBAColorValue selfColor =
|
||||
visibleWhen(color("voiceChatSelfColor", "self_color", "#FF55AAFF"), showSelf::get);
|
||||
private final RGBAColorValue mutedColor = visibleWhen(
|
||||
color("voiceChatMutedColor", "muted_color", "#FFFF5555"), () -> showSelf.get() && showMuted.get());
|
||||
|
||||
@Override
|
||||
protected String getUnavailableReason() {
|
||||
return VoiceChatBridge.isInstalled() ? "" : "Simple Voice Chat is not installed";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
return WorldPhase.AFTER_POST_PROCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!isEnabled() || mc.level == null || mc.player == null || !VoiceChatBridge.isInstalled()) return;
|
||||
|
||||
MeshBuilder rings = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE, Renderer3D.DepthMode.MAIN);
|
||||
MeshBuilder halo = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
|
||||
TextureStorage.BLOOM, Renderer3D.DepthMode.MAIN);
|
||||
if (rings == null || halo == null) return;
|
||||
|
||||
VoiceState state = VoiceChatBridge.state();
|
||||
long hold = holdMs.get();
|
||||
double time = (GameClock.millis() % 100000L) / 1000.0;
|
||||
float size = indicatorSize.get();
|
||||
double maxSq = (double) maxDistance.get() * maxDistance.get();
|
||||
|
||||
for (VoiceState.Speaker speaker : state.speakers(hold, MAX_SPEAKERS)) {
|
||||
Player player = mc.level.getPlayerByUUID(speaker.id());
|
||||
if (player == null || player == mc.player) continue;
|
||||
Vec3 head = headPosition(player, tickDelta);
|
||||
if (head.distanceToSqr(mc.player.position()) > maxSq) continue;
|
||||
int argb = speaker.whisper() ? whisperColor.getArgb() : color.getArgb();
|
||||
VoiceIndicatorRenderer.speaking(rings, halo, head, size, state.level(speaker.id(), hold), argb, time, pulse.get());
|
||||
}
|
||||
|
||||
if (showSelf.get() && !mc.options.getCameraType().isFirstPerson()) {
|
||||
renderSelf(mc.player, state, rings, halo, hold, time, size, tickDelta);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderSelf(Player self, VoiceState state, MeshBuilder rings, MeshBuilder halo,
|
||||
long hold, double time, float size, float tickDelta) {
|
||||
Vec3 head = headPosition(self, tickDelta);
|
||||
float level = state.selfLevel(hold);
|
||||
if (level > 0f) {
|
||||
int argb = state.isSelfWhispering(hold) ? whisperColor.getArgb() : selfColor.getArgb();
|
||||
VoiceIndicatorRenderer.speaking(rings, halo, head, size, level, argb, time, pulse.get());
|
||||
}
|
||||
VoiceState.MicStatus mic = state.micStatus(hold);
|
||||
boolean silenced = mic == VoiceState.MicStatus.MUTED || mic == VoiceState.MicStatus.DISABLED;
|
||||
if (showMuted.get() && silenced) {
|
||||
VoiceIndicatorRenderer.muted(rings, head, size * 0.45f, mutedColor.getArgb());
|
||||
}
|
||||
}
|
||||
|
||||
private static Vec3 headPosition(Player player, float tickDelta) {
|
||||
Vec3 pos = RenderMath.getLerpedPos(player, tickDelta);
|
||||
return new Vec3(pos.x, pos.y + player.getBbHeight() + HEAD_OFFSET, pos.z);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package dev.loki.lovisual.features.module.modules.misc.voice;
|
||||
|
||||
/** Pure animation of the expanding rings drawn above a talking player. */
|
||||
final class VoiceIndicatorPulse {
|
||||
static final int RINGS = 2;
|
||||
private static final double PULSE_HZ = 0.9;
|
||||
|
||||
private VoiceIndicatorPulse() {
|
||||
}
|
||||
|
||||
/** @param radius ring radius as a fraction of the indicator size; @param alpha opacity in [0, 1] */
|
||||
record Ring(float radius, float alpha) { }
|
||||
|
||||
static Ring ring(double timeSec, int index, boolean pulse) {
|
||||
double offset = (double) index / RINGS;
|
||||
double phase = pulse ? (timeSec * PULSE_HZ + offset) % 1.0 : 0.35 + 0.3 * offset;
|
||||
float p = (float) phase;
|
||||
return new Ring(0.45f + 0.75f * p, pulse ? 1.0f - p : 0.6f - 0.25f * (float) offset);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package dev.loki.lovisual.features.module.modules.misc.voice;
|
||||
|
||||
import dev.loki.lovisual.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.*;
|
||||
|
||||
/** Draws the billboard glyphs of {@link VoiceChat}: a glowing halo with rings, and a crossed-out mic mark. */
|
||||
final class VoiceIndicatorRenderer {
|
||||
private static final int RING_SEGMENTS = 24;
|
||||
private static final float RING_WIDTH = 0.045f;
|
||||
private static final float CROSS_WIDTH = 0.05f;
|
||||
|
||||
private VoiceIndicatorRenderer() {
|
||||
}
|
||||
|
||||
/** @param ringMesh untextured additive mesh; @param haloMesh soft glow texture mesh */
|
||||
static void speaking(MeshBuilder ringMesh, MeshBuilder haloMesh, Vec3 at, float size, float level,
|
||||
int argb, double timeSec, boolean pulse) {
|
||||
addBillboardQuad(haloMesh, at.x, at.y, at.z, size * 0.9f, applyOpacity(argb, level * 0.55f));
|
||||
Vector3f right = new Vector3f(1, 0, 0).rotate(RenderState.cameraRotation);
|
||||
Vector3f up = new Vector3f(0, 1, 0).rotate(RenderState.cameraRotation);
|
||||
for (int i = 0; i < VoiceIndicatorPulse.RINGS; i++) {
|
||||
VoiceIndicatorPulse.Ring ring = VoiceIndicatorPulse.ring(timeSec, i, pulse);
|
||||
ring(ringMesh, at, right, up, size * ring.radius(), applyOpacity(argb, level * ring.alpha()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Red cross, used over your own head when the microphone is muted or disabled. */
|
||||
static void muted(MeshBuilder mesh, Vec3 at, float size, int argb) {
|
||||
Vector3f right = new Vector3f(1, 0, 0).rotate(RenderState.cameraRotation).mul(size * 0.5f);
|
||||
Vector3f up = new Vector3f(0, 1, 0).rotate(RenderState.cameraRotation).mul(size * 0.5f);
|
||||
Vector3f normal = new Vector3f(right).cross(up).normalize();
|
||||
bar(mesh, at, new Vector3f(right).add(up), new Vector3f(right).sub(up), normal, size, argb);
|
||||
bar(mesh, at, new Vector3f(right).sub(up), new Vector3f(right).add(up), normal, size, argb);
|
||||
}
|
||||
|
||||
private static void ring(MeshBuilder mesh, Vec3 at, Vector3f right, Vector3f up, float radius, int argb) {
|
||||
float inner = Math.max(0f, radius - RING_WIDTH);
|
||||
for (int i = 0; i < RING_SEGMENTS; i++) {
|
||||
double a1 = i * Math.PI * 2.0 / RING_SEGMENTS;
|
||||
double a2 = (i + 1) * Math.PI * 2.0 / RING_SEGMENTS;
|
||||
mesh.ensureQuadCapacity();
|
||||
int v1 = point(mesh, at, right, up, a1, radius, argb);
|
||||
int v2 = point(mesh, at, right, up, a2, radius, argb);
|
||||
int v3 = point(mesh, at, right, up, a2, inner, argb);
|
||||
int v4 = point(mesh, at, right, up, a1, inner, argb);
|
||||
mesh.quad(v1, v2, v3, v4);
|
||||
}
|
||||
}
|
||||
|
||||
private static int point(MeshBuilder mesh, Vec3 at, Vector3f right, Vector3f up, double angle, float r, int argb) {
|
||||
float x = (float) Math.cos(angle) * r;
|
||||
float y = (float) Math.sin(angle) * r;
|
||||
return addVertex(mesh, at.x + right.x * x + up.x * y, at.y + right.y * x + up.y * y,
|
||||
at.z + right.z * x + up.z * y, argb);
|
||||
}
|
||||
|
||||
/** Thin camera-facing quad along {@code from -> to} (offsets from {@code at}). */
|
||||
private static void bar(MeshBuilder mesh, Vec3 at, Vector3f from, Vector3f to,
|
||||
Vector3f normal, float size, int argb) {
|
||||
Vector3f side = new Vector3f(to).sub(from).cross(normal);
|
||||
if (side.lengthSquared() < 1.0E-8f) return;
|
||||
side.normalize(CROSS_WIDTH * size);
|
||||
mesh.ensureQuadCapacity();
|
||||
int v1 = addVertex(mesh, at.x + from.x - side.x, at.y + from.y - side.y, at.z + from.z - side.z, argb);
|
||||
int v2 = addVertex(mesh, at.x + from.x + side.x, at.y + from.y + side.y, at.z + from.z + side.z, argb);
|
||||
int v3 = addVertex(mesh, at.x + to.x + side.x, at.y + to.y + side.y, at.z + to.z + side.z, argb);
|
||||
int v4 = addVertex(mesh, at.x + to.x - side.x, at.y + to.y - side.y, at.z + to.z - side.z, argb);
|
||||
mesh.quad(v1, v2, v3, v4);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import dev.loki.lovisual.config.values.primitive.NumberValue;
|
|||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodKillHelper;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
|
|
@ -25,39 +26,46 @@ import java.util.Map;
|
|||
//todo Description
|
||||
@ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS)
|
||||
public class KillEffect extends Module {
|
||||
private static final String SETTING_MODE = "mode";
|
||||
private static final String SETTING_Y_SPEED = "y_speed";
|
||||
private static final String SETTING_PLAY_SOUND = "play_sound";
|
||||
private static final String SETTING_SOUND_VOLUME = "sound_volume";
|
||||
private static final String SETTING_COLOR = "color";
|
||||
private static final String SETTING_MOBS = "mobs";
|
||||
private static final String SETTING_BEAM_INTENSITY = "beam_intensity";
|
||||
private static final String SETTING_BEAM_SIZE = "beam_size";
|
||||
private static final Identifier ORTHODOX_SOUND =
|
||||
Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav");
|
||||
|
||||
private final ModeValue mode = modeSetting("killEffectMode", SETTING_MODE, "Orthodox",
|
||||
"Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado");
|
||||
private final ModeValue mode = modeSetting("killEffectMode", "mode", "Orthodox",
|
||||
"Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado", "Blood");
|
||||
private final NumberValue<Integer> ySpeed = visibleWhen(
|
||||
num("killEffectYSpeed", SETTING_Y_SPEED, 0, -10, 10), () -> isMode("Orthodox"));
|
||||
num("killEffectYSpeed", "y_speed", 0, -10, 10), () -> isMode("Orthodox"));
|
||||
private final BooleanValue playSound = visibleWhen(
|
||||
bool("killEffectPlaySound", SETTING_PLAY_SOUND, true), () -> isMode("Orthodox"));
|
||||
bool("killEffectPlaySound", "play_sound", true), () -> isMode("Orthodox"));
|
||||
private final NumberValue<Double> soundVolume = visibleWhen(
|
||||
num("killEffectSoundVolume", SETTING_SOUND_VOLUME, 1.0, 0.0, 2.0),
|
||||
num("killEffectSoundVolume", "sound_volume", 1.0, 0.0, 2.0),
|
||||
() -> isMode("Orthodox") && playSound.get());
|
||||
private final RGBAColorValue color = visibleWhen(
|
||||
color("killEffectColor", SETTING_COLOR, "#FFFF9600"), this::usesColorSetting);
|
||||
private final BooleanValue mobs = bool("killEffectMobs", SETTING_MOBS, false);
|
||||
color("killEffectColor", "color", "#FFFF9600"), this::usesColorSetting);
|
||||
private final BooleanValue mobs = bool("killEffectMobs", "mobs", false);
|
||||
private final NumberValue<Float> beamIntensity = visibleWhen(
|
||||
num("killEffectBeamIntensity", SETTING_BEAM_INTENSITY, 1.2f, 0.2f, 3.0f), this::usesBeamSettings);
|
||||
num("killEffectBeamIntensity", "beam_intensity", 1.2f, 0.2f, 3.0f), this::usesBeamSettings);
|
||||
private final NumberValue<Float> beamSize = visibleWhen(
|
||||
num("killEffectBeamSize", SETTING_BEAM_SIZE, 2.0f, 0.5f, 5.0f), this::usesBeamSettings);
|
||||
num("killEffectBeamSize", "beam_size", 2.0f, 0.5f, 5.0f), this::usesBeamSettings);
|
||||
|
||||
private final RGBAColorValue bloodColor = visibleWhen(
|
||||
color("killEffectBloodColor", "blood_color", "#FF8A0303"), () -> isMode("Blood"));
|
||||
private final ModeValue bloodStyle = visibleWhen(
|
||||
modeSetting("killEffectBloodStyle", "blood_style", "Burst", "Burst", "Fountain", "Ring", "Spray"),
|
||||
() -> isMode("Blood"));
|
||||
private final NumberValue<Integer> bloodAmount = visibleWhen(
|
||||
num("killEffectBloodAmount", "blood_amount", 70, 10, 250), () -> isMode("Blood"));
|
||||
private final NumberValue<Float> bloodPower = visibleWhen(
|
||||
num("killEffectBloodPower", "blood_power", 1.0f, 0.3f, 2.5f), () -> isMode("Blood"));
|
||||
private final NumberValue<Float> bloodSize = visibleWhen(
|
||||
num("killEffectBloodSize", "blood_size", 1.0f, 0.3f, 3.0f), () -> isMode("Blood"));
|
||||
private final NumberValue<Float> bloodTime = visibleWhen(
|
||||
num("killEffectBloodTime", "blood_time", 6.0f, 1.0f, 30.0f), () -> isMode("Blood"));
|
||||
|
||||
private final Map<Integer, Long> handled = new HashMap<>();
|
||||
private final KillOrthodoxHelper orthodoxHelper = new KillOrthodoxHelper();
|
||||
private final KillEmberHelper emberHelper = new KillEmberHelper();
|
||||
private final KillLightningHelper lightningHelper = new KillLightningHelper();
|
||||
private final KillBeamHelper beamHelper = new KillBeamHelper();
|
||||
private final BloodKillHelper bloodHelper = new BloodKillHelper();
|
||||
|
||||
private boolean isMode(String value) {
|
||||
return value.equals(mode.get());
|
||||
|
|
@ -107,6 +115,7 @@ public class KillEffect extends Module {
|
|||
beamHelper.tick();
|
||||
lightningHelper.tick();
|
||||
orthodoxHelper.tick(now);
|
||||
bloodHelper.tick(mc.level, mc.player, bloodTime.get());
|
||||
}
|
||||
|
||||
private void handleKill(Vec3 pos, LivingEntity entity, long now) {
|
||||
|
|
@ -126,6 +135,11 @@ public class KillEffect extends Module {
|
|||
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.PORTAL);
|
||||
} else if ("Tornado".equals(current)) {
|
||||
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.TORNADO);
|
||||
} else if ("Blood".equals(current)) {
|
||||
bloodHelper.spawn(new Vec3(pos.x, pos.y + entity.getBbHeight() * 0.6, pos.z),
|
||||
Math.clamp(entity.getBbWidth(), 0.3f, 1.6f), Math.clamp(entity.getBbHeight(), 0.3f, 2.6f),
|
||||
bloodAmount.get(), bloodPower.get(), bloodSize.get(), bloodColor.getArgb(),
|
||||
bloodStyle.get(), pos.subtract(Minecraft.getInstance().player.position()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +150,7 @@ public class KillEffect extends Module {
|
|||
emberHelper.clear();
|
||||
lightningHelper.clear();
|
||||
beamHelper.clear();
|
||||
bloodHelper.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -153,6 +168,7 @@ public class KillEffect extends Module {
|
|||
emberHelper.render(renderer, tickDelta);
|
||||
}
|
||||
beamHelper.render(renderer, mc, beamIntensity.get(), beamSize.get());
|
||||
bloodHelper.render(renderer, mc.gameRenderer.mainCamera().position(), tickDelta, bloodTime.get());
|
||||
}
|
||||
|
||||
private void spawnFallingLava(LivingEntity entity) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodParticles.Drop;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodParticles.Splat;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.addVertex;
|
||||
|
||||
/** Builds the triangles of a teardrop (drop) and a ragged puddle (splat), plus color helpers. */
|
||||
final class BloodGeometry {
|
||||
private static final int RING = 6;
|
||||
private static final int SPLAT_SEGMENTS = 14;
|
||||
|
||||
private BloodGeometry() {
|
||||
}
|
||||
|
||||
/** Teardrop: a 6-sided ring with a front cap and a speed-stretched, fading tail. */
|
||||
static void drop(MeshBuilder mesh, Vec3 p, Drop d) {
|
||||
double speed = d.vel.length();
|
||||
Vec3 dir = speed < 1.0E-4 ? new Vec3(0, -1, 0) : d.vel.scale(1.0 / speed);
|
||||
float r = d.size * 0.5f;
|
||||
float head = r * 0.9f;
|
||||
float tail = r * (1.6f + (float) Math.min(speed, 0.9) * 5.5f);
|
||||
Vec3 up = Math.abs(dir.y) > 0.95 ? new Vec3(1, 0, 0) : new Vec3(0, 1, 0);
|
||||
Vec3 s1 = dir.cross(up).normalize();
|
||||
Vec3 s2 = dir.cross(s1).normalize();
|
||||
Vec3 front = p.add(dir.scale(head));
|
||||
Vec3 back = p.subtract(dir.scale(tail));
|
||||
Vec3 center = p.add(dir.scale(head * 0.15));
|
||||
for (int i = 0; i < RING; i++) {
|
||||
int j = (i + 1) % RING;
|
||||
Vec3 n1 = ringNormal(s1, s2, i);
|
||||
Vec3 n2 = ringNormal(s1, s2, j);
|
||||
Vec3 n = n1.add(n2).normalize();
|
||||
int cf = lit(d.color, n.add(dir.scale(0.6)).normalize(), d.light, true);
|
||||
int cb = lit(d.color, n.add(dir.scale(-0.3)).normalize(), d.light, false);
|
||||
tri(mesh, front, center.add(n1.scale(r)), center.add(n2.scale(r)), cf, cf, cf);
|
||||
tri(mesh, center.add(n1.scale(r)), back, center.add(n2.scale(r)), cb, withAlpha(shade(cb, 0.8f), 150), cb);
|
||||
}
|
||||
}
|
||||
|
||||
/** Puddle: grows quickly, then fades and darkens as it dries. Returns false when finished. */
|
||||
static boolean splat(MeshBuilder mesh, Splat s, long nowMs, float lifeMs) {
|
||||
long age = nowMs - s.startMs();
|
||||
float life = age / lifeMs;
|
||||
float fade = 1.0f - smooth((life - 0.7f) / 0.3f);
|
||||
if (life >= 1.0f || fade <= 0.01f) return false;
|
||||
float grow = 1.0f - (float) Math.pow(1.0f - Math.min(1f, age / 260f), 3);
|
||||
float dry = smooth(life / 0.8f);
|
||||
float radius = s.radius() * (0.35f + 0.65f * grow);
|
||||
int base = shade(s.color(), s.light() * (1.0f - 0.45f * dry));
|
||||
int centerCol = withAlpha(shade(base, 0.72f), Math.round(235 * fade));
|
||||
int edgeCol = withAlpha(base, Math.round(205 * fade));
|
||||
int rimCol = withAlpha(base, 0);
|
||||
Vec3 c = new Vec3(s.pos().x, s.pos().y + 0.006 + s.layer() * 0.0004, s.pos().z);
|
||||
for (int i = 0; i < SPLAT_SEGMENTS; i++) {
|
||||
int j = (i + 1) % SPLAT_SEGMENTS;
|
||||
float r1 = radius * (0.72f + noise(s.seed(), i) * 0.5f);
|
||||
float r2 = radius * (0.72f + noise(s.seed(), j) * 0.5f);
|
||||
double a1 = i * Math.PI * 2.0 / SPLAT_SEGMENTS;
|
||||
double a2 = j * Math.PI * 2.0 / SPLAT_SEGMENTS;
|
||||
Vec3 p1 = c.add(Math.cos(a1) * r1, 0, Math.sin(a1) * r1);
|
||||
Vec3 p2 = c.add(Math.cos(a2) * r2, 0, Math.sin(a2) * r2);
|
||||
Vec3 o1 = c.add(Math.cos(a1) * r1 * 1.18, 0, Math.sin(a1) * r1 * 1.18);
|
||||
Vec3 o2 = c.add(Math.cos(a2) * r2 * 1.18, 0, Math.sin(a2) * r2 * 1.18);
|
||||
tri(mesh, c, p1, p2, centerCol, edgeCol, edgeCol);
|
||||
tri(mesh, p1, o1, o2, edgeCol, rimCol, rimCol);
|
||||
tri(mesh, p1, o2, p2, edgeCol, rimCol, edgeCol);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vec3 ringNormal(Vec3 s1, Vec3 s2, int i) {
|
||||
double a = i * Math.PI * 2.0 / RING;
|
||||
return s1.scale(Math.cos(a)).add(s2.scale(Math.sin(a)));
|
||||
}
|
||||
|
||||
private static void tri(MeshBuilder m, Vec3 a, Vec3 b, Vec3 c, int ca, int cb, int cc) {
|
||||
m.ensureTriCapacity();
|
||||
m.triangle(addVertex(m, a.x, a.y, a.z, ca), addVertex(m, b.x, b.y, b.z, cb), addVertex(m, c.x, c.y, c.z, cc));
|
||||
}
|
||||
|
||||
private static int lit(int color, Vec3 normal, float light, boolean highlight) {
|
||||
double dot = Math.max(0.0, normal.x * 0.35 + normal.y * 0.85 + normal.z * 0.25);
|
||||
int c = shade(color, (float) (0.45 + 0.75 * dot) * light);
|
||||
return highlight && dot > 0.8 ? mix(c, 0xFFFF9A9A, (float) ((dot - 0.8) / 0.2) * 0.45f * light) : c;
|
||||
}
|
||||
|
||||
static int shade(int color, float k) {
|
||||
int r = Math.min(255, Math.round(((color >> 16) & 255) * k));
|
||||
int g = Math.min(255, Math.round(((color >> 8) & 255) * k));
|
||||
int b = Math.min(255, Math.round((color & 255) * k));
|
||||
return (color & 0xFF000000) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
static int mix(int first, int second, float t) {
|
||||
float k = Math.max(0f, Math.min(1f, t));
|
||||
int r = Math.round(((first >> 16) & 255) * (1 - k) + ((second >> 16) & 255) * k);
|
||||
int g = Math.round(((first >> 8) & 255) * (1 - k) + ((second >> 8) & 255) * k);
|
||||
int b = Math.round((first & 255) * (1 - k) + (second & 255) * k);
|
||||
return (first & 0xFF000000) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
static int withAlpha(int color, int alpha) {
|
||||
return (Math.max(0, Math.min(255, alpha)) << 24) | (color & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
static float smooth(float v) {
|
||||
float t = Math.max(0f, Math.min(1f, v));
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
/** Deterministic per-puddle noise in [0, 1]. */
|
||||
static float noise(long seed, int salt) {
|
||||
long v = seed + 0x9E3779B97F4A7C15L * (salt + 1L);
|
||||
v = (v ^ (v >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
v = (v ^ (v >>> 27)) * 0x94D049BB133111EBL;
|
||||
return ((v ^ (v >>> 31)) >>> 40) / 16777215.0f;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodParticles.Drop;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodParticles.Splat;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.ClipContext;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** KillEffect "Blood" mode: a burst of physically simulated drops that stain the floor. */
|
||||
public final class BloodKillHelper {
|
||||
private static final int MAX_DROPS = 900;
|
||||
private static final int MAX_SPLATS = 700;
|
||||
private static final double RANGE_SQ = 96.0 * 96.0;
|
||||
|
||||
private final Random random = new Random();
|
||||
private final List<Drop> drops = new ArrayList<>();
|
||||
private final List<Splat> splats = new ArrayList<>();
|
||||
|
||||
/** @param style BURST, FOUNTAIN, RING or SPRAY (case-insensitive); {@code away} points attacker to victim */
|
||||
public void spawn(Vec3 center, float width, float height, int count, float power, float scale, int color,
|
||||
String style, Vec3 away) {
|
||||
Vec3 flat = new Vec3(away.x, 0, away.z);
|
||||
Vec3 dir = flat.lengthSqr() < 1.0E-6 ? new Vec3(1, 0, 0) : flat.normalize();
|
||||
BloodPhysics.burst(drops, center, width, height, count, power, scale, color,
|
||||
BloodPhysics.Style.of(style), dir, random);
|
||||
trim(drops, MAX_DROPS);
|
||||
}
|
||||
|
||||
public void tick(ClientLevel level, Entity viewer, float splatSeconds) {
|
||||
long now = GameClock.millis();
|
||||
long life = (long) (splatSeconds * 1000f);
|
||||
splats.removeIf(s -> now - s.startMs() > life);
|
||||
if (drops.isEmpty()) return;
|
||||
|
||||
BloodPhysics.Surface surface = surface(level, viewer);
|
||||
List<Drop> children = new ArrayList<>();
|
||||
drops.removeIf(d -> !BloodPhysics.step(d, surface, splats, children, now, random));
|
||||
drops.addAll(children);
|
||||
trim(drops, MAX_DROPS);
|
||||
trim(splats, MAX_SPLATS);
|
||||
}
|
||||
|
||||
public void render(Renderer3D renderer, Vec3 camera, float tickDelta, float splatSeconds) {
|
||||
if (renderer == null || (drops.isEmpty() && splats.isEmpty())) return;
|
||||
MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_DEPTH, Renderer3D.DepthMode.MAIN);
|
||||
if (mesh == null) return;
|
||||
|
||||
long now = GameClock.millis();
|
||||
float lifeMs = splatSeconds * 1000f;
|
||||
for (Splat s : splats) {
|
||||
if (s.pos().distanceToSqr(camera) <= RANGE_SQ) {
|
||||
BloodGeometry.splat(mesh, s, now, lifeMs);
|
||||
}
|
||||
}
|
||||
float t = Math.max(0f, Math.min(1f, tickDelta));
|
||||
for (Drop d : drops) {
|
||||
Vec3 p = d.prev.add(d.pos.subtract(d.prev).scale(t));
|
||||
if (p.distanceToSqr(camera) <= RANGE_SQ) {
|
||||
BloodGeometry.drop(mesh, p, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
drops.clear();
|
||||
splats.clear();
|
||||
}
|
||||
|
||||
private static BloodPhysics.Surface surface(ClientLevel level, Entity viewer) {
|
||||
return new BloodPhysics.Surface() {
|
||||
@Override
|
||||
public BloodPhysics.Hit cast(Vec3 from, Vec3 to) {
|
||||
BlockHitResult hit = level.clip(new ClipContext(from, to,
|
||||
ClipContext.Block.COLLIDER, ClipContext.Fluid.ANY, viewer));
|
||||
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return null;
|
||||
return new BloodPhysics.Hit(hit.getLocation(), hit.getDirection().getStepX(),
|
||||
hit.getDirection().getStepY(), hit.getDirection().getStepZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float light(Vec3 pos) {
|
||||
return 0.3f + 0.7f * (level.getMaxLocalRawBrightness(BlockPos.containing(pos)) / 15.0f);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void trim(List<?> list, int max) {
|
||||
if (list.size() > max) {
|
||||
list.subList(0, list.size() - max).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Mutable blood drop in flight and immutable floor splat. */
|
||||
final class BloodParticles {
|
||||
private BloodParticles() {
|
||||
}
|
||||
|
||||
static final class Drop {
|
||||
Vec3 pos;
|
||||
Vec3 prev;
|
||||
Vec3 vel;
|
||||
final float size;
|
||||
final int color;
|
||||
final boolean child;
|
||||
final float scale;
|
||||
float light = 1.0f;
|
||||
int age;
|
||||
|
||||
Drop(Vec3 pos, Vec3 vel, float size, int color, float scale, boolean child) {
|
||||
this.pos = pos;
|
||||
this.prev = pos;
|
||||
this.vel = vel;
|
||||
this.size = size;
|
||||
this.color = color | 0xFF000000;
|
||||
this.scale = scale;
|
||||
this.child = child;
|
||||
}
|
||||
}
|
||||
|
||||
record Splat(Vec3 pos, float radius, int color, long startMs, long seed, float light, int layer) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodParticles.Drop;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodParticles.Splat;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** Drop physics: ballistic flight, wall bounces, floor splats and secondary droplets. */
|
||||
final class BloodPhysics {
|
||||
static final int MAX_FLIGHT_TICKS = 120;
|
||||
private static final double GRAVITY = 0.045;
|
||||
private static final double DRAG = 0.985;
|
||||
|
||||
private BloodPhysics() {
|
||||
}
|
||||
|
||||
record Hit(Vec3 at, int stepX, int stepY, int stepZ) {
|
||||
}
|
||||
|
||||
/** World access, kept abstract so the physics stays testable. */
|
||||
interface Surface {
|
||||
/** First solid block hit on the segment, or null. */
|
||||
Hit cast(Vec3 from, Vec3 to);
|
||||
|
||||
/** Brightness multiplier in [0.3, 1]. */
|
||||
float light(Vec3 pos);
|
||||
}
|
||||
|
||||
/** Shape of the initial blood eruption. */
|
||||
enum Style {
|
||||
BURST, FOUNTAIN, RING, SPRAY;
|
||||
|
||||
static Style of(String name) {
|
||||
for (Style s : values()) {
|
||||
if (s.name().equalsIgnoreCase(name)) return s;
|
||||
}
|
||||
return BURST;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param away horizontal unit vector pointing from the attacker to the victim, used by SPRAY
|
||||
*/
|
||||
static void burst(List<Drop> out, Vec3 center, float width, float height, int count, float power,
|
||||
float scale, int color, Style style, Vec3 away, Random rnd) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
Vec3 origin = center.add((rnd.nextDouble() - 0.5) * width * 0.6,
|
||||
(rnd.nextDouble() - 0.5) * height * 0.5, (rnd.nextDouble() - 0.5) * width * 0.6);
|
||||
float size = (float) (0.025 + Math.pow(rnd.nextDouble(), 2.2) * 0.075) * scale;
|
||||
int shade = BloodGeometry.shade(color, 0.82f + rnd.nextFloat() * 0.3f);
|
||||
out.add(new Drop(origin, velocity(style, power, away, rnd), size, shade, scale, false));
|
||||
}
|
||||
}
|
||||
|
||||
private static Vec3 velocity(Style style, double power, Vec3 away, Random rnd) {
|
||||
double angle = rnd.nextDouble() * Math.PI * 2.0;
|
||||
double cos = Math.cos(angle);
|
||||
double sin = Math.sin(angle);
|
||||
return switch (style) {
|
||||
case FOUNTAIN -> {
|
||||
double h = (0.02 + rnd.nextDouble() * 0.07) * power;
|
||||
yield new Vec3(cos * h, (0.25 + rnd.nextDouble() * 0.35) * power * 1.6, sin * h);
|
||||
}
|
||||
case RING -> {
|
||||
double h = (0.18 + rnd.nextDouble() * 0.12) * power;
|
||||
yield new Vec3(cos * h, (0.04 + rnd.nextDouble() * 0.05) * power, sin * h);
|
||||
}
|
||||
case SPRAY -> {
|
||||
double forward = (0.12 + rnd.nextDouble() * 0.25) * power;
|
||||
double side = (rnd.nextDouble() - 0.5) * 0.35 * power;
|
||||
yield new Vec3(away.x * forward - away.z * side, (0.05 + rnd.nextDouble() * 0.2) * power,
|
||||
away.z * forward + away.x * side);
|
||||
}
|
||||
default -> {
|
||||
double h = (0.05 + rnd.nextDouble() * 0.24) * power;
|
||||
yield new Vec3(cos * h, (0.06 + rnd.nextDouble() * 0.34) * power, sin * h);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Advances one tick; returns false once the drop is spent (landed or expired). */
|
||||
static boolean step(Drop d, Surface surface, List<Splat> splats, List<Drop> children, long nowMs, Random rnd) {
|
||||
d.prev = d.pos;
|
||||
if (++d.age > MAX_FLIGHT_TICKS) return false;
|
||||
|
||||
Vec3 vel = new Vec3(d.vel.x * DRAG, (d.vel.y - GRAVITY) * DRAG, d.vel.z * DRAG);
|
||||
Vec3 next = d.pos.add(vel);
|
||||
Hit hit = surface.cast(d.pos, next);
|
||||
if (hit == null) {
|
||||
d.pos = next;
|
||||
d.vel = vel;
|
||||
if ((d.age & 3) == 0) d.light = surface.light(next);
|
||||
return true;
|
||||
}
|
||||
if (hit.stepY() > 0) {
|
||||
land(d, hit.at(), vel, surface, splats, children, nowMs, rnd);
|
||||
return false;
|
||||
}
|
||||
d.vel = bounce(hit, vel);
|
||||
d.pos = hit.at().add(hit.stepX() * 0.01, hit.stepY() * 0.01, hit.stepZ() * 0.01);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vec3 bounce(Hit hit, Vec3 vel) {
|
||||
if (hit.stepY() < 0) {
|
||||
return new Vec3(vel.x * 0.4, -Math.abs(vel.y) * 0.1, vel.z * 0.4);
|
||||
}
|
||||
double vx = hit.stepX() != 0 ? -vel.x * 0.12 : vel.x * 0.3;
|
||||
double vz = hit.stepZ() != 0 ? -vel.z * 0.12 : vel.z * 0.3;
|
||||
return new Vec3(vx, Math.min(vel.y, 0.0) * 0.5, vz);
|
||||
}
|
||||
|
||||
private static void land(Drop d, Vec3 at, Vec3 vel, Surface surface, List<Splat> splats,
|
||||
List<Drop> children, long nowMs, Random rnd) {
|
||||
float speed = (float) vel.length();
|
||||
float radius = d.size * (2.2f + Math.min(speed, 0.8f) * 4.0f);
|
||||
splats.add(new Splat(at, radius, d.color, nowMs, rnd.nextLong(), surface.light(at), rnd.nextInt(40)));
|
||||
if (d.child || d.size <= 0.055f * d.scale || speed <= 0.25f) return;
|
||||
int n = 2 + rnd.nextInt(3);
|
||||
for (int k = 0; k < n; k++) {
|
||||
double a = rnd.nextDouble() * Math.PI * 2.0;
|
||||
double h = 0.03 + rnd.nextDouble() * 0.07;
|
||||
Vec3 v = new Vec3(Math.cos(a) * h + vel.x * 0.25, 0.05 + rnd.nextDouble() * 0.08, Math.sin(a) * h + vel.z * 0.25);
|
||||
children.add(new Drop(at.add(0, 0.02, 0), v, d.size * 0.35f, d.color, d.scale, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held;
|
||||
|
||||
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.modules.visuals.items.held.lua.HandScripts;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
|
|
@ -29,6 +31,11 @@ import net.minecraft.world.InteractionHand;
|
|||
public final class HoldMyItems extends Module {
|
||||
|
||||
private final HandAnimState handAnim = new HandAnimState();
|
||||
private HandScripts scripts;
|
||||
|
||||
/** Dynamics: Java-динамика; Scripts: Lua из config/lovisual/holdmyitems; Both: оба подряд. */
|
||||
public final ModeValue handMode =
|
||||
modeSetting("holdMyItemsMode", "mode", "Dynamics", "Dynamics", "Scripts", "Both");
|
||||
|
||||
public final NumberValue<Float> swayIntensity = num("sway_intensity", 1.0f, 0.0f, 2.0f);
|
||||
public final NumberValue<Float> easeStrength = num("ease_strength", 1.6f, 0.5f, 3.5f);
|
||||
|
|
@ -48,6 +55,20 @@ public final class HoldMyItems extends Module {
|
|||
public final NumberValue<Float> gripTurn = visibleWhen(num("grip_turn", 0.0f, -90.0f, 90.0f), gripEnabled::get);
|
||||
public final NumberValue<Float> gripRoll = visibleWhen(num("grip_roll", 0.0f, -180.0f, 180.0f), gripEnabled::get);
|
||||
|
||||
public boolean dynamicsActive() {
|
||||
return !"Scripts".equals(handMode.get());
|
||||
}
|
||||
|
||||
public boolean scriptsActive() {
|
||||
return !"Dynamics".equals(handMode.get());
|
||||
}
|
||||
|
||||
/** Рантайм Lua-скриптов; создаётся лениво, чтобы LuaJ не грузился в режиме Dynamics. */
|
||||
public HandScripts scripts() {
|
||||
if (scripts == null) scripts = new HandScripts();
|
||||
return scripts;
|
||||
}
|
||||
|
||||
public HandAnimState handAnim() {
|
||||
return handAnim;
|
||||
}
|
||||
|
|
@ -71,6 +92,7 @@ public final class HoldMyItems extends Module {
|
|||
@Override
|
||||
public void onEnable() {
|
||||
handAnim.reset();
|
||||
if (scripts != null) scripts.reload();
|
||||
}
|
||||
|
||||
/** Перекладывает линейный equip-прогресс через ease-out-back с настройками модуля. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Контекст кадра руки, который видят Lua-скрипты (как `context` в HoldMyItems). */
|
||||
public record HandFrame(
|
||||
Player player,
|
||||
ItemStack stack,
|
||||
boolean mainHand,
|
||||
boolean rightArm,
|
||||
float swingProgress,
|
||||
float equipProgress,
|
||||
float tickDelta,
|
||||
PoseStack pose
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.Easings;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.EntityApi;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.LuaTables;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.PoseApi;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.LuaSandbox;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.ScriptSlot;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Скрипты руки: {@code hand_pose.lua} (поза всей руки) и {@code item_pose.lua} (поза предмета).
|
||||
* Источник: {@code config/lovisual/holdmyitems/<name>.lua} (горячая перезагрузка по mtime раз в
|
||||
* секунду), иначе встроенный пример из ресурсов мода. Ошибка скрипта пишется в лог один раз и
|
||||
* отключает только этот скрипт до правки файла.
|
||||
*/
|
||||
public final class HandScripts {
|
||||
|
||||
public static final String HAND_POSE = "hand_pose";
|
||||
public static final String ITEM_POSE = "item_pose";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HandScripts.class);
|
||||
private static final long RECHECK_MS = 1000L;
|
||||
|
||||
private final LuaSandbox sandbox = new LuaSandbox();
|
||||
private final PoseApi pose = new PoseApi();
|
||||
private final long[] stamps = {Long.MIN_VALUE, Long.MIN_VALUE};
|
||||
private final ScriptSlot[] slots = new ScriptSlot[2];
|
||||
private final float[][] lastState = new float[2][2];
|
||||
private long lastCheck;
|
||||
private Player player;
|
||||
private ItemStack stack = ItemStack.EMPTY;
|
||||
|
||||
public HandScripts() {
|
||||
Globals g = sandbox.globals();
|
||||
g.set("M", pose.table());
|
||||
g.set("P", EntityApi.player(() -> player));
|
||||
g.set("I", EntityApi.item(() -> stack));
|
||||
LuaTable easings = new LuaTable();
|
||||
Easings.all().forEach((name, f) -> LuaTables.put(easings, name, a -> LuaValue.valueOf(f.applyAsDouble(LuaTables.num(a, 2, 0)))));
|
||||
g.set("Easings", easings);
|
||||
}
|
||||
|
||||
/** Применяет hand_pose к матрице руки; true, если был pushPose и нужен popPose. */
|
||||
public static boolean applyHandPose(HoldMyItems module, AbstractClientPlayer player, InteractionHand hand,
|
||||
float swing, float equip, float tickDelta, ItemStack item, PoseStack ms) {
|
||||
if (!module.scriptsActive()) return false;
|
||||
boolean mainHand = hand == InteractionHand.MAIN_HAND;
|
||||
boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand;
|
||||
ms.pushPose();
|
||||
module.scripts().runHand(new HandFrame(player, item, mainHand, rightArm, swing, equip, tickDelta, ms));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Применяет item_pose к матрице предмета; true, если был pushPose и нужен popPose. */
|
||||
public static boolean applyItemPose(HoldMyItems module, LivingEntity entity, ItemStack stack,
|
||||
ItemDisplayContext ctx, PoseStack ms) {
|
||||
if (!ctx.firstPerson() || !(entity instanceof AbstractClientPlayer player)) return false;
|
||||
if (!module.isEnabled() || !module.scriptsActive()) return false;
|
||||
boolean rightArm = ctx == ItemDisplayContext.FIRST_PERSON_RIGHT_HAND;
|
||||
boolean mainHand = (player.getMainArm() == HumanoidArm.RIGHT) == rightArm;
|
||||
HandScripts scripts = module.scripts();
|
||||
float[] last = scripts.lastState(mainHand);
|
||||
ms.pushPose();
|
||||
scripts.runItem(new HandFrame(player, stack, mainHand, rightArm, last[0], last[1], 0.0f, ms));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Path scriptDir() {
|
||||
return FabricLoader.getInstance().getConfigDir().resolve("lovisual").resolve("holdmyitems");
|
||||
}
|
||||
|
||||
/** Принудительная перезагрузка на следующем кадре. */
|
||||
public void reload() {
|
||||
stamps[0] = Long.MIN_VALUE;
|
||||
stamps[1] = Long.MIN_VALUE;
|
||||
lastCheck = 0L;
|
||||
}
|
||||
|
||||
public void runHand(HandFrame f) {
|
||||
run(0, HAND_POSE, f);
|
||||
lastState[f.mainHand() ? 0 : 1][0] = f.swingProgress();
|
||||
lastState[f.mainHand() ? 0 : 1][1] = f.equipProgress();
|
||||
}
|
||||
|
||||
public void runItem(HandFrame f) {
|
||||
run(1, ITEM_POSE, f);
|
||||
}
|
||||
|
||||
/** Последний известный swing/equip руки: item_pose рендерится вне submitArmWithItem. */
|
||||
public float[] lastState(boolean mainHand) {
|
||||
return lastState[mainHand ? 0 : 1];
|
||||
}
|
||||
|
||||
private void run(int index, String name, HandFrame f) {
|
||||
refresh();
|
||||
ScriptSlot slot = slots[index];
|
||||
if (slot == null || !slot.canRun()) return;
|
||||
this.player = f.player();
|
||||
this.stack = f.stack();
|
||||
Globals g = sandbox.globals();
|
||||
g.set("registry", slot.registryTable());
|
||||
g.set("mainHand", LuaValue.valueOf(f.mainHand()));
|
||||
g.set("rightArm", LuaValue.valueOf(f.rightArm()));
|
||||
g.set("swingProgress", LuaValue.valueOf(f.swingProgress()));
|
||||
g.set("equipProgress", LuaValue.valueOf(f.equipProgress()));
|
||||
g.set("tickDelta", LuaValue.valueOf(f.tickDelta()));
|
||||
g.set("usingItem", LuaValue.valueOf(f.player().isUsingItem()));
|
||||
g.set("useDuration", LuaValue.valueOf(f.player().getTicksUsingItem()));
|
||||
g.set("time", LuaValue.valueOf(System.nanoTime() / 1.0e9));
|
||||
pose.bind(f.pose());
|
||||
try {
|
||||
slot.run();
|
||||
} finally {
|
||||
pose.finish();
|
||||
}
|
||||
if (!slot.canRun()) LOG.warn("HoldMyItems script {} disabled: {}", name, slot.error());
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastCheck < RECHECK_MS) return;
|
||||
lastCheck = now;
|
||||
String[] names = {HAND_POSE, ITEM_POSE};
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
Path file = scriptDir().resolve(names[i] + ".lua");
|
||||
long stamp = stampOf(file);
|
||||
if (stamp == stamps[i]) continue;
|
||||
stamps[i] = stamp;
|
||||
String source = read(file, names[i]);
|
||||
slots[i] = source == null ? null : new ScriptSlot(names[i], source, sandbox);
|
||||
if (slots[i] != null && !slots[i].canRun()) LOG.warn("HoldMyItems script {}: {}", names[i], slots[i].error());
|
||||
}
|
||||
}
|
||||
|
||||
private static long stampOf(Path file) {
|
||||
try {
|
||||
return Files.isRegularFile(file) ? Files.getLastModifiedTime(file).toMillis() : -1L;
|
||||
} catch (IOException e) {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
private static String read(Path file, String name) {
|
||||
try {
|
||||
if (Files.isRegularFile(file)) return Files.readString(file, StandardCharsets.UTF_8);
|
||||
try (InputStream in = HandScripts.class.getResourceAsStream("/assets/lovisual/holdmyitems/" + name + ".lua")) {
|
||||
return in == null ? null : new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Cannot read HoldMyItems script {}: {}", name, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api;
|
||||
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
/** Функции плавности для Lua-скриптов (`Easings:easeOutBack(t)`), t в [0,1]. */
|
||||
public final class Easings {
|
||||
|
||||
private static final double C1 = 1.70158;
|
||||
private static final double C3 = C1 + 1.0;
|
||||
|
||||
private Easings() {
|
||||
}
|
||||
|
||||
public static double outQuad(double t) {
|
||||
return 1 - (1 - t) * (1 - t);
|
||||
}
|
||||
|
||||
public static double inOutCubic(double t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
public static double outCubic(double t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
public static double inOutSine(double t) {
|
||||
return -(Math.cos(Math.PI * t) - 1) / 2;
|
||||
}
|
||||
|
||||
public static double outBack(double t) {
|
||||
return 1 + C3 * Math.pow(t - 1, 3) + C1 * Math.pow(t - 1, 2);
|
||||
}
|
||||
|
||||
public static double inOutBack(double t) {
|
||||
double c2 = C1 * 1.525;
|
||||
return t < 0.5
|
||||
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
|
||||
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
|
||||
}
|
||||
|
||||
public static double outElastic(double t) {
|
||||
if (t <= 0 || t >= 1) return Math.max(0, Math.min(1, t));
|
||||
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
|
||||
}
|
||||
|
||||
public static double outBounce(double t) {
|
||||
double n = 7.5625, d = 2.75;
|
||||
if (t < 1 / d) return n * t * t;
|
||||
if (t < 2 / d) { t -= 1.5 / d; return n * t * t + 0.75; }
|
||||
if (t < 2.5 / d) { t -= 2.25 / d; return n * t * t + 0.9375; }
|
||||
t -= 2.625 / d;
|
||||
return n * t * t + 0.984375;
|
||||
}
|
||||
|
||||
/** Имя функции в Lua и сама функция. */
|
||||
public static java.util.Map<String, DoubleUnaryOperator> all() {
|
||||
java.util.Map<String, DoubleUnaryOperator> m = new java.util.LinkedHashMap<>();
|
||||
m.put("easeOutQuad", Easings::outQuad);
|
||||
m.put("easeInOutCubic", Easings::inOutCubic);
|
||||
m.put("easeOutCubic", Easings::outCubic);
|
||||
m.put("easeInOutSine", Easings::inOutSine);
|
||||
m.put("easeOutBack", Easings::outBack);
|
||||
m.put("easeInOutBack", Easings::inOutBack);
|
||||
m.put("easeOutElastic", Easings::outElastic);
|
||||
m.put("easeOutBounce", Easings::outBounce);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api;
|
||||
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.ItemUseAnimation;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Lua-глобалы {@code P} (игрок) и {@code I} (предмет текущей руки), только чтение. */
|
||||
public final class EntityApi {
|
||||
|
||||
private EntityApi() {
|
||||
}
|
||||
|
||||
public static LuaTable player(Supplier<Player> p) {
|
||||
LuaTable t = new LuaTable();
|
||||
LuaTables.put(t, "getSpeed", a -> LuaValue.valueOf(p.get().getDeltaMovement().horizontalDistance()));
|
||||
LuaTables.put(t, "getXSpeed", a -> LuaValue.valueOf(p.get().getDeltaMovement().x));
|
||||
LuaTables.put(t, "getYSpeed", a -> LuaValue.valueOf(p.get().getDeltaMovement().y));
|
||||
LuaTables.put(t, "getZSpeed", a -> LuaValue.valueOf(p.get().getDeltaMovement().z));
|
||||
LuaTables.put(t, "getYaw", a -> LuaValue.valueOf(p.get().getYRot()));
|
||||
LuaTables.put(t, "getPitch", a -> LuaValue.valueOf(p.get().getXRot()));
|
||||
LuaTables.put(t, "getAge", a -> LuaValue.valueOf(p.get().tickCount));
|
||||
LuaTables.put(t, "getHealth", a -> LuaValue.valueOf(p.get().getHealth()));
|
||||
LuaTables.put(t, "isOnGround", a -> LuaValue.valueOf(p.get().onGround()));
|
||||
LuaTables.put(t, "isSneaking", a -> LuaValue.valueOf(p.get().isShiftKeyDown()));
|
||||
LuaTables.put(t, "isSprinting", a -> LuaValue.valueOf(p.get().isSprinting()));
|
||||
LuaTables.put(t, "isSwimming", a -> LuaValue.valueOf(p.get().isSwimming()));
|
||||
LuaTables.put(t, "isUsingItem", a -> LuaValue.valueOf(p.get().isUsingItem()));
|
||||
LuaTables.put(t, "isTouchingWater", a -> LuaValue.valueOf(p.get().isInWater()));
|
||||
LuaTables.put(t, "isClimbing", a -> LuaValue.valueOf(p.get().onClimbable()));
|
||||
LuaTables.put(t, "hasVehicle", a -> LuaValue.valueOf(p.get().isPassenger()));
|
||||
return t;
|
||||
}
|
||||
|
||||
public static LuaTable item(Supplier<ItemStack> s) {
|
||||
LuaTable t = new LuaTable();
|
||||
LuaTables.put(t, "isEmpty", a -> LuaValue.valueOf(s.get().isEmpty()));
|
||||
LuaTables.put(t, "isOf", a -> LuaValue.valueOf(id(s.get()).equals(withNamespace(a.arg(2).tojstring()))));
|
||||
LuaTables.put(t, "getName", a -> LuaValue.valueOf(id(s.get())));
|
||||
LuaTables.put(t, "isBlock", a -> LuaValue.valueOf(s.get().getItem() instanceof net.minecraft.world.item.BlockItem));
|
||||
LuaTables.put(t, "isEnchanted", a -> LuaValue.valueOf(s.get().isEnchanted()));
|
||||
LuaTables.put(t, "isSword", a -> LuaValue.valueOf(s.get().is(ItemTags.SWORDS)));
|
||||
LuaTables.put(t, "getUseAction", a -> LuaValue.valueOf(useAction(s.get())));
|
||||
return t;
|
||||
}
|
||||
|
||||
static String id(ItemStack stack) {
|
||||
return BuiltInRegistries.ITEM.getKey(stack.getItem()).toString();
|
||||
}
|
||||
|
||||
static String withNamespace(String name) {
|
||||
return name.indexOf(':') >= 0 ? name : "minecraft:" + name;
|
||||
}
|
||||
|
||||
private static String useAction(ItemStack stack) {
|
||||
ItemUseAnimation anim = stack.getUseAnimation();
|
||||
return anim.name().toLowerCase(java.util.Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api;
|
||||
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
import org.luaj.vm2.lib.VarArgFunction;
|
||||
|
||||
/** Сборка Lua-таблиц из Java-лямбд. Вызовы `T:name(x)` приходят с self в arg(1). */
|
||||
public final class LuaTables {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Fn {
|
||||
LuaValue call(Varargs args);
|
||||
}
|
||||
|
||||
private LuaTables() {
|
||||
}
|
||||
|
||||
public static void put(LuaTable table, String name, Fn fn) {
|
||||
table.set(name, new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
return fn.call(args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Число-аргумент по индексу (учитывая self в arg(1)); отсутствует = def. */
|
||||
public static double num(Varargs a, int i, double def) {
|
||||
return a.arg(i).isnil() ? def : a.arg(i).todouble();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
/**
|
||||
* Lua-глобал {@code M}: преобразования матрицы руки/предмета (moveX/Y/Z, rotateX/Y/Z с pivot,
|
||||
* scale, push/pop) и математика. PoseStack подставляется на каждый вызов скрипта; лишние push
|
||||
* закрываются {@link #finish()}, лишние pop игнорируются.
|
||||
*/
|
||||
public final class PoseApi {
|
||||
|
||||
private PoseStack stack;
|
||||
private int depth;
|
||||
|
||||
public PoseApi bind(PoseStack stack) {
|
||||
this.stack = stack;
|
||||
this.depth = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Закрывает push, оставленные скриптом незакрытыми. */
|
||||
public void finish() {
|
||||
while (stack != null && depth > 0) {
|
||||
stack.popPose();
|
||||
depth--;
|
||||
}
|
||||
stack = null;
|
||||
}
|
||||
|
||||
private void rotate(Axis axis, double deg, double px, double py, double pz) {
|
||||
if (stack == null) return;
|
||||
stack.translate((float) px, (float) py, (float) pz);
|
||||
stack.mulPose(axis.rotationDegrees((float) deg));
|
||||
stack.translate((float) -px, (float) -py, (float) -pz);
|
||||
}
|
||||
|
||||
public LuaTable table() {
|
||||
LuaTable t = new LuaTable();
|
||||
LuaTables.put(t, "moveX", a -> move(LuaTables.num(a, 2, 0), 0, 0));
|
||||
LuaTables.put(t, "moveY", a -> move(0, LuaTables.num(a, 2, 0), 0));
|
||||
LuaTables.put(t, "moveZ", a -> move(0, 0, LuaTables.num(a, 2, 0)));
|
||||
LuaTables.put(t, "translate", a -> move(LuaTables.num(a, 2, 0), LuaTables.num(a, 3, 0), LuaTables.num(a, 4, 0)));
|
||||
LuaTables.put(t, "rotateX", a -> rot(Axis.XP, a));
|
||||
LuaTables.put(t, "rotateY", a -> rot(Axis.YP, a));
|
||||
LuaTables.put(t, "rotateZ", a -> rot(Axis.ZP, a));
|
||||
LuaTables.put(t, "scale", a -> {
|
||||
if (stack != null) {
|
||||
double x = LuaTables.num(a, 2, 1);
|
||||
stack.scale((float) x, (float) LuaTables.num(a, 3, x), (float) LuaTables.num(a, 4, x));
|
||||
}
|
||||
return LuaValue.NIL;
|
||||
});
|
||||
LuaTables.put(t, "push", a -> {
|
||||
if (stack != null) { stack.pushPose(); depth++; }
|
||||
return LuaValue.NIL;
|
||||
});
|
||||
LuaTables.put(t, "pop", a -> {
|
||||
if (stack != null && depth > 0) { stack.popPose(); depth--; }
|
||||
return LuaValue.NIL;
|
||||
});
|
||||
LuaTables.put(t, "sin", a -> LuaValue.valueOf(Math.sin(LuaTables.num(a, 2, 0))));
|
||||
LuaTables.put(t, "cos", a -> LuaValue.valueOf(Math.cos(LuaTables.num(a, 2, 0))));
|
||||
LuaTables.put(t, "abs", a -> LuaValue.valueOf(Math.abs(LuaTables.num(a, 2, 0))));
|
||||
LuaTables.put(t, "round", a -> LuaValue.valueOf((double) Math.round(LuaTables.num(a, 2, 0))));
|
||||
LuaTables.put(t, "floor", a -> LuaValue.valueOf(Math.floor(LuaTables.num(a, 2, 0))));
|
||||
LuaTables.put(t, "ceil", a -> LuaValue.valueOf(Math.ceil(LuaTables.num(a, 2, 0))));
|
||||
LuaTables.put(t, "pow", a -> LuaValue.valueOf(Math.pow(LuaTables.num(a, 2, 0), LuaTables.num(a, 3, 1))));
|
||||
LuaTables.put(t, "clamp", a -> LuaValue.valueOf(
|
||||
Math.max(LuaTables.num(a, 3, 0), Math.min(LuaTables.num(a, 4, 1), LuaTables.num(a, 2, 0)))));
|
||||
LuaTables.put(t, "lerp", a -> {
|
||||
double from = LuaTables.num(a, 2, 0);
|
||||
return LuaValue.valueOf(from + (LuaTables.num(a, 3, 0) - from) * LuaTables.num(a, 4, 0));
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
private LuaValue move(double x, double y, double z) {
|
||||
if (stack != null) stack.translate((float) x, (float) y, (float) z);
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
|
||||
private LuaValue rot(Axis axis, org.luaj.vm2.Varargs a) {
|
||||
rotate(axis, LuaTables.num(a, 2, 0), LuaTables.num(a, 3, 0), LuaTables.num(a, 4, 0), LuaTables.num(a, 5, 0));
|
||||
return LuaValue.NIL;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Препроцессор скриптов: строка {@code global.x = v;} становится локальной переменной,
|
||||
* которая берётся из {@code registry} (живёт между кадрами), а в конец скрипта дописывается
|
||||
* сохранение значений обратно. Так скрипту не нужны собственные глобальные таблицы.
|
||||
*/
|
||||
public final class LuaPreprocessor {
|
||||
|
||||
private static final Pattern GLOBAL_DECL =
|
||||
Pattern.compile("(?m)^[ \\t]*global\\.([A-Za-z_][A-Za-z0-9_]*)[ \\t]*=[ \\t]*(.+?)[ \\t]*;?[ \\t]*$");
|
||||
|
||||
private LuaPreprocessor() {
|
||||
}
|
||||
|
||||
public static String process(String source) {
|
||||
List<String> names = new ArrayList<>();
|
||||
Matcher m = GLOBAL_DECL.matcher(source);
|
||||
StringBuilder out = new StringBuilder();
|
||||
while (m.find()) {
|
||||
String name = m.group(1);
|
||||
names.add(name);
|
||||
String line = "local " + name + " = registry:getOrDefault('" + name + "', " + m.group(2) + ")";
|
||||
m.appendReplacement(out, Matcher.quoteReplacement(line));
|
||||
}
|
||||
m.appendTail(out);
|
||||
for (String name : names) {
|
||||
out.append("\nregistry:put('").append(name).append("', ").append(name).append(')');
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime;
|
||||
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.luaj.vm2.LoadState;
|
||||
import org.luaj.vm2.LuaError;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.Varargs;
|
||||
import org.luaj.vm2.compiler.LuaC;
|
||||
import org.luaj.vm2.lib.BaseLib;
|
||||
import org.luaj.vm2.lib.Bit32Lib;
|
||||
import org.luaj.vm2.lib.DebugLib;
|
||||
import org.luaj.vm2.lib.PackageLib;
|
||||
import org.luaj.vm2.lib.StringLib;
|
||||
import org.luaj.vm2.lib.TableLib;
|
||||
import org.luaj.vm2.lib.VarArgFunction;
|
||||
import org.luaj.vm2.lib.jse.JseMathLib;
|
||||
|
||||
/**
|
||||
* Песочница LuaJ: только base/bit32/table/string/math. Нет io, os, luajava, package, файловых
|
||||
* загрузчиков и debug; Java-объекты в Lua не отдаются вообще (API собирается из таблиц с функциями).
|
||||
* Бесконечные циклы обрезаются счётчиком инструкций через хук, который ставится один раз.
|
||||
*/
|
||||
public final class LuaSandbox {
|
||||
|
||||
/** Шаг хука в инструкциях и допустимое число шагов на один запуск (≈ 400k инструкций). */
|
||||
private static final int HOOK_STEP = 1000;
|
||||
private static final int HOOK_LIMIT = 400;
|
||||
private static final String[] REMOVED = {"package", "require", "dofile", "loadfile", "load", "loadstring", "debug", "collectgarbage"};
|
||||
|
||||
private final Globals globals;
|
||||
private int budget;
|
||||
|
||||
public LuaSandbox() {
|
||||
Globals g = new Globals();
|
||||
g.load(new BaseLib());
|
||||
g.load(new PackageLib());
|
||||
g.load(new Bit32Lib());
|
||||
g.load(new TableLib());
|
||||
g.load(new StringLib());
|
||||
g.load(new JseMathLib());
|
||||
g.load(new DebugLib());
|
||||
LoadState.install(g);
|
||||
LuaC.install(g);
|
||||
LuaValue hook = new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs args) {
|
||||
if (++budget > HOOK_LIMIT) throw new LuaError("script is too slow (instruction limit)");
|
||||
return NONE;
|
||||
}
|
||||
};
|
||||
g.get("debug").get("sethook").invoke(LuaValue.varargsOf(
|
||||
new LuaValue[]{hook, LuaValue.valueOf(""), LuaValue.valueOf(HOOK_STEP)}));
|
||||
for (String name : REMOVED) g.set(name, LuaValue.NIL);
|
||||
this.globals = g;
|
||||
}
|
||||
|
||||
public Globals globals() {
|
||||
return globals;
|
||||
}
|
||||
|
||||
/** Сбрасывает бюджет инструкций перед очередным запуском скрипта. */
|
||||
public void beginRun() {
|
||||
budget = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime;
|
||||
|
||||
import org.luaj.vm2.LuaError;
|
||||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.VarArgFunction;
|
||||
import org.luaj.vm2.Varargs;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Один загруженный скрипт: скомпилированный чанк, свой {@code registry} и флаг «упал».
|
||||
* После ошибки скрипт не запускается, пока его не перезагрузят (правка файла или реинициализация).
|
||||
*/
|
||||
public final class ScriptSlot {
|
||||
|
||||
private final String name;
|
||||
private final LuaSandbox sandbox;
|
||||
private final Map<String, LuaValue> store = new HashMap<>();
|
||||
private final LuaTable registry = buildRegistry();
|
||||
private LuaValue chunk;
|
||||
private String error;
|
||||
|
||||
public ScriptSlot(String name, String source, LuaSandbox sandbox) {
|
||||
this.name = name;
|
||||
this.sandbox = sandbox;
|
||||
try {
|
||||
this.chunk = sandbox.globals().load(LuaPreprocessor.process(source), name);
|
||||
} catch (LuaError e) {
|
||||
this.error = e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String error() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public boolean canRun() {
|
||||
return chunk != null && error == null;
|
||||
}
|
||||
|
||||
/** Таблица {@code registry} для глобалов скрипта: getOrDefault/put. */
|
||||
public LuaTable registryTable() {
|
||||
return registry;
|
||||
}
|
||||
|
||||
private LuaTable buildRegistry() {
|
||||
LuaTable t = new LuaTable();
|
||||
t.set("getOrDefault", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs a) {
|
||||
LuaValue v = store.get(a.arg(2).tojstring());
|
||||
return v != null ? v : a.arg(3);
|
||||
}
|
||||
});
|
||||
t.set("put", new VarArgFunction() {
|
||||
@Override
|
||||
public Varargs invoke(Varargs a) {
|
||||
store.put(a.arg(2).tojstring(), a.arg(3));
|
||||
return NONE;
|
||||
}
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Запускает чанк; ошибка Lua блокирует скрипт и сохраняется в {@link #error()}. */
|
||||
public void run() {
|
||||
if (!canRun()) return;
|
||||
sandbox.beginRun();
|
||||
try {
|
||||
chunk.call();
|
||||
} catch (LuaError e) {
|
||||
error = e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +1,69 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.CircleAnimation;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.CircleColorMode;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.CircleRenderer;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.JumpCircle;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape.Shape;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||
import dev.loki.lovisual.render.engine.math.ColorMath;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.util.time.Timer;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.IntUnaryOperator;
|
||||
|
||||
//todo Description
|
||||
/** Draws an animated shape on the ground where a player left it by jumping. */
|
||||
@ModuleInfo(
|
||||
id = "jumpcircles",
|
||||
displayName = "JumpCircles",
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public class JumpCircles extends Module {
|
||||
private static final double SURFACE_LIFT = 0.02;
|
||||
|
||||
private final EnumValue<CircleMode> mode =
|
||||
enumSetting("jumpCirclesMode", "mode", CircleMode.DEFAULT);
|
||||
private final BooleanValue easeOut = bool("jumpCirclesEaseOut", "ease_out", true);
|
||||
private final NumberValue<Float> rotateSpeed = num("jumpCirclesRotateSpeed", "rotate_speed", 2f, 0.5f, 5f);
|
||||
private final NumberValue<Float> circleScale = common(
|
||||
num("jumpCirclesCircleScale", "circle_scale", 1f, 0.5f, 5f),
|
||||
enumSetting("jumpCirclesMode", "mode", CircleMode.CIRCLE);
|
||||
private final NumberValue<Float> radius = common(
|
||||
num("jumpCirclesCircleScale", "circle_scale", 1.5f, 0.5f, 5f),
|
||||
CommonSettingSchemas.RENDER_SCALE.commonI18nKey()
|
||||
);
|
||||
private final NumberValue<Float> pulseMaxSize = visibleWhen(num(
|
||||
"jumpCirclesPulseMaxSize", "pulse_max_size", 2.5f, 1.0f, 3.0f
|
||||
), this::isPulseMode);
|
||||
private final NumberValue<Integer> pulseLifetime = visibleWhen(num(
|
||||
"jumpCirclesPulseLifetime", "pulse_lifetime", 1000, 500, 5000
|
||||
), this::isPulseMode);
|
||||
private final NumberValue<Float> speed = num("jumpCirclesSpeed", "speed", 1f, 0.5f, 3f);
|
||||
private final NumberValue<Float> fadeSpeed = num("jumpCirclesFadeSpeed", "fade_speed", 1f, 0.5f, 3f);
|
||||
private final NumberValue<Float> rotateSpeed = num("jumpCirclesRotateSpeed", "rotate_speed", 1f, 0f, 5f);
|
||||
private final NumberValue<Float> thickness = visibleWhen(
|
||||
num("jumpCirclesThickness", "thickness", 0.25f, 0.08f, 0.5f), this::usesThickness);
|
||||
private final NumberValue<Integer> segments = visibleWhen(
|
||||
num("jumpCirclesSegments", "segments", 32, 12, 96), this::usesSegments);
|
||||
private final NumberValue<Integer> maxCircles = num("jumpCirclesMax", "max_circles", 12, 1, 32);
|
||||
private final BooleanValue onlySelf = bool("jumpCirclesOnlySelf", "only_self", false);
|
||||
private final BooleanValue depthTest = common(
|
||||
bool("jumpCirclesDepthTest", "depth_test", true),
|
||||
CommonSettingSchemas.RENDER_DEPTH_TEST.commonI18nKey()
|
||||
);
|
||||
private final EnumValue<ColorMode> colorMode =
|
||||
enumCommon("jumpCirclesColorMode", "color_mode",
|
||||
CommonSettingSchemas.RENDER_COLOR_MODE.commonI18nKey(), ColorMode.STATIC);
|
||||
private final EnumValue<CircleColorMode> colorMode =
|
||||
enumCommon("jumpCirclesCircleColorMode", "color_mode",
|
||||
CommonSettingSchemas.RENDER_COLOR_MODE.commonI18nKey(), CircleColorMode.STATIC);
|
||||
private final NumberValue<Integer> colorSpeed = common(
|
||||
num("jumpCirclesColorSpeed", "color_speed", 18, 2, 54),
|
||||
CommonSettingSchemas.RENDER_COLOR_SPEED.commonI18nKey()
|
||||
|
|
@ -66,9 +75,12 @@ public class JumpCircles extends Module {
|
|||
private final RGBAColorValue colorValue2 = visibleWhen(common(
|
||||
color("jumpCirclesColor2", "color2", "#FFFF5555"),
|
||||
CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey()
|
||||
), this::usesSecondaryColor);
|
||||
private final List<Circle> circles = new ArrayList<>();
|
||||
private final Map<UUID, Boolean> groundState = new HashMap<>();
|
||||
), () -> AnimatedRenderColors.usesSecondary(colorMode.get().animated()));
|
||||
|
||||
private final Deque<JumpCircle> circles = new ArrayDeque<>();
|
||||
private final Map<UUID, Double> lastGroundY = new HashMap<>();
|
||||
private final Set<UUID> seen = new HashSet<>();
|
||||
private final CircleRenderer circleRenderer = new CircleRenderer();
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
|
|
@ -80,33 +92,28 @@ public class JumpCircles extends Module {
|
|||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||
|
||||
boolean selfOnly = onlySelf.get();
|
||||
Set<UUID> active = new HashSet<>();
|
||||
|
||||
seen.clear();
|
||||
for (Player pl : mc.level.players()) {
|
||||
if (selfOnly && pl != mc.player) continue;
|
||||
if (onlySelf.get() && pl != mc.player) continue;
|
||||
UUID id = pl.getUUID();
|
||||
active.add(id);
|
||||
|
||||
boolean prevGround = groundState.getOrDefault(id, false);
|
||||
boolean nowGround = pl.onGround();
|
||||
if (prevGround && !nowGround) {
|
||||
circles.add(new Circle(
|
||||
new Vec3(pl.getX(), Math.floor(pl.getY()) + 0.001f, pl.getZ()),
|
||||
new Timer()
|
||||
));
|
||||
seen.add(id);
|
||||
if (pl.onGround()) {
|
||||
lastGroundY.put(id, pl.getY());
|
||||
} else {
|
||||
Double groundY = lastGroundY.remove(id);
|
||||
if (groundY != null) spawn(pl.getX(), groundY, pl.getZ());
|
||||
}
|
||||
groundState.put(id, nowGround);
|
||||
}
|
||||
|
||||
groundState.keySet().removeIf(id -> !active.contains(id));
|
||||
circles.removeIf(c -> c.timer.passedMs(lifetimeMs()));
|
||||
lastGroundY.keySet().retainAll(seen);
|
||||
long now = GameClock.millis();
|
||||
float lifetime = CircleAnimation.lifetimeMs(speed.get());
|
||||
circles.removeIf(c -> now - c.spawnMs() >= lifetime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
circles.clear();
|
||||
groundState.clear();
|
||||
lastGroundY.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -114,185 +121,78 @@ public class JumpCircles extends Module {
|
|||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!isEnabled() || mc.level == null || mc.player == null || circles.isEmpty()) return;
|
||||
|
||||
boolean useDepth = depthTest.get();
|
||||
RenderPipeline pipeline = useDepth
|
||||
? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE
|
||||
: LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE;
|
||||
Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN;
|
||||
MeshBuilder mesh = renderer.batchTextured(pipeline, resolveTexture(), depthMode);
|
||||
CircleMode circleMode = mode.get();
|
||||
Shape shape = circleMode.shape;
|
||||
MeshBuilder mesh = circleRenderer.openMesh(renderer, shape, circleMode.texture, depthTest.get());
|
||||
if (mesh == null) return;
|
||||
float nowMs = GameClock.millis() + tickDelta * GameClock.TICK_MS;
|
||||
float lifetime = CircleAnimation.lifetimeMs(speed.get());
|
||||
IntUnaryOperator palette = this::colorAt;
|
||||
for (JumpCircle c : circles) {
|
||||
float age = Math.max(0f, nowMs - c.spawnMs());
|
||||
float progress = CircleAnimation.progress(age, lifetime);
|
||||
float alpha = CircleAnimation.alpha(progress, fadeSpeed.get());
|
||||
if (progress >= 1f || alpha <= 0f) continue;
|
||||
|
||||
for (int i = circles.size() - 1; i >= 0; i--) {
|
||||
Circle c = circles.get(i);
|
||||
if (isPulseMode()) {
|
||||
renderPulseCircle(mesh, c);
|
||||
CircleRenderer.Pose pose = new CircleRenderer.Pose(c.x(), c.y(), c.z(),
|
||||
CircleAnimation.radius(progress, radius.get()),
|
||||
CircleAnimation.angle(age / 1000f, rotateSpeed.get(), progress),
|
||||
alpha, CircleAnimation.wavePhase(age / 1000f));
|
||||
if (shape == null) {
|
||||
circleRenderer.texturedQuad(mesh, pose, palette);
|
||||
} else {
|
||||
renderLegacyCircle(mesh, c);
|
||||
circleRenderer.shape(mesh, pose, shape, segments.get(), thickness.get(), palette);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLegacyCircle(MeshBuilder mesh, Circle c) {
|
||||
float colorAnim = c.timer.getPassedTimeMs() / 6000f;
|
||||
float t = (c.timer.getPassedTimeMs() * (easeOut.get() ? 2f : 1f)) / 5000f;
|
||||
float sizeAnim = circleScale.get() - (float) Math.pow(1f - t, 4f);
|
||||
if (sizeAnim <= 0f) return;
|
||||
|
||||
float angleDeg = sizeAnim * rotateSpeed.get() * 1000f;
|
||||
emitCircleQuad(mesh, c.pos, sizeAnim, angleDeg, 1f - colorAnim);
|
||||
private void spawn(double x, double groundY, double z) {
|
||||
circles.addLast(new JumpCircle(x, groundY + SURFACE_LIFT, z, GameClock.millis()));
|
||||
while (circles.size() > maxCircles.get()) {
|
||||
circles.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderPulseCircle(MeshBuilder mesh, Circle c) {
|
||||
float progress = Math.min(c.timer.getPassedTimeMs() / (float) pulseLifetime.get(), 1f);
|
||||
if (progress >= 1f) return;
|
||||
|
||||
float scale = bounceOut(progress) * pulseMaxSize.get() * circleScale.get();
|
||||
if (scale <= 0f) return;
|
||||
|
||||
float alpha = pulseAlpha(progress);
|
||||
emitCircleQuad(mesh, c.pos, scale * 0.5f, 0f, alpha);
|
||||
private boolean usesThickness() {
|
||||
return mode.get().shape != null && mode.get().shape.usesThickness();
|
||||
}
|
||||
|
||||
private void emitCircleQuad(MeshBuilder mesh, Vec3 center, float half, float angleDeg, float alpha) {
|
||||
double radians = Math.toRadians(angleDeg);
|
||||
float cos = (float) Math.cos(radians);
|
||||
float sin = (float) Math.sin(radians);
|
||||
|
||||
Vec3 right = new Vec3(cos, 0.0, -sin);
|
||||
Vec3 forward = new Vec3(sin, 0.0, cos);
|
||||
|
||||
int c1 = ColorMath.scaleAlpha(getColorArgb(270), alpha);
|
||||
int c2 = ColorMath.scaleAlpha(getColorArgb(0), alpha);
|
||||
int c3 = ColorMath.scaleAlpha(getColorArgb(180), alpha);
|
||||
int c4 = ColorMath.scaleAlpha(getColorArgb(90), alpha);
|
||||
|
||||
Vec3 p1 = center.add(right.scale(-half)).add(forward.scale(half));
|
||||
Vec3 p2 = center.add(right.scale(half)).add(forward.scale(half));
|
||||
Vec3 p3 = center.add(right.scale(half)).add(forward.scale(-half));
|
||||
Vec3 p4 = center.add(right.scale(-half)).add(forward.scale(-half));
|
||||
|
||||
mesh.ensureQuadCapacity();
|
||||
int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 1).color(new RenderColor(c1)).next();
|
||||
int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 1).color(new RenderColor(c2)).next();
|
||||
int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 0).color(new RenderColor(c3)).next();
|
||||
int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 0).color(new RenderColor(c4)).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
private boolean usesSegments() {
|
||||
return mode.get().shape != null && mode.get().shape.usesSegments();
|
||||
}
|
||||
|
||||
private boolean usesSecondaryColor() {
|
||||
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
||||
}
|
||||
|
||||
private boolean isPulseMode() {
|
||||
return mode.get() == CircleMode.PULSE;
|
||||
}
|
||||
|
||||
private long lifetimeMs() {
|
||||
return isPulseMode() ? pulseLifetime.get() : (easeOut.get() ? 5000L : 6000L);
|
||||
}
|
||||
|
||||
private int getColorArgb(int count) {
|
||||
private int colorAt(int phase) {
|
||||
return AnimatedRenderColors.resolve(
|
||||
animatedColorMode(),
|
||||
colorSpeed.get(),
|
||||
count,
|
||||
colorValue.getArgb(),
|
||||
colorValue2.getArgb()
|
||||
);
|
||||
colorMode.get().animated(), colorSpeed.get(), phase, colorValue.getArgb(), colorValue2.getArgb());
|
||||
}
|
||||
|
||||
private AnimatedRenderColors.Mode animatedColorMode() {
|
||||
return switch (colorMode.get()) {
|
||||
case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW;
|
||||
case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
|
||||
case SKY -> AnimatedRenderColors.Mode.SKY;
|
||||
case FADE -> AnimatedRenderColors.Mode.FADE;
|
||||
case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
|
||||
case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS;
|
||||
case THEME -> AnimatedRenderColors.Mode.THEME;
|
||||
case STATIC -> AnimatedRenderColors.Mode.STATIC;
|
||||
};
|
||||
}
|
||||
private enum CircleMode implements EnumValue.AliasProvider {
|
||||
CIRCLE(TextureStorage.DEFAULT_CIRCLE, null),
|
||||
RING(TextureStorage.JUMP_CIRCLE, null),
|
||||
BUBBLE(TextureStorage.BUBBLE, null),
|
||||
STAR(null, Shape.STAR),
|
||||
DIAMOND(null, Shape.DIAMOND),
|
||||
SPIRAL(null, Shape.SPIRAL),
|
||||
HEXAGON(null, Shape.HEXAGON),
|
||||
WAVE(null, Shape.WAVE),
|
||||
CROSS(null, Shape.CROSS),
|
||||
PETALS(null, Shape.PETALS);
|
||||
|
||||
private Identifier resolveTexture() {
|
||||
return switch (mode.get()) {
|
||||
case BUBBLE -> TextureStorage.BUBBLE;
|
||||
case PULSE -> TextureStorage.JUMP_CIRCLE;
|
||||
case DEFAULT -> TextureStorage.DEFAULT_CIRCLE;
|
||||
};
|
||||
}
|
||||
private final Identifier texture;
|
||||
private final Shape shape;
|
||||
|
||||
private float pulseAlpha(float progress) {
|
||||
final float fadeInDuration = 0.15f;
|
||||
final float glowStart = 0.65f;
|
||||
final float fadeOutStart = 0.85f;
|
||||
float alpha;
|
||||
|
||||
if (progress < fadeInDuration) {
|
||||
alpha = progress / fadeInDuration;
|
||||
} else if (progress >= fadeOutStart) {
|
||||
float fadeOutProgress = (progress - fadeOutStart) / (1f - fadeOutStart);
|
||||
alpha = 1f - fadeOutProgress;
|
||||
if (progress > glowStart) {
|
||||
float glowProgress = (progress - glowStart) / (fadeOutStart - glowStart);
|
||||
float glowPulse = (float) (Math.sin(glowProgress * Math.PI * 3) * 0.3 + 0.3);
|
||||
alpha += glowPulse * (1f - fadeOutProgress);
|
||||
}
|
||||
} else if (progress > glowStart) {
|
||||
float glowProgress = (progress - glowStart) / (fadeOutStart - glowStart);
|
||||
float glowPulse = (float) (Math.sin(glowProgress * Math.PI * 3) * 0.3 + 0.3);
|
||||
alpha = 1f + glowPulse;
|
||||
} else {
|
||||
alpha = 1f;
|
||||
CircleMode(Identifier texture, Shape shape) {
|
||||
this.texture = texture;
|
||||
this.shape = shape;
|
||||
}
|
||||
|
||||
return Math.max(0f, Math.min(1f, alpha));
|
||||
}
|
||||
|
||||
private float bounceOut(float value) {
|
||||
float n1 = 7.5625f;
|
||||
float d1 = 2.75f;
|
||||
if (value < 1.0f / d1) {
|
||||
return n1 * value * value;
|
||||
} else if (value < 2.0f / d1) {
|
||||
value -= 1.5f / d1;
|
||||
return n1 * value * value + 0.75f;
|
||||
} else if (value < 2.5f / d1) {
|
||||
value -= 2.25f / d1;
|
||||
return n1 * value * value + 0.9375f;
|
||||
} else {
|
||||
value -= 2.625f / d1;
|
||||
return n1 * value * value + 0.984375f;
|
||||
}
|
||||
}
|
||||
|
||||
private enum CircleMode {
|
||||
DEFAULT,
|
||||
BUBBLE,
|
||||
PULSE
|
||||
}
|
||||
|
||||
private enum ColorMode implements EnumValue.AliasProvider {
|
||||
STATIC,
|
||||
RAINBOW,
|
||||
LIGHT_RAINBOW,
|
||||
SKY,
|
||||
FADE,
|
||||
DOUBLE_COLOR,
|
||||
ANALOGOUS,
|
||||
THEME;
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return switch (this) {
|
||||
case LIGHT_RAINBOW -> List.of("LightRainbow");
|
||||
case DOUBLE_COLOR -> List.of("DoubleColor");
|
||||
case THEME -> List.of("Theme");
|
||||
case CIRCLE -> List.of("Default");
|
||||
case RING -> List.of("Pulse");
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private record Circle(Vec3 pos, Timer timer) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles;
|
||||
|
||||
/** Pure timing and easing maths for a single jump circle; no Minecraft dependencies. */
|
||||
public final class CircleAnimation {
|
||||
public static final float BASE_LIFETIME_MS = 2400f;
|
||||
private static final float TWO_PI = (float) (Math.PI * 2.0);
|
||||
private static final float FADE_IN = 0.08f;
|
||||
private static final float SCALE_PULSE = 0.04f;
|
||||
private static final float ALPHA_PULSE = 0.15f;
|
||||
private static final float SWAY = 0.15f;
|
||||
private static final float SPIN_PER_SECOND = 0.8f;
|
||||
private static final float WAVE_RADIANS_PER_SECOND = 3f;
|
||||
|
||||
private CircleAnimation() {
|
||||
}
|
||||
|
||||
public static float lifetimeMs(float speed) {
|
||||
return BASE_LIFETIME_MS / Math.max(0.1f, speed);
|
||||
}
|
||||
|
||||
public static float progress(float ageMs, float lifetimeMs) {
|
||||
return clamp01(ageMs / lifetimeMs);
|
||||
}
|
||||
|
||||
public static float easeOutCubic(float p) {
|
||||
float inv = 1f - clamp01(p);
|
||||
return 1f - inv * inv * inv;
|
||||
}
|
||||
|
||||
/** Eased growth to {@code maxRadius} with a slight breathing; never exceeds {@code maxRadius}. */
|
||||
public static float radius(float progress, float maxRadius) {
|
||||
float breathe = 0.5f + 0.5f * (float) Math.sin(progress * TWO_PI * 4f);
|
||||
return maxRadius * easeOutCubic(progress) * (1f - SCALE_PULSE + SCALE_PULSE * breathe);
|
||||
}
|
||||
|
||||
/** Short fade-in, then a fade-out whose steepness grows with {@code fadeSpeed}; result is in [0,1]. */
|
||||
public static float alpha(float progress, float fadeSpeed) {
|
||||
float in = clamp01(progress / FADE_IN);
|
||||
float out = clamp01((1f - progress) * Math.max(0f, fadeSpeed));
|
||||
float flicker = 1f - ALPHA_PULSE + ALPHA_PULSE * (0.5f + 0.5f * (float) Math.sin(progress * TWO_PI * 5f));
|
||||
return clamp01(in * out * flicker);
|
||||
}
|
||||
|
||||
/** Steady spin over time plus a small sway driven by progress, in radians. */
|
||||
public static float angle(float ageSeconds, float rotateSpeed, float progress) {
|
||||
return ageSeconds * rotateSpeed * SPIN_PER_SECOND + SWAY * (float) Math.sin(progress * TWO_PI * 2f);
|
||||
}
|
||||
|
||||
public static float wavePhase(float ageSeconds) {
|
||||
return ageSeconds * WAVE_RADIANS_PER_SECOND;
|
||||
}
|
||||
|
||||
private static float clamp01(float v) {
|
||||
return Math.max(0f, Math.min(1f, v));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles;
|
||||
|
||||
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Colour source options for JumpCircles, mapped onto the shared animated render colours. */
|
||||
public enum CircleColorMode implements EnumValue.AliasProvider {
|
||||
STATIC(AnimatedRenderColors.Mode.STATIC),
|
||||
RAINBOW(AnimatedRenderColors.Mode.RAINBOW),
|
||||
LIGHT_RAINBOW(AnimatedRenderColors.Mode.LIGHT_RAINBOW),
|
||||
SKY(AnimatedRenderColors.Mode.SKY),
|
||||
FADE(AnimatedRenderColors.Mode.FADE),
|
||||
DOUBLE_COLOR(AnimatedRenderColors.Mode.DOUBLE_COLOR),
|
||||
ANALOGOUS(AnimatedRenderColors.Mode.ANALOGOUS),
|
||||
THEME(AnimatedRenderColors.Mode.THEME);
|
||||
|
||||
private final AnimatedRenderColors.Mode animated;
|
||||
|
||||
CircleColorMode(AnimatedRenderColors.Mode animated) {
|
||||
this.animated = animated;
|
||||
}
|
||||
|
||||
public AnimatedRenderColors.Mode animated() {
|
||||
return animated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return switch (this) {
|
||||
case LIGHT_RAINBOW -> List.of("LightRainbow");
|
||||
case DOUBLE_COLOR -> List.of("DoubleColor");
|
||||
case THEME -> List.of("Theme");
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles;
|
||||
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape.Shape;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape.ShapeGeometry;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape.ShapeMesh;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.function.IntUnaryOperator;
|
||||
|
||||
/** Writes jump circles into world meshes: coloured shape triangles or one textured quad. */
|
||||
public final class CircleRenderer {
|
||||
private static final float OUTLINE_BRIGHTNESS = 0.35f;
|
||||
private final ShapeMesh shapeMesh = new ShapeMesh();
|
||||
|
||||
/** Where and how a circle is drawn this frame; angle and wavePhase are in radians. */
|
||||
public record Pose(double x, double y, double z, float radius, float angle, float alpha, float wavePhase) {
|
||||
}
|
||||
|
||||
/** Additive batch for shapes (coloured) or for the texture when {@code shape} is null; depth mask is never written. */
|
||||
public MeshBuilder openMesh(Renderer3D renderer, Shape shape, Identifier texture, boolean depthTest) {
|
||||
Renderer3D.DepthMode depthMode = depthTest ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN;
|
||||
if (shape != null) {
|
||||
RenderPipeline pipeline = depthTest
|
||||
? LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH
|
||||
: LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE;
|
||||
return renderer.batch(pipeline, depthMode);
|
||||
}
|
||||
RenderPipeline pipeline = depthTest
|
||||
? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE
|
||||
: LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE;
|
||||
return renderer.batchTextured(pipeline, texture, depthMode);
|
||||
}
|
||||
|
||||
/** {@code palette} maps a 0..360 gradient phase to ARGB. */
|
||||
public void shape(MeshBuilder mesh, Pose pose, Shape shape, int segments, float thickness,
|
||||
IntUnaryOperator palette) {
|
||||
ShapeGeometry.build(shape, shapeMesh, segments, thickness, pose.wavePhase());
|
||||
int vertices = shapeMesh.vertexCount();
|
||||
int indices = shapeMesh.indexCount();
|
||||
mesh.ensureCapacity(vertices, indices);
|
||||
float cos = (float) Math.cos(pose.angle());
|
||||
float sin = (float) Math.sin(pose.angle());
|
||||
int base = -1;
|
||||
for (int i = 0; i < vertices; i++) {
|
||||
float lx = shapeMesh.x(i) * pose.radius();
|
||||
float lz = shapeMesh.z(i) * pose.radius();
|
||||
int main = palette.applyAsInt(Math.round(shapeMesh.gradient(i) * 360f));
|
||||
int argb = AnimatedRenderColors.mixArgb(main,
|
||||
AnimatedRenderColors.brighten(main, OUTLINE_BRIGHTNESS), shapeMesh.shade(i));
|
||||
int v = put(mesh, pose.x() + lx * cos - lz * sin, pose.y(), pose.z() + lx * sin + lz * cos,
|
||||
AnimatedRenderColors.scaleAlpha(argb, pose.alpha()), false, 0, 0);
|
||||
if (base < 0) base = v;
|
||||
}
|
||||
for (int i = 0; i < indices; i += 3) {
|
||||
mesh.triangle(base + shapeMesh.index(i), base + shapeMesh.index(i + 1), base + shapeMesh.index(i + 2));
|
||||
}
|
||||
}
|
||||
|
||||
public void texturedQuad(MeshBuilder mesh, Pose pose, IntUnaryOperator palette) {
|
||||
float r = pose.radius();
|
||||
float cos = (float) Math.cos(pose.angle()) * r;
|
||||
float sin = (float) Math.sin(pose.angle()) * r;
|
||||
mesh.ensureQuadCapacity();
|
||||
int a = corner(mesh, pose, -cos - sin, cos - sin, 0, 1, palette.applyAsInt(270));
|
||||
int b = corner(mesh, pose, cos - sin, cos + sin, 1, 1, palette.applyAsInt(0));
|
||||
int c = corner(mesh, pose, cos + sin, sin - cos, 1, 0, palette.applyAsInt(180));
|
||||
int d = corner(mesh, pose, -cos + sin, -sin - cos, 0, 0, palette.applyAsInt(90));
|
||||
mesh.quad(a, b, c, d);
|
||||
}
|
||||
|
||||
private static int corner(MeshBuilder mesh, Pose pose, double dx, double dz, double u, double v, int argb) {
|
||||
return put(mesh, pose.x() + dx, pose.y(), pose.z() + dz,
|
||||
AnimatedRenderColors.scaleAlpha(argb, pose.alpha()), true, u, v);
|
||||
}
|
||||
|
||||
private static int put(MeshBuilder mesh, double x, double y, double z, int argb,
|
||||
boolean textured, double u, double v) {
|
||||
mesh.vec3(x, y, z);
|
||||
if (textured) mesh.vec2(u, v);
|
||||
mesh.color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, argb >>> 24);
|
||||
return mesh.next();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles;
|
||||
|
||||
/** A circle born at the take-off point; {@code spawnMs} is GameClock time. */
|
||||
public record JumpCircle(double x, double y, double z, long spawnMs) {
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape;
|
||||
|
||||
/** Ground-plane shapes JumpCircles can draw; flags tell which tuning parameters a shape honours. */
|
||||
public enum Shape {
|
||||
STAR(false, false),
|
||||
DIAMOND(false, false),
|
||||
SPIRAL(true, true),
|
||||
HEXAGON(true, false),
|
||||
WAVE(true, true),
|
||||
CROSS(false, false),
|
||||
PETALS(false, true);
|
||||
|
||||
private final boolean usesThickness;
|
||||
private final boolean usesSegments;
|
||||
|
||||
Shape(boolean usesThickness, boolean usesSegments) {
|
||||
this.usesThickness = usesThickness;
|
||||
this.usesSegments = usesSegments;
|
||||
}
|
||||
|
||||
public boolean usesThickness() {
|
||||
return usesThickness;
|
||||
}
|
||||
|
||||
public boolean usesSegments() {
|
||||
return usesSegments;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape;
|
||||
|
||||
/** Pure geometry for the JumpCircles shapes; every vertex stays inside the unit circle. */
|
||||
public final class ShapeGeometry {
|
||||
public static final float EDGE_SHADE = 0.55f;
|
||||
private static final float TWO_PI = (float) (Math.PI * 2.0);
|
||||
private static final int STAR_POINTS = 5;
|
||||
private static final float STAR_INNER = 0.45f;
|
||||
private static final float DIAMOND_WIDTH = 0.7f;
|
||||
private static final int SPIRAL_ARMS = 3;
|
||||
private static final int HEX_SIDES = 6;
|
||||
private static final float WAVE_AMPLITUDE = 0.08f;
|
||||
private static final float WAVE_COUNT = 6.0f;
|
||||
private static final float CROSS_CORE = 0.12f;
|
||||
private static final int PETAL_COUNT = 6;
|
||||
private static final float PETAL_CENTER = 0.55f;
|
||||
private static final float PETAL_LENGTH = 0.45f;
|
||||
private static final float PETAL_WIDTH = 0.2f;
|
||||
|
||||
private ShapeGeometry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds {@code out} for the given shape.
|
||||
*
|
||||
* @param segments detail per arm, petal or ring (used by SPIRAL, WAVE, PETALS)
|
||||
* @param thickness ring band width in unit radius (used by SPIRAL, HEXAGON, WAVE)
|
||||
* @param phase wave travel phase in radians (used by WAVE)
|
||||
*/
|
||||
public static void build(Shape shape, ShapeMesh out, int segments, float thickness, float phase) {
|
||||
out.clear();
|
||||
int seg = Math.max(3, segments);
|
||||
float band = Math.max(0.02f, Math.min(0.5f, thickness));
|
||||
switch (shape) {
|
||||
case STAR -> star(out);
|
||||
case DIAMOND -> diamond(out);
|
||||
case SPIRAL -> spiral(out, seg, band);
|
||||
case HEXAGON -> hexagon(out, band);
|
||||
case WAVE -> wave(out, Math.max(8, seg), band, phase);
|
||||
case CROSS -> cross(out);
|
||||
case PETALS -> petals(out, seg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void star(ShapeMesh m) {
|
||||
int center = m.vertex(0, 0, 0.5f, 0);
|
||||
int n = STAR_POINTS * 2;
|
||||
int prev = -1;
|
||||
for (int i = 0; i <= n; i++) {
|
||||
int k = i % n;
|
||||
boolean tip = (k & 1) == 0;
|
||||
float a = TWO_PI * k / n - (float) Math.PI / 2f;
|
||||
float r = tip ? 1f : STAR_INNER;
|
||||
int v = m.vertex((float) Math.cos(a) * r, (float) Math.sin(a) * r,
|
||||
seamless((float) k / n), tip ? EDGE_SHADE : 0.2f);
|
||||
if (prev >= 0) m.triangle(center, prev, v);
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
|
||||
private static void diamond(ShapeMesh m) {
|
||||
int center = m.vertex(0, 0, 0.5f, 0);
|
||||
int north = m.vertex(0, -1, 0f, EDGE_SHADE);
|
||||
int east = m.vertex(DIAMOND_WIDTH, 0, 0.5f, EDGE_SHADE);
|
||||
int south = m.vertex(0, 1, 1f, EDGE_SHADE);
|
||||
int west = m.vertex(-DIAMOND_WIDTH, 0, 0.5f, EDGE_SHADE);
|
||||
m.triangle(center, north, east);
|
||||
m.triangle(center, east, south);
|
||||
m.triangle(center, south, west);
|
||||
m.triangle(center, west, north);
|
||||
}
|
||||
|
||||
private static void spiral(ShapeMesh m, int segments, float band) {
|
||||
for (int arm = 0; arm < SPIRAL_ARMS; arm++) {
|
||||
float start = TWO_PI * arm / SPIRAL_ARMS;
|
||||
int prevIn = -1;
|
||||
int prevOut = -1;
|
||||
for (int k = 0; k <= segments; k++) {
|
||||
float u = (float) k / segments;
|
||||
float a = start + u * TWO_PI * 0.3f;
|
||||
float center = (1f - band * 0.5f) * (0.5f + 0.5f * u);
|
||||
float half = band * 0.5f * (float) Math.sin(Math.PI * u);
|
||||
float grad = seamless((arm + u) / SPIRAL_ARMS);
|
||||
int in = polar(m, a, center - half, grad, 0f);
|
||||
int out = polar(m, a, center + half, grad, EDGE_SHADE);
|
||||
strip(m, prevIn, prevOut, in, out);
|
||||
prevIn = in;
|
||||
prevOut = out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hexagon(ShapeMesh m, float band) {
|
||||
for (int i = 0; i < HEX_SIDES; i++) {
|
||||
float a0 = TWO_PI * i / HEX_SIDES;
|
||||
float a1 = TWO_PI * (i + 1) / HEX_SIDES;
|
||||
float shade = (i & 1) == 0 ? 0f : EDGE_SHADE;
|
||||
float grad = seamless((float) i / HEX_SIDES);
|
||||
int in0 = polar(m, a0, 1f - band, grad, shade);
|
||||
int out0 = polar(m, a0, 1f, grad, shade);
|
||||
int in1 = polar(m, a1, 1f - band, grad, shade);
|
||||
int out1 = polar(m, a1, 1f, grad, shade);
|
||||
strip(m, in0, out0, in1, out1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void wave(ShapeMesh m, int segments, float band, float phase) {
|
||||
int prevIn = -1;
|
||||
int prevOut = -1;
|
||||
int firstIn = -1;
|
||||
int firstOut = -1;
|
||||
for (int k = 0; k < segments; k++) {
|
||||
float a = TWO_PI * k / segments;
|
||||
float outer = 1f - WAVE_AMPLITUDE + WAVE_AMPLITUDE * (float) Math.sin(WAVE_COUNT * a + phase);
|
||||
float grad = seamless((float) k / segments);
|
||||
int in = polar(m, a, Math.max(0f, outer - band), grad, 0f);
|
||||
int out = polar(m, a, outer, grad, EDGE_SHADE);
|
||||
if (firstIn < 0) {
|
||||
firstIn = in;
|
||||
firstOut = out;
|
||||
}
|
||||
strip(m, prevIn, prevOut, in, out);
|
||||
prevIn = in;
|
||||
prevOut = out;
|
||||
}
|
||||
strip(m, prevIn, prevOut, firstIn, firstOut);
|
||||
}
|
||||
|
||||
private static void cross(ShapeMesh m) {
|
||||
int c0 = m.vertex(CROSS_CORE, -CROSS_CORE, 0.5f, 0f);
|
||||
int c1 = m.vertex(CROSS_CORE, CROSS_CORE, 0.5f, 0f);
|
||||
int c2 = m.vertex(-CROSS_CORE, CROSS_CORE, 0.5f, 0f);
|
||||
int c3 = m.vertex(-CROSS_CORE, -CROSS_CORE, 0.5f, 0f);
|
||||
m.triangle(c0, c1, c2);
|
||||
m.triangle(c0, c2, c3);
|
||||
m.triangle(c0, c1, m.vertex(1f, 0f, 0f, EDGE_SHADE));
|
||||
m.triangle(c1, c2, m.vertex(0f, 1f, 0.5f, EDGE_SHADE));
|
||||
m.triangle(c2, c3, m.vertex(-1f, 0f, 1f, EDGE_SHADE));
|
||||
m.triangle(c3, c0, m.vertex(0f, -1f, 0.5f, EDGE_SHADE));
|
||||
}
|
||||
|
||||
private static void petals(ShapeMesh m, int segments) {
|
||||
for (int p = 0; p < PETAL_COUNT; p++) {
|
||||
float a = TWO_PI * p / PETAL_COUNT;
|
||||
float cos = (float) Math.cos(a);
|
||||
float sin = (float) Math.sin(a);
|
||||
float grad = seamless((float) p / PETAL_COUNT);
|
||||
int center = m.vertex(cos * PETAL_CENTER, sin * PETAL_CENTER, grad, 0f);
|
||||
int prev = -1;
|
||||
for (int k = 0; k <= segments; k++) {
|
||||
float u = TWO_PI * k / segments;
|
||||
float along = PETAL_CENTER + PETAL_LENGTH * (float) Math.cos(u);
|
||||
float side = PETAL_WIDTH * (float) Math.sin(u);
|
||||
int v = m.vertex(cos * along - sin * side, sin * along + cos * side, grad, EDGE_SHADE);
|
||||
if (prev >= 0) m.triangle(center, prev, v);
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int polar(ShapeMesh m, float angle, float radius, float gradient, float shade) {
|
||||
return m.vertex((float) Math.cos(angle) * radius, (float) Math.sin(angle) * radius, gradient, shade);
|
||||
}
|
||||
|
||||
/** Joins two consecutive inner/outer vertex pairs of a band; the first pair has index -1 and is skipped. */
|
||||
private static void strip(ShapeMesh m, int in0, int out0, int in1, int out1) {
|
||||
if (in0 < 0) return;
|
||||
m.triangle(in0, out0, out1);
|
||||
m.triangle(in0, out1, in1);
|
||||
}
|
||||
|
||||
/** Maps [0,1] to a there-and-back ramp so a gradient wraps around a closed ring without a seam. */
|
||||
private static float seamless(float f) {
|
||||
return 1f - Math.abs(2f * f - 1f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.player.jumpcircles.shape;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Reusable triangle list in the unit ground plane. Each vertex carries a gradient position
|
||||
* {@code t} in [0,1] and a {@code shade} in [0,1] (0 = main colour, 1 = darkened outline colour).
|
||||
*/
|
||||
public final class ShapeMesh {
|
||||
private float[] x = new float[64];
|
||||
private float[] z = new float[64];
|
||||
private float[] t = new float[64];
|
||||
private float[] shade = new float[64];
|
||||
private int[] indices = new int[192];
|
||||
private int vertexCount;
|
||||
private int indexCount;
|
||||
|
||||
public void clear() {
|
||||
vertexCount = 0;
|
||||
indexCount = 0;
|
||||
}
|
||||
|
||||
public int vertex(float px, float pz, float gradient, float vertexShade) {
|
||||
if (vertexCount == x.length) {
|
||||
int size = vertexCount * 2;
|
||||
x = Arrays.copyOf(x, size);
|
||||
z = Arrays.copyOf(z, size);
|
||||
t = Arrays.copyOf(t, size);
|
||||
shade = Arrays.copyOf(shade, size);
|
||||
}
|
||||
x[vertexCount] = px;
|
||||
z[vertexCount] = pz;
|
||||
t[vertexCount] = gradient;
|
||||
shade[vertexCount] = vertexShade;
|
||||
return vertexCount++;
|
||||
}
|
||||
|
||||
public void triangle(int a, int b, int c) {
|
||||
if (indexCount + 3 > indices.length) {
|
||||
indices = Arrays.copyOf(indices, indices.length * 2);
|
||||
}
|
||||
indices[indexCount++] = a;
|
||||
indices[indexCount++] = b;
|
||||
indices[indexCount++] = c;
|
||||
}
|
||||
|
||||
public int vertexCount() {
|
||||
return vertexCount;
|
||||
}
|
||||
|
||||
public int indexCount() {
|
||||
return indexCount;
|
||||
}
|
||||
|
||||
public float x(int i) {
|
||||
return x[i];
|
||||
}
|
||||
|
||||
public float z(int i) {
|
||||
return z[i];
|
||||
}
|
||||
|
||||
public float gradient(int i) {
|
||||
return t[i];
|
||||
}
|
||||
|
||||
public float shade(int i) {
|
||||
return shade[i];
|
||||
}
|
||||
|
||||
public int index(int i) {
|
||||
return indices[i];
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ public final class ServerPolicyClient {
|
|||
public static void init() {
|
||||
PayloadTypeRegistry.serverboundPlay().register(Payload.TYPE, Payload.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(Payload.TYPE, Payload.CODEC);
|
||||
ClientPlayNetworking.registerReceiver(Payload.TYPE,
|
||||
ClientPlayNetworking.registerGlobalReceiver(Payload.TYPE,
|
||||
(payload, context) -> onPolicy(payload.json()));
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) ->
|
||||
ClientPlayNetworking.send(new Payload(ServerPolicyCodec.REQUEST_JSON)));
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public final class ThemeStore {
|
|||
final List<ThemeEntry> themes = new ArrayList<>();
|
||||
final List<ThemeEntry> customThemes = new ArrayList<>();
|
||||
final Map<String, ThemeEntry> themesById = new LinkedHashMap<>();
|
||||
String currentId = Themes.THEME_CLASSIC;
|
||||
String currentId = Themes.THEME_DEFAULT;
|
||||
|
||||
private ThemeStore() {
|
||||
ThemeStoreIO.load(this);
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ public enum Themes {
|
|||
public static final String THEME_PRISM = "prism";
|
||||
public static final String THEME_CINDER = "cinder";
|
||||
public static final String THEME_FOREST = "forest";
|
||||
/** Theme used on a fresh install and whenever the saved id is missing. */
|
||||
public static final String THEME_DEFAULT = THEME_AMBER;
|
||||
|
||||
static final long THEME_TRANSITION_MS = 340L;
|
||||
static final ThemeStore STORE = ThemeStore.get();
|
||||
|
||||
/** Display order for the preset picker; also drives {@link #PRESET_ENTRIES}. */
|
||||
static final List<ThemeEntry> PRESET_LIST = List.of(
|
||||
ThemePresets1.CLASSIC_ENTRY, ThemePresets1.LEGACY_ENTRY, ThemePresets1.AMBER_ENTRY,
|
||||
|
|
@ -49,6 +49,8 @@ public enum Themes {
|
|||
ThemePresets3.PRISM_ENTRY, ThemePresets3.CINDER_ENTRY, ThemePresets3.FOREST_ENTRY
|
||||
);
|
||||
static final Map<String, ThemeEntry> PRESET_ENTRIES = buildPresetMap(PRESET_LIST);
|
||||
// Must come after the preset tables: loading the store rebuilds its index from presetEntries().
|
||||
static final ThemeStore STORE = ThemeStore.get();
|
||||
|
||||
private static Map<String, ThemeEntry> buildPresetMap(List<ThemeEntry> list) {
|
||||
Map<String, ThemeEntry> m = new LinkedHashMap<>();
|
||||
|
|
@ -57,15 +59,15 @@ public enum Themes {
|
|||
}
|
||||
|
||||
// Mutable transition state; read / written by ThemesTransition (same package).
|
||||
static ThemeEntry currentEntry = ThemePresets1.CLASSIC_ENTRY;
|
||||
static ThemeEntry transitionFromEntry = ThemePresets1.CLASSIC_ENTRY;
|
||||
static ThemeEntry animatedEntryCache = ThemePresets1.CLASSIC_ENTRY;
|
||||
static Theme current = ThemePresets1.CLASSIC;
|
||||
static String currentId = THEME_CLASSIC;
|
||||
static Theme transitionFromTheme = ThemePresets1.CLASSIC;
|
||||
static ThemeEntry currentEntry = ThemePresets1.AMBER_ENTRY;
|
||||
static ThemeEntry transitionFromEntry = ThemePresets1.AMBER_ENTRY;
|
||||
static ThemeEntry animatedEntryCache = ThemePresets1.AMBER_ENTRY;
|
||||
static Theme current = ThemePresets1.AMBER;
|
||||
static String currentId = THEME_DEFAULT;
|
||||
static Theme transitionFromTheme = ThemePresets1.AMBER;
|
||||
static long transitionStartMs = 0L;
|
||||
static boolean transitionActive = false;
|
||||
static Theme animatedThemeCache = ThemePresets1.CLASSIC;
|
||||
static Theme animatedThemeCache = ThemePresets1.AMBER;
|
||||
static int animatedCacheKey = -1;
|
||||
|
||||
static {
|
||||
|
|
|
|||
|
|
@ -33,15 +33,15 @@ final class ThemesTransition {
|
|||
/** Switch to the theme stored under the given preset / custom id, or fall back to classic. */
|
||||
static void setTheme(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
boolean animate = !Themes.THEME_CLASSIC.equals(Themes.currentId) || Themes.transitionActive;
|
||||
applyThemeFromEntry(ThemePresets1.CLASSIC_ENTRY, animate);
|
||||
Themes.STORE.setCurrentId(Themes.THEME_CLASSIC);
|
||||
boolean animate = !Themes.THEME_DEFAULT.equals(Themes.currentId) || Themes.transitionActive;
|
||||
applyThemeFromEntry(ThemePresets1.AMBER_ENTRY, animate);
|
||||
Themes.STORE.setCurrentId(Themes.THEME_DEFAULT);
|
||||
return;
|
||||
}
|
||||
String key = id.toLowerCase();
|
||||
ThemeEntry entry = Themes.STORE.getById(key);
|
||||
if (entry == null) entry = Themes.PRESET_ENTRIES.get(key);
|
||||
if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY;
|
||||
if (entry == null) entry = ThemePresets1.AMBER_ENTRY;
|
||||
boolean animate = !entry.getId().equals(Themes.currentId) || Themes.transitionActive;
|
||||
applyThemeFromEntry(entry, animate);
|
||||
Themes.STORE.setCurrentId(entry.getId());
|
||||
|
|
@ -78,8 +78,8 @@ final class ThemesTransition {
|
|||
static void applyFromStore() {
|
||||
String id = Themes.STORE.getCurrentId();
|
||||
ThemeEntry entry = Themes.STORE.getById(id);
|
||||
if (entry == null) entry = Themes.PRESET_ENTRIES.get(Themes.THEME_CLASSIC);
|
||||
if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY;
|
||||
if (entry == null) entry = Themes.PRESET_ENTRIES.get(Themes.THEME_DEFAULT);
|
||||
if (entry == null) entry = ThemePresets1.AMBER_ENTRY;
|
||||
applyThemeFromEntry(entry, false);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ final class ThemeStoreIO {
|
|||
retireLegacyFile(store);
|
||||
rebuildMergedIndex(store);
|
||||
if (store.getById(store.currentId) == null) {
|
||||
store.currentId = Themes.THEME_CLASSIC;
|
||||
store.currentId = Themes.THEME_DEFAULT;
|
||||
saveState(store);
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ final class ThemeStoreIO {
|
|||
String fromState = readCurrentId(STATE_FILE);
|
||||
if (fromState != null) return fromState;
|
||||
String fromLegacy = readCurrentId(LEGACY_FILE);
|
||||
return fromLegacy != null ? fromLegacy : Themes.THEME_CLASSIC;
|
||||
return fromLegacy != null ? fromLegacy : Themes.THEME_DEFAULT;
|
||||
}
|
||||
|
||||
private static String readCurrentId(Path file) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.hud;
|
||||
package dev.loki.lovisual.mixinhandlers.hud;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
|
|
@ -14,11 +14,11 @@ import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomHealthBar
|
|||
* vanilla {@code extractPlayerHealth} pass is fed neutral values (absorption 0, max health 20) so it
|
||||
* reserves the correct space without drawing hearts we replace ourselves.
|
||||
*/
|
||||
final class GuiHealthLayoutHandler {
|
||||
public final class GuiHealthLayoutHandler {
|
||||
private GuiHealthLayoutHandler() {
|
||||
}
|
||||
|
||||
static float ignoreAbsorptionForLayout(Player player) {
|
||||
public static float ignoreAbsorptionForLayout(Player player) {
|
||||
if (!Lifecycle.canRunHud()) {
|
||||
return player.getAbsorptionAmount();
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ final class GuiHealthLayoutHandler {
|
|||
return player.getAbsorptionAmount();
|
||||
}
|
||||
|
||||
static double clampMaxHealthForLayout(Player player, Holder<Attribute> attribute) {
|
||||
public static double clampMaxHealthForLayout(Player player, Holder<Attribute> attribute) {
|
||||
if (!Lifecycle.canRunHud()) {
|
||||
return player.getAttributes().getValue(attribute);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.hud;
|
||||
package dev.loki.lovisual.mixinhandlers.hud;
|
||||
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
|
@ -25,14 +25,14 @@ import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
|||
* health bar. All passes are emitted through a deferred {@link Renderer2D} layer so they land after
|
||||
* vanilla misc overlays, then drop a stratum marker via {@link IGuiGraphics}.
|
||||
*/
|
||||
final class GuiHudBarHandler {
|
||||
public final class GuiHudBarHandler {
|
||||
private GuiHudBarHandler() {
|
||||
}
|
||||
|
||||
static final Renderer2D.Deferred2DLayer VANILLA_HUD_REPLACEMENT_LAYER =
|
||||
public static final Renderer2D.Deferred2DLayer VANILLA_HUD_REPLACEMENT_LAYER =
|
||||
Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS;
|
||||
|
||||
static void renderHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) {
|
||||
public static void renderHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null || mc.player == null) return;
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ final class GuiHudBarHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void afterVanillaHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter) {
|
||||
public static void afterVanillaHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null || mc.player == null) return;
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ final class GuiHudBarHandler {
|
|||
renderCustomBarOnly(ctx, tickCounter, mc, bar);
|
||||
}
|
||||
|
||||
static void renderHealth(GuiGraphicsExtractor context, Player player, int x, int y, float maxHealth, CallbackInfo ci) {
|
||||
public static void renderHealth(GuiGraphicsExtractor context, Player player, int x, int y, float maxHealth, CallbackInfo ci) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
if (!(player instanceof LocalPlayer clientPlayer)) return;
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.hud;
|
||||
package dev.loki.lovisual.mixinhandlers.hud;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
|
@ -20,18 +20,18 @@ import dev.loki.lovisual.util.text.format.TextJsonUtil;
|
|||
* potion effects, crosshair, vignette and the portal/sleep overlays. Each entry point cancels the
|
||||
* vanilla {@code extract*}/{@code display*} call when a module or custom HUD takes over.
|
||||
*/
|
||||
final class GuiOverlayHandler {
|
||||
public final class GuiOverlayHandler {
|
||||
private GuiOverlayHandler() {
|
||||
}
|
||||
|
||||
static void hideScoreboardSidebar(CallbackInfo ci) {
|
||||
public static void hideScoreboardSidebar(CallbackInfo ci) {
|
||||
NoRender noRender = Modules.get(NoRender.class);
|
||||
if ((noRender != null && noRender.hideScoreboard()) || Scoreboard.shouldReplaceVanilla()) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
static void filterOverlayMessage(Component message, boolean tinted, CallbackInfo ci) {
|
||||
public static void filterOverlayMessage(Component message, boolean tinted, CallbackInfo ci) {
|
||||
if (DebugLog.serverOnly()) {
|
||||
if (message == null) {
|
||||
// DebugLog.server("HUD overlay message: tinted=%s raw=null", tinted);
|
||||
|
|
@ -51,7 +51,7 @@ final class GuiOverlayHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void hideVanillaPotions(CallbackInfo ci) {
|
||||
public static void hideVanillaPotions(CallbackInfo ci) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null || mc.player == null) return;
|
||||
if (!DraggableHudElementRegistry.isEnabled(Potions.class)) return;
|
||||
|
|
@ -59,23 +59,23 @@ final class GuiOverlayHandler {
|
|||
ci.cancel();
|
||||
}
|
||||
|
||||
static void replaceCrosshair(CallbackInfo ci) {
|
||||
public static void replaceCrosshair(CallbackInfo ci) {
|
||||
Crosshair crosshair = Modules.get(Crosshair.class);
|
||||
if (crosshair != null && crosshair.shouldHideVanilla()) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
static void killVignette(CallbackInfo ci) {
|
||||
public static void killVignette(CallbackInfo ci) {
|
||||
// Completely remove vanilla vignette post-processing
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
static void disablePortalOverlay(CallbackInfo ci) {
|
||||
public static void disablePortalOverlay(CallbackInfo ci) {
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
static void disableSleepOverlay(CallbackInfo ci) {
|
||||
public static void disableSleepOverlay(CallbackInfo ci) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.hud;
|
||||
package dev.loki.lovisual.mixinhandlers.hud;
|
||||
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
|
@ -18,11 +18,11 @@ import dev.loki.lovisual.util.item.enchant.TopEnchantUtil;
|
|||
* rarity / illegal / top-enchant derived name color and a fade driven by the highlight timer,
|
||||
* cancelling the vanilla render when the custom SwapTooltip is active.
|
||||
*/
|
||||
final class GuiTooltipHandler {
|
||||
public final class GuiTooltipHandler {
|
||||
private GuiTooltipHandler() {
|
||||
}
|
||||
|
||||
static void colorizeHeldItem(int toolHighlightTimer, ItemStack lastToolHighlight, Minecraft minecraft, CallbackInfo ci) {
|
||||
public static void colorizeHeldItem(int toolHighlightTimer, ItemStack lastToolHighlight, Minecraft minecraft, CallbackInfo ci) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
SwapTooltip swapTooltip = SwapTooltip.get();
|
||||
if (swapTooltip == null || !swapTooltip.isEnabled()) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.network.game;
|
||||
package dev.loki.lovisual.mixinhandlers.network;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
|
|
@ -30,7 +30,7 @@ import java.util.regex.Pattern;
|
|||
* Chat, tab-metric and totem-pop packet bodies for {@link ClientPacketListenerMixin}.
|
||||
* Physically under {@code handlers/} but declares the parent package for direct access.
|
||||
*/
|
||||
final class ClientPacketListenerPvpHandler {
|
||||
public final class ClientPacketListenerPvpHandler {
|
||||
|
||||
// "TPS: 20.0" или "TPS: *20.0"
|
||||
private static final Pattern TPS_PATTERN =
|
||||
|
|
@ -42,7 +42,7 @@ final class ClientPacketListenerPvpHandler {
|
|||
private ClientPacketListenerPvpHandler() {
|
||||
}
|
||||
|
||||
static void captureTab(ClientboundTabListPacket packet) {
|
||||
public static void captureTab(ClientboundTabListPacket packet) {
|
||||
String header = packet.header() == null ? "" : packet.header().getString();
|
||||
String footer = packet.footer() == null ? "" : packet.footer().getString();
|
||||
ServerDumpUtil.dumpTabText("HEADER", packet.header());
|
||||
|
|
@ -52,7 +52,7 @@ final class ClientPacketListenerPvpHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void onSystemChat(ClientboundSystemChatPacket packet) {
|
||||
public static void onSystemChat(ClientboundSystemChatPacket packet) {
|
||||
String msg = packet.content().getString();
|
||||
|
||||
// PvP state listeners via events; legacy direct setters disabled.
|
||||
|
|
@ -61,7 +61,7 @@ final class ClientPacketListenerPvpHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void onPlayerChat(ClientboundPlayerChatPacket packet) {
|
||||
public static void onPlayerChat(ClientboundPlayerChatPacket packet) {
|
||||
String msg = packet == null ? null : packet.body().content();
|
||||
if (msg == null || msg.isBlank()) return;
|
||||
if (Events.BUS.hasListeners(PvpChatEvent.class)) {
|
||||
|
|
@ -69,7 +69,7 @@ final class ClientPacketListenerPvpHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void parseTabMetrics(Component text) {
|
||||
public static void parseTabMetrics(Component text) {
|
||||
if (text == null) return;
|
||||
|
||||
String raw = text.getString();
|
||||
|
|
@ -92,7 +92,7 @@ final class ClientPacketListenerPvpHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void onTotemStatus(ClientboundEntityEventPacket packet, ClientPacketListener listener) {
|
||||
public static void onTotemStatus(ClientboundEntityEventPacket packet, ClientPacketListener listener) {
|
||||
if (packet.getEventId() == 35) { // 35 = сработал тотем бессмертия
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.network.game;
|
||||
package dev.loki.lovisual.mixinhandlers.network;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket;
|
||||
|
|
@ -22,12 +22,12 @@ import dev.loki.lovisual.util.player.effect.heuristics.StatusEffectHeuristics;
|
|||
* {@code @Inject}/{@code @Redirect} wiring. Physically under {@code handlers/} but
|
||||
* declares the parent package for direct access.
|
||||
*/
|
||||
final class ClientPacketListenerSoundHandler {
|
||||
public final class ClientPacketListenerSoundHandler {
|
||||
|
||||
private ClientPacketListenerSoundHandler() {
|
||||
}
|
||||
|
||||
static void filterSound(ClientboundSoundPacket packet, CallbackInfo ci) {
|
||||
public static void filterSound(ClientboundSoundPacket packet, CallbackInfo ci) {
|
||||
NoSound module = Modules.get(NoSound.class);
|
||||
if (module == null || !module.isEnabled()) return;
|
||||
if (packet == null || packet.getSound() == null) return;
|
||||
|
|
@ -37,7 +37,7 @@ final class ClientPacketListenerSoundHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void filterSoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) {
|
||||
public static void filterSoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) {
|
||||
NoSound module = Modules.get(NoSound.class);
|
||||
if (module == null || !module.isEnabled()) return;
|
||||
if (packet == null || packet.getSound() == null) return;
|
||||
|
|
@ -47,7 +47,7 @@ final class ClientPacketListenerSoundHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void onPlaySound(ClientboundSoundPacket packet) {
|
||||
public static void onPlaySound(ClientboundSoundPacket packet) {
|
||||
SoundESP esp = Modules.get(SoundESP.class);
|
||||
if (esp == null || !esp.isEnabled()) return;
|
||||
if (packet == null || packet.getSound() == null) return;
|
||||
|
|
@ -55,7 +55,7 @@ final class ClientPacketListenerSoundHandler {
|
|||
esp.handleSound(packet.getSound().value().location(), pos);
|
||||
}
|
||||
|
||||
static void onPlaySoundFromEntity(ClientboundSoundEntityPacket packet) {
|
||||
public static void onPlaySoundFromEntity(ClientboundSoundEntityPacket packet) {
|
||||
SoundESP esp = Modules.get(SoundESP.class);
|
||||
if (esp == null || !esp.isEnabled()) return;
|
||||
if (packet == null || packet.getSound() == null) return;
|
||||
|
|
@ -68,7 +68,7 @@ final class ClientPacketListenerSoundHandler {
|
|||
esp.handleSound(packet.getSound().value().location(), entity.position());
|
||||
}
|
||||
|
||||
static void heuristicParticle(ClientboundLevelParticlesPacket packet) {
|
||||
public static void heuristicParticle(ClientboundLevelParticlesPacket packet) {
|
||||
var p = packet.getParticle();
|
||||
// Log raw particles once per hook for diagnostics
|
||||
DebugLog.info("Particle packet type=%s pos=%.2f %.2f %.2f count=%d",
|
||||
|
|
@ -78,7 +78,7 @@ final class ClientPacketListenerSoundHandler {
|
|||
StatusEffectHeuristics.handle(p, packet.getX(), packet.getY(), packet.getZ());
|
||||
}
|
||||
|
||||
static void logStatusEffect(ClientboundUpdateMobEffectPacket packet) {
|
||||
public static void logStatusEffect(ClientboundUpdateMobEffectPacket packet) {
|
||||
DebugLog.server("Packet: add/refresh effect eid=%d effect=%s amp=%d dur=%d particles=%s icon=%s ambient=%s keepFading=%s",
|
||||
packet.getEntityId(),
|
||||
packet.getEffect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown"),
|
||||
|
|
@ -91,7 +91,7 @@ final class ClientPacketListenerSoundHandler {
|
|||
);
|
||||
}
|
||||
|
||||
static void logRemove(ClientboundRemoveMobEffectPacket packet) {
|
||||
public static void logRemove(ClientboundRemoveMobEffectPacket packet) {
|
||||
DebugLog.server("Packet: remove effect eid=%d (%s) effect=%s",
|
||||
packet.entityId(),
|
||||
resolveName(packet.entityId()),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.network.game;
|
||||
package dev.loki.lovisual.mixinhandlers.network;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
|
|
@ -19,12 +19,12 @@ import java.util.UUID;
|
|||
* staff tracker and totem-pop counter from spawn/despawn and player-info updates.
|
||||
* Physically under {@code handlers/} but declares the parent package for direct access.
|
||||
*/
|
||||
final class ClientPacketListenerStaffHandler {
|
||||
public final class ClientPacketListenerStaffHandler {
|
||||
|
||||
private ClientPacketListenerStaffHandler() {
|
||||
}
|
||||
|
||||
static void onEntitiesDestroy(ClientboundRemoveEntitiesPacket packet) {
|
||||
public static void onEntitiesDestroy(ClientboundRemoveEntitiesPacket packet) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
ClientLevel world = mc.level;
|
||||
if (world == null) return;
|
||||
|
|
@ -38,7 +38,7 @@ final class ClientPacketListenerStaffHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void onEntitySpawn(ClientboundAddEntityPacket packet) {
|
||||
public static void onEntitySpawn(ClientboundAddEntityPacket packet) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
ClientLevel world = mc.level;
|
||||
if (world == null) return;
|
||||
|
|
@ -51,7 +51,7 @@ final class ClientPacketListenerStaffHandler {
|
|||
StaffTracker.INSTANCE.onEntityAppear(player.getUUID(), player.getGameProfile().name());
|
||||
}
|
||||
|
||||
static void onPlayerList(ClientboundPlayerInfoUpdatePacket packet) {
|
||||
public static void onPlayerList(ClientboundPlayerInfoUpdatePacket packet) {
|
||||
var actions = packet.actions();
|
||||
boolean listedAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)
|
||||
|| actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER);
|
||||
|
|
@ -77,7 +77,7 @@ final class ClientPacketListenerStaffHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static void onPlayerRemove(ClientboundPlayerInfoRemovePacket packet) {
|
||||
public static void onPlayerRemove(ClientboundPlayerInfoRemovePacket packet) {
|
||||
for (UUID id : packet.profileIds()) {
|
||||
|
||||
// убрали из TAB
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.entity.player;
|
||||
package dev.loki.lovisual.mixinhandlers.player;
|
||||
|
||||
import dev.loki.lovisual.events.Events;
|
||||
import dev.loki.lovisual.events.impl.input.SprintControlEvent;
|
||||
|
|
@ -10,14 +10,14 @@ import net.minecraft.client.player.LocalPlayer;
|
|||
import net.minecraft.world.entity.player.Input;
|
||||
import net.minecraft.world.phys.Vec2;
|
||||
|
||||
final class LocalPlayerInputHandler {
|
||||
public final class LocalPlayerInputHandler {
|
||||
|
||||
boolean shouldBlockWASD() {
|
||||
public boolean shouldBlockWASD() {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
return fc != null && fc.isEnabled() && fc.isCameraInput();
|
||||
}
|
||||
|
||||
void handleFreecamAfterInputTick(LocalPlayer self) {
|
||||
public void handleFreecamAfterInputTick(LocalPlayer self) {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
if (fc == null || !fc.isEnabled() || !fc.isCameraInput()) return;
|
||||
if (self.input == null) return;
|
||||
|
|
@ -26,7 +26,7 @@ final class LocalPlayerInputHandler {
|
|||
self.input.keyPresses = new Input(false, false, false, false, false, false, sprint);
|
||||
}
|
||||
|
||||
void handleAfterInputTickSprint(LocalPlayer self) {
|
||||
public void handleAfterInputTickSprint(LocalPlayer self) {
|
||||
if (self.input == null) return;
|
||||
Input original = self.input.keyPresses;
|
||||
if (original == null) return;
|
||||
|
|
@ -51,7 +51,7 @@ final class LocalPlayerInputHandler {
|
|||
if (!sameInput(original, input)) applyPlayerInput(self, input);
|
||||
}
|
||||
|
||||
void applyPlayerInput(LocalPlayer self, Input input) {
|
||||
public void applyPlayerInput(LocalPlayer self, Input input) {
|
||||
if (self.input == null || input == null) return;
|
||||
InputAccessor accessor = (InputAccessor) self.input;
|
||||
accessor.setPlayerInput(input);
|
||||
|
|
@ -60,9 +60,9 @@ final class LocalPlayerInputHandler {
|
|||
accessor.setMovementVector(new Vec2(sideways, forward).normalized());
|
||||
}
|
||||
|
||||
Input transformDirection(Input input) { return input; }
|
||||
public Input transformDirection(Input input) { return input; }
|
||||
|
||||
boolean postSprintControl(LocalPlayer self, boolean original, SprintControlEvent.Source source) {
|
||||
public boolean postSprintControl(LocalPlayer self, boolean original, SprintControlEvent.Source source) {
|
||||
if (self.input == null) return original;
|
||||
if (!Events.BUS.hasListeners(SprintControlEvent.class)) return original;
|
||||
Vec2 movement = self.input.getMoveVector();
|
||||
|
|
@ -71,19 +71,19 @@ final class LocalPlayerInputHandler {
|
|||
return event.shouldSprint();
|
||||
}
|
||||
|
||||
boolean redirectShift(Input in, LocalPlayer self) {
|
||||
public boolean redirectShift(Input in, LocalPlayer self) {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
if (fc != null && fc.isEnabled() && fc.isCameraInput() && self.getAbilities().flying) return false;
|
||||
return in.shift();
|
||||
}
|
||||
|
||||
boolean sameInput(Input a, Input b) {
|
||||
public boolean sameInput(Input a, Input b) {
|
||||
if (a == b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
return a.forward() == b.forward() && a.backward() == b.backward() && a.left() == b.left() && a.right() == b.right() && a.jump() == b.jump() && a.shift() == b.shift() && a.sprint() == b.sprint();
|
||||
}
|
||||
|
||||
float impulse(boolean positive, boolean negative) {
|
||||
public float impulse(boolean positive, boolean negative) {
|
||||
if (positive == negative) return 0.0f;
|
||||
return positive ? 1.0f : -1.0f;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.entity.player;
|
||||
package dev.loki.lovisual.mixinhandlers.player;
|
||||
|
||||
import dev.loki.lovisual.events.Events;
|
||||
import dev.loki.lovisual.events.impl.world.EventPushOutOfBlocks;
|
||||
|
|
@ -11,7 +11,7 @@ import net.minecraft.world.entity.PortalProcessor;
|
|||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
final class LocalPlayerMovementHandler {
|
||||
public final class LocalPlayerMovementHandler {
|
||||
|
||||
private float lastHealth = Float.NaN;
|
||||
private boolean frozen;
|
||||
|
|
@ -19,7 +19,7 @@ final class LocalPlayerMovementHandler {
|
|||
private double savedX, savedY, savedZ;
|
||||
private float savedBodyYaw, savedHeadYaw;
|
||||
|
||||
void onPortalNausea(LocalPlayer self, CallbackInfo ci) {
|
||||
public void onPortalNausea(LocalPlayer self, CallbackInfo ci) {
|
||||
PortalProcessor portal = null;
|
||||
if (self instanceof EntityAccessor accessor) portal = accessor.lovisual$getPortalManager();
|
||||
if (portal != null && portal.isInsidePortalThisTick()) {
|
||||
|
|
@ -31,26 +31,26 @@ final class LocalPlayerMovementHandler {
|
|||
}
|
||||
}
|
||||
|
||||
void onHurt(float newHealth) {
|
||||
public void onHurt(float newHealth) {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
if (fc == null || !fc.isEnabled() || !fc.disableOnDamage()) { lastHealth = newHealth; return; }
|
||||
if (!Float.isNaN(lastHealth) && newHealth < lastHealth) fc.toggle();
|
||||
lastHealth = newHealth;
|
||||
}
|
||||
|
||||
void onPushOutOfBlocks(double x, double z, CallbackInfo ci) {
|
||||
public void onPushOutOfBlocks(double x, double z, CallbackInfo ci) {
|
||||
if (!Events.BUS.hasListeners(EventPushOutOfBlocks.class)) return;
|
||||
EventPushOutOfBlocks event = new EventPushOutOfBlocks(x, z);
|
||||
Events.BUS.post(event);
|
||||
if (event.isCancelled()) ci.cancel();
|
||||
}
|
||||
|
||||
boolean shouldFreezeNow() {
|
||||
public boolean shouldFreezeNow() {
|
||||
Freecam freecam = Modules.get(Freecam.class);
|
||||
return freecam != null && freecam.freezePlayer() && freecam.isCameraInput();
|
||||
}
|
||||
|
||||
void onFreezeTick(LocalPlayer self, CallbackInfo ci) {
|
||||
public void onFreezeTick(LocalPlayer self, CallbackInfo ci) {
|
||||
if (!shouldFreezeNow()) {
|
||||
if (frozen) { self.setDeltaMovement(savedVelocity); frozen = false; }
|
||||
return;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.entity.player;
|
||||
package dev.loki.lovisual.mixinhandlers.player;
|
||||
|
||||
import dev.loki.lovisual.events.Events;
|
||||
import dev.loki.lovisual.events.impl.network.EventPostSync;
|
||||
|
|
@ -8,13 +8,13 @@ import dev.loki.lovisual.util.combat.rotation.SprintController;
|
|||
import net.minecraft.client.player.LocalPlayer;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
final class LocalPlayerNetworkHandler {
|
||||
public final class LocalPlayerNetworkHandler {
|
||||
|
||||
private Runnable eventPostAction;
|
||||
private EventSync activeSyncEvent;
|
||||
private boolean postUpdateLock;
|
||||
|
||||
boolean onSendPositionHead(LocalPlayer self, CallbackInfo ci) {
|
||||
public boolean onSendPositionHead(LocalPlayer self, CallbackInfo ci) {
|
||||
if (!Events.BUS.hasListeners(EventSync.class)) return false;
|
||||
eventPostAction = null;
|
||||
EventSync event = new EventSync(self.getYRot(), self.getXRot());
|
||||
|
|
@ -30,15 +30,15 @@ final class LocalPlayerNetworkHandler {
|
|||
return false;
|
||||
}
|
||||
|
||||
void onSendPositionReturn() {
|
||||
public void onSendPositionReturn() {
|
||||
if (Events.BUS.hasListeners(EventPostSync.class)) Events.BUS.post(new EventPostSync());
|
||||
if (eventPostAction != null) { eventPostAction.run(); eventPostAction = null; }
|
||||
activeSyncEvent = null;
|
||||
}
|
||||
|
||||
EventSync getActiveSyncEvent() { return activeSyncEvent; }
|
||||
public EventSync getActiveSyncEvent() { return activeSyncEvent; }
|
||||
|
||||
boolean onPostPlayerUpdate(LocalPlayer self, CallbackInfo ci, Runnable sendPosition) {
|
||||
public boolean onPostPlayerUpdate(LocalPlayer self, CallbackInfo ci, Runnable sendPosition) {
|
||||
if (!Events.BUS.hasListeners(PostPlayerUpdateEvent.class)) return false;
|
||||
if (postUpdateLock) return false;
|
||||
PostPlayerUpdateEvent event = new PostPlayerUpdateEvent();
|
||||
|
|
@ -56,7 +56,7 @@ final class LocalPlayerNetworkHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
boolean shouldBlockSprintNetwork(boolean original) {
|
||||
public boolean shouldBlockSprintNetwork(boolean original) {
|
||||
if (SprintController.INSTANCE.isSprintBlocked()) return false;
|
||||
return original;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.mixins.entity.player;
|
||||
package dev.loki.lovisual.mixinhandlers.player;
|
||||
|
||||
import dev.loki.lovisual.events.impl.network.EventSync;
|
||||
import dev.loki.lovisual.mixininterface.entity.ILocalPlayer;
|
||||
|
|
@ -7,14 +7,14 @@ import dev.loki.lovisual.util.aiming.rotation.RotationManager;
|
|||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
final class LocalPlayerRotationHandler {
|
||||
public final class LocalPlayerRotationHandler {
|
||||
|
||||
private double rotationPrevX;
|
||||
private double rotationPrevZ;
|
||||
private float rotationPrevBodyYaw;
|
||||
private boolean rotationBodyYawInitialized;
|
||||
|
||||
void onTickCache(LocalPlayer self) {
|
||||
public void onTickCache(LocalPlayer self) {
|
||||
if (self instanceof ILocalPlayer access) {
|
||||
access.lovisual$setLastYaw(self.getYRot());
|
||||
access.lovisual$setLastPitch(self.getXRot());
|
||||
|
|
@ -28,31 +28,31 @@ final class LocalPlayerRotationHandler {
|
|||
}
|
||||
}
|
||||
|
||||
float resolveSendYaw(LocalPlayer self, float original, EventSync active) {
|
||||
public float resolveSendYaw(LocalPlayer self, float original, EventSync active) {
|
||||
updateVisualBodyYaw(self);
|
||||
if (active != null && active.hasRotationOverride()) return active.getYaw();
|
||||
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
|
||||
return rotation != null ? rotation.yaw() : original;
|
||||
}
|
||||
|
||||
float resolveSendPitch(float original, EventSync active) {
|
||||
public float resolveSendPitch(float original, EventSync active) {
|
||||
if (active != null && active.hasRotationOverride()) return Mth.clamp(active.getPitch(), -90.0f, 90.0f);
|
||||
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
|
||||
return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original;
|
||||
}
|
||||
|
||||
float resolveTickYaw(LocalPlayer self, float original) {
|
||||
public float resolveTickYaw(LocalPlayer self, float original) {
|
||||
updateVisualBodyYaw(self);
|
||||
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
|
||||
return rotation != null ? rotation.yaw() : original;
|
||||
}
|
||||
|
||||
float resolveTickPitch(float original) {
|
||||
public float resolveTickPitch(float original) {
|
||||
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
|
||||
return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original;
|
||||
}
|
||||
|
||||
void updateVisualBodyYaw(LocalPlayer self) {
|
||||
public void updateVisualBodyYaw(LocalPlayer self) {
|
||||
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
|
||||
if (rotation == null) {
|
||||
rotationPrevX = self.getX();
|
||||
|
|
@ -67,7 +67,7 @@ final class LocalPlayerRotationHandler {
|
|||
rotationPrevZ = self.getZ();
|
||||
}
|
||||
|
||||
float simulateBodyYaw(float yaw, float prevBodyYaw, double prevX, double prevZ, double currentX, double currentZ, float handSwing) {
|
||||
public float simulateBodyYaw(float yaw, float prevBodyYaw, double prevX, double prevZ, double currentX, double currentZ, float handSwing) {
|
||||
double motionX = currentX - prevX;
|
||||
double motionZ = currentZ - prevZ;
|
||||
float motionSquared = (float) (motionX * motionX + motionZ * motionZ);
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package dev.loki.lovisual.mixins.entity.player;
|
||||
|
||||
import dev.loki.lovisual.mixinhandlers.player.LocalPlayerInputHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.player.LocalPlayerRotationHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.player.LocalPlayerNetworkHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.player.LocalPlayerMovementHandler;
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import dev.loki.lovisual.events.impl.input.SprintControlEvent;
|
||||
import dev.loki.lovisual.mixininterface.entity.ILocalPlayer;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package dev.loki.lovisual.mixins.gui.chat;
|
||||
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import dev.loki.lovisual.Lifecycle;
|
||||
import dev.loki.lovisual.features.command.core.CommandManager;
|
||||
import dev.loki.lovisual.features.command.util.CommandPrefix;
|
||||
import dev.loki.lovisual.features.command.suggest.SuggestionPlan;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
|
||||
import net.minecraft.client.gui.components.CommandSuggestions;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Baritone-style autocomplete for client commands in the vanilla chat: while the input starts
|
||||
* with the command prefix, the popup lists our commands and arguments as you type.
|
||||
* BetterChat draws its own popup, so it keeps its existing path.
|
||||
*/
|
||||
@Mixin(CommandSuggestions.class)
|
||||
public abstract class CommandSuggestionsMixin {
|
||||
@Shadow @Final private EditBox input;
|
||||
@Shadow @Final private List<FormattedCharSequence> commandUsage;
|
||||
@Shadow private CompletableFuture<Suggestions> pendingSuggestions;
|
||||
@Shadow private CommandSuggestions.SuggestionsList suggestions;
|
||||
@Shadow private boolean keepSuggestions;
|
||||
|
||||
@Shadow
|
||||
public abstract void showSuggestions(boolean narrateFirstSuggestion);
|
||||
|
||||
@Inject(method = "updateCommandInfo", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$clientCommandSuggestions(CallbackInfo ci) {
|
||||
if (!Lifecycle.isActive() || BetterChat.isActive()) return;
|
||||
String text = input.getValue();
|
||||
if (!CommandPrefix.isCommandLike(text)) return;
|
||||
|
||||
ci.cancel();
|
||||
commandUsage.clear();
|
||||
if (!keepSuggestions) {
|
||||
input.setSuggestion(null);
|
||||
suggestions = null;
|
||||
}
|
||||
SuggestionPlan plan = SuggestionPlan.of(CommandManager.suggest(text, input.getCursorPosition()));
|
||||
if (plan.suggestions() != null) {
|
||||
pendingSuggestions = CompletableFuture.completedFuture(plan.suggestions());
|
||||
showSuggestions(false);
|
||||
} else if (plan.ghostHint() != null && !keepSuggestions) {
|
||||
input.setSuggestion(" " + plan.ghostHint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import org.spongepowered.asm.mixin.Mixin;
|
|||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.LoVisualMainMenuScreen;
|
||||
import dev.loki.lovisual.features.gui.mainmenu.MainMenus;
|
||||
|
||||
@Mixin(TitleScreen.class)
|
||||
public abstract class TitleScreenMixin {
|
||||
|
|
@ -15,11 +15,11 @@ public abstract class TitleScreenMixin {
|
|||
@Inject(method = "init", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$replaceTitleScreen(CallbackInfo ci) {
|
||||
|
||||
if (LoVisualMainMenuScreen.shouldUseVanillaTitleScreen()) return;
|
||||
if (MainMenus.shouldUseVanillaTitleScreen()) return;
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null) return;
|
||||
if (ClientScreen.current() instanceof LoVisualMainMenuScreen) return;
|
||||
ClientScreen.show(mc, new LoVisualMainMenuScreen());
|
||||
if (MainMenus.isMainMenu(ClientScreen.current())) return;
|
||||
ClientScreen.show(mc, MainMenus.create());
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package dev.loki.lovisual.mixins.hud;
|
||||
|
||||
import dev.loki.lovisual.mixinhandlers.hud.GuiOverlayHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.hud.GuiHudBarHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.hud.GuiHealthLayoutHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.hud.GuiTooltipHandler;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package dev.loki.lovisual.mixins.network.game;
|
||||
|
||||
import dev.loki.lovisual.mixinhandlers.network.ClientPacketListenerSoundHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.network.ClientPacketListenerStaffHandler;
|
||||
import dev.loki.lovisual.mixinhandlers.network.ClientPacketListenerPvpHandler;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import org.slf4j.Logger;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
|||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.HandScripts;
|
||||
import dev.loki.lovisual.util.animation.hand.HandDynamics;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel;
|
||||
import dev.loki.lovisual.util.animation.EquipProgressComposer;
|
||||
|
|
@ -31,6 +32,8 @@ import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams;
|
|||
public abstract class ItemInHandRendererMixin {
|
||||
|
||||
@Unique private boolean lovisual$handPosePushed;
|
||||
@Unique private boolean lovisual$scriptPosePushed;
|
||||
@Unique private boolean lovisual$itemPosePushed;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
|
||||
private void lovisual$skipTickWithoutPlayer(CallbackInfo ci) {
|
||||
|
|
@ -56,7 +59,11 @@ public abstract class ItemInHandRendererMixin {
|
|||
HandAnimState handAnim = holdMyItems.handAnim();
|
||||
if (handAnim == null) return;
|
||||
|
||||
lovisual$handPosePushed = HandDynamics.apply(holdMyItems, handAnim, player, tickDelta, hand, item, matrices);
|
||||
if (holdMyItems.dynamicsActive()) {
|
||||
lovisual$handPosePushed = HandDynamics.apply(holdMyItems, handAnim, player, tickDelta, hand, item, matrices);
|
||||
}
|
||||
lovisual$scriptPosePushed = HandScripts.applyHandPose(
|
||||
holdMyItems, player, hand, swingProgress, equipProgress, tickDelta, item, matrices);
|
||||
}
|
||||
|
||||
@Inject(method = "submitArmWithItem", at = @At("RETURN"))
|
||||
|
|
@ -65,27 +72,20 @@ public abstract class ItemInHandRendererMixin {
|
|||
float swingProgress, ItemStack item, float equipProgress, PoseStack matrices,
|
||||
SubmitNodeCollector queue, int light, CallbackInfo ci
|
||||
) {
|
||||
if (lovisual$scriptPosePushed) {
|
||||
lovisual$scriptPosePushed = false;
|
||||
matrices.popPose();
|
||||
}
|
||||
if (!lovisual$handPosePushed) return;
|
||||
lovisual$handPosePushed = false;
|
||||
matrices.popPose();
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "submitArmWithItem",
|
||||
at = @At("HEAD")
|
||||
)
|
||||
@Inject(method = "submitArmWithItem", at = @At("HEAD"))
|
||||
private void lovisual$applyOffsets(
|
||||
AbstractClientPlayer player,
|
||||
float tickDelta,
|
||||
float pitch,
|
||||
InteractionHand hand,
|
||||
float swingProgress,
|
||||
ItemStack item,
|
||||
float equipProgress,
|
||||
PoseStack matrices,
|
||||
SubmitNodeCollector queue,
|
||||
int light,
|
||||
CallbackInfo ci
|
||||
AbstractClientPlayer player, float tickDelta, float pitch, InteractionHand hand,
|
||||
float swingProgress, ItemStack item, float equipProgress, PoseStack matrices,
|
||||
SubmitNodeCollector queue, int light, CallbackInfo ci
|
||||
) {
|
||||
ViewModel viewModel = Modules.get(ViewModel.class);
|
||||
if (viewModel == null || !viewModel.isEnabled()) return;
|
||||
|
|
@ -112,7 +112,7 @@ public abstract class ItemInHandRendererMixin {
|
|||
ViewModel viewModel = Modules.get(ViewModel.class);
|
||||
HoldMyItems holdMyItems = Modules.get(HoldMyItems.class);
|
||||
boolean lowerEnabled = viewModel != null && viewModel.isEnabled();
|
||||
boolean curveEnabled = holdMyItems != null && holdMyItems.isEnabled()
|
||||
boolean curveEnabled = holdMyItems != null && holdMyItems.isEnabled() && holdMyItems.dynamicsActive()
|
||||
&& !(hand == InteractionHand.OFF_HAND && !holdMyItems.offhandToo.get());
|
||||
if (!lowerEnabled && !curveEnabled) return equipProgress;
|
||||
return EquipProgressComposer.compose(
|
||||
|
|
@ -146,6 +146,8 @@ public abstract class ItemInHandRendererMixin {
|
|||
int light,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
HoldMyItems held = Modules.get(HoldMyItems.class);
|
||||
if (held != null) lovisual$itemPosePushed = HandScripts.applyItemPose(held, entity, stack, ctx, ms);
|
||||
ViewModel viewModel = Modules.get(ViewModel.class);
|
||||
if (viewModel == null || !viewModel.isEnabled()) return;
|
||||
if (!ctx.firstPerson()) return;
|
||||
|
|
@ -155,6 +157,16 @@ public abstract class ItemInHandRendererMixin {
|
|||
}
|
||||
}
|
||||
|
||||
@Inject(method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemDisplayContext;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V",
|
||||
at = @At("RETURN"))
|
||||
private void lovisual$restoreItemPose(
|
||||
LivingEntity entity, ItemStack stack, ItemDisplayContext ctx, PoseStack ms,
|
||||
SubmitNodeCollector queue, int light, CallbackInfo ci) {
|
||||
if (!lovisual$itemPosePushed) return;
|
||||
lovisual$itemPosePushed = false;
|
||||
ms.popPose();
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "swingArm",
|
||||
at = @At("HEAD"),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import org.spongepowered.asm.mixin.injection.Redirect;
|
|||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import dev.loki.lovisual.config.i18n.MainConfig;
|
||||
import dev.loki.lovisual.render.gamerenderer.GameRendererProjectionHelper;
|
||||
import dev.loki.lovisual.render.gamerenderer.GameRendererWorldEngine;
|
||||
import dev.loki.lovisual.render.gamerenderer.GameRendererWorldPost;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.camera.AspectRatio;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue