chore(history): squash 6 commit(s) from 2026-09-16
- feat(balancer): новый модуль Balancer + команда %tier — тиры с mctiers.com - docs(todo): отмечен Balancer как выполненный - feat(trinket): новый модуль Trinket — прыгающий шарик-компаньон у ног игрока - docs(todo): отмечен Trinket как выполненный - feat(chams): добавлен режим Invert руки (HandsShader) — закрывает Фазу 7 - docs(todo): Фаза 7 полностью закрыта
This commit is contained in:
parent
4a73a70dbf
commit
25060c9fa9
14 changed files with 781 additions and 17 deletions
43
TODO.md
43
TODO.md
|
|
@ -28,8 +28,9 @@
|
||||||
- [x] Фаза 4: Security (BackdoorProtection, StaffTracker)
|
- [x] Фаза 4: Security (BackdoorProtection, StaffTracker)
|
||||||
- [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock)
|
- [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock)
|
||||||
- [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md`
|
- [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md`
|
||||||
- [ ] Фаза 7: Новые модули по мотивам SoupVisuals — Cosmetics Overhaul ЗАВЕРШЁН 2026-09-11,
|
- [x] Фаза 7: Новые модули по мотивам SoupVisuals — ЗАВЕРШЕНА 2026-09-16 (Cosmetics
|
||||||
см. ниже (остатки: Gps, HitBubbles, AmbientParticles, EndCrystal, Trinket, Balancer, HandsShader)
|
Overhaul 2026-09-11; Gps/HitBubbles/AmbientParticles/EndCrystal/Balancer/Trinket/
|
||||||
|
HandsShader — 2026-09-15…16, см. ниже)
|
||||||
- [ ] Фаза 8.5: Аудит структуры ≤200/≤4 — 8.5.1 (папки >4) ЗАВЕРШЕНА 2026-09-10
|
- [ ] Фаза 8.5: Аудит структуры ≤200/≤4 — 8.5.1 (папки >4) ЗАВЕРШЕНА 2026-09-10
|
||||||
(0 папок >4, коммиты 8846d53…303b17e); далее 8.5.2 (гиганты >600),
|
(0 папок >4, коммиты 8846d53…303b17e); далее 8.5.2 (гиганты >600),
|
||||||
план в `combatant-client-26.2/TODO.md`
|
план в `combatant-client-26.2/TODO.md`
|
||||||
|
|
@ -76,10 +77,15 @@
|
||||||
ваниль убирает сущность мгновенно). Референс: `other/EndCrystal.java` — идея взята
|
ваниль убирает сущность мгновенно). Референс: `other/EndCrystal.java` — идея взята
|
||||||
(кастомный цвет, "доиграть" разрушение), Sync/Custom×4-цвета+Wave/Vertex упрощены до
|
(кастомный цвет, "доиграть" разрушение), Sync/Custom×4-цвета+Wave/Vertex упрощены до
|
||||||
общего AnimatedRenderColors. Коммит 50db8e3.
|
общего AnimatedRenderColors. Коммит 50db8e3.
|
||||||
- [ ] **Trinket 2.0** — апгрейд нашего CosmeticDisplay до уровня Soup Trinket:
|
- [x] **Trinket** (не "2.0" — CosmeticDisplay удалён откатом 2026-09-11, апгрейдить нечего) —
|
||||||
иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл
|
ЗАВЕРШЕНО 2026-09-16: отдельный модуль `visuals/items/Trinket.java` в стиле текущих
|
||||||
(цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы
|
ChinaHat/TazikHat. `util/physics/TrinketPhysics` (чистая, тесты) — шарик на пружине
|
||||||
(у Soup 315 стр.). Референс: `other/Trinket.java`
|
у ног игрока (гравитация/отскок/тетер). Рендер — тонированный billboard (наш
|
||||||
|
AnimatedRenderColors) + затухающий шлейф (кольцевой буфер) + звук на отскок
|
||||||
|
(ванильный SoundEvents.SLIME_JUMP). Библиотека 10 иконок-спутников референса и
|
||||||
|
8 кастомных звуковых тем НЕ портированы — нужны новые ассеты. Референс:
|
||||||
|
`other/Trinket.java` (там тоже почти нет реализации — только настройки).
|
||||||
|
Коммит 840a716.
|
||||||
- [x] **Cosmetics Overhaul (Unified Cosmetics)** — ЗАВЕРШЁН 2026-09-11:
|
- [x] **Cosmetics Overhaul (Unified Cosmetics)** — ЗАВЕРШЁН 2026-09-11:
|
||||||
слоты `CosmeticSlot` (MODEL/PET/HEAD/SHOULDERS/BACK/WAIST/LEGS/HELD), парсинг
|
слоты `CosmeticSlot` (MODEL/PET/HEAD/SHOULDERS/BACK/WAIST/LEGS/HELD), парсинг
|
||||||
`slot` из avatar.json + эвристика по имени папки, `CosmeticManager` на EnumMap
|
`slot` из avatar.json + эвристика по имени папки, `CosmeticManager` на EnumMap
|
||||||
|
|
@ -95,14 +101,23 @@
|
||||||
- Не сделано из плана (следующий заход): иконки-превью built-in в списке слота,
|
- Не сделано из плана (следующий заход): иконки-превью built-in в списке слота,
|
||||||
правка слотов SHOULDERS/LEGS/PET из экрана (сейчас только HEAD/BACK built-in),
|
правка слотов SHOULDERS/LEGS/PET из экрана (сейчас только HEAD/BACK built-in),
|
||||||
предпросмотр до эквипа.
|
предпросмотр до эквипа.
|
||||||
- [ ] **Balancer** (mctiers) — тиры игроков с mctiers.info (у Soup отдельная категория
|
- [x] **Balancer** (mctiers) — ЗАВЕРШЕНО 2026-09-16: `features/module/modules/misc/Balancer.java`
|
||||||
MCTiers). Референс: `mctiers/Balancer.java`
|
(fixType Vanilla/UHC/Pot/NethOP/SMP/Sword/Axe/Mace) + `util/mctiers/MCTiersCache`
|
||||||
- [ ] **HandsShader: новые шейдеры руки** — у Soup 10 режимов (Solid, Chroma, Balatro,
|
(асинхронный кэш, паттерн SkinManager) + `util/mctiers/MCTiersTier` (форматирование,
|
||||||
Smoke, Stripes, Glow, Glass, Invert, Snow) + dual-режим с blend-режимами
|
тесты) + команда `%tier <ник> [режим]`. Референс `mctiers/Balancer.java` почти пуст —
|
||||||
(Mix/Add/Multiply/Screen/Overlay/Difference); шейдеры лежат в
|
логика была в недоступной McTiersModule-базе; реальный публичный API mctiers.com
|
||||||
`ref/soupvisuals-src/assets/soupapi/shaders/hand/`. Пере-реализовать недостающие
|
найден и проверен через сторонний open-source плагин (sinnayuh/MCTier):
|
||||||
и добавить к нашим (BackSword/Chams hand shaders уже есть dual — сверить набор).
|
`GET https://mctiers.com/api/search_profile/<username>`. Коммит cf81ee4.
|
||||||
Референс: `visuals/HandsShader.java`
|
- [x] **HandsShader** — ЗАВЕРШЕНО 2026-09-16: сверка показала, что Chams уже реализует
|
||||||
|
9/10 режимов Soup (Solid/Chroma/Balatro/Smoke/Stripes/Glow/Glass/Snow + свои
|
||||||
|
Metallic/Noise) и полный dual/blend-набор (Mix/Add/Multiply/Screen/Overlay/
|
||||||
|
Difference, `HandBlendUniforms`) — сверх ожиданий. Не хватало только "Invert":
|
||||||
|
добавлен `hand_chams_invert.frag` (RGB-инверт под маской руки через уже
|
||||||
|
существующий UBO HandChams/u_Mix.x, без HSV-тумблеров и edge-glow референса —
|
||||||
|
это дублировало бы наш Glow) + регистрация пайплайна + вписан в оба mode-списка
|
||||||
|
Chams (основной и dual shader1/shader2). Референс: `visuals/HandsShader.java`.
|
||||||
|
Коммит 76ffdec. **Это был последний пункт Фазы 7** — SoupVisuals-раздел закрыт
|
||||||
|
полностью (Gps/HitBubbles/AmbientParticles/EndCrystal/Balancer/Trinket/HandsShader).
|
||||||
- [x] **Фикс ChinaHat** — текущая геометрия кривая (смахивает на самбреро/зонт);
|
- [x] **Фикс ChinaHat** — текущая геометрия кривая (смахивает на самбреро/зонт);
|
||||||
переделать модель в нормальную конусную шляпу по референсу Soup
|
переделать модель в нормальную конусную шляпу по референсу Soup
|
||||||
(`visuals/ChinaHat.java` + `ChinaHatModel.java`).
|
(`visuals/ChinaHat.java` + `ChinaHatModel.java`).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.features.command.impl.world;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.features.command.core.CommandContext;
|
||||||
|
import dev.loki.lovisual.features.command.core.CommandInfo;
|
||||||
|
import dev.loki.lovisual.features.command.util.ClientCommand;
|
||||||
|
import dev.loki.lovisual.features.command.util.CommandOutput;
|
||||||
|
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||||
|
import dev.loki.lovisual.features.module.modules.misc.Balancer;
|
||||||
|
import dev.loki.lovisual.util.mctiers.MCTiersCache;
|
||||||
|
import dev.loki.lovisual.util.mctiers.MCTiersTier;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code %tier <ник> [режим]} — тир игрока с mctiers.com. Первый запрос уходит
|
||||||
|
* в фон (см. {@link MCTiersCache}) и почти всегда возвращает null сразу же —
|
||||||
|
* повторный вызов команды через пару секунд покажет результат из кэша.
|
||||||
|
*/
|
||||||
|
@CommandInfo(
|
||||||
|
id = "tier",
|
||||||
|
aliases = {"mctier"},
|
||||||
|
usage = "%tier <player> [vanilla|uhc|pot|nethop|smp|sword|axe|mace]",
|
||||||
|
descriptionKey = "command.tier.description"
|
||||||
|
)
|
||||||
|
public final class TierCommand implements ClientCommand {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean execute(CommandContext ctx) {
|
||||||
|
String player = ctx.arg(0);
|
||||||
|
if (player == null || player.isBlank()) {
|
||||||
|
CommandOutput.warning("Usage: " + metadata().usage());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Balancer balancer = Modules.get(Balancer.class);
|
||||||
|
String fixType = balancer != null ? balancer.fixType() : "Vanilla";
|
||||||
|
String override = ctx.arg(1);
|
||||||
|
if (override != null && !override.isBlank()) {
|
||||||
|
fixType = normalizeFixType(override);
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiMode = MCTiersTier.modeApiKey(fixType);
|
||||||
|
MCTiersTier tier = MCTiersCache.getTier(player, apiMode);
|
||||||
|
if (tier == null) {
|
||||||
|
CommandOutput.send("Fetching mctiers.com data for " + player + "... run %tier again in a moment.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandOutput.success(String.format(Locale.ROOT, "%s — %s: %s%s",
|
||||||
|
player, fixType, tier.label(), tier.retired() ? " (retired)" : ""));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeFixType(String raw) {
|
||||||
|
return switch (raw.toLowerCase(Locale.ROOT)) {
|
||||||
|
case "uhc" -> "UHC";
|
||||||
|
case "pot" -> "Pot";
|
||||||
|
case "nethop", "neth_pot", "netherite" -> "NethOP";
|
||||||
|
case "smp" -> "SMP";
|
||||||
|
case "sword" -> "Sword";
|
||||||
|
case "axe" -> "Axe";
|
||||||
|
case "mace" -> "Mace";
|
||||||
|
default -> "Vanilla";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||||
|
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.util.mctiers.MCTiersCache;
|
||||||
|
import dev.loki.lovisual.util.mctiers.MCTiersTier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тиры игроков с mctiers.com (референс — Soup Balancer): выбор игрового
|
||||||
|
* режима, тир по которому смотрим. Референсный Balancer сам почти не содержит
|
||||||
|
* логики — она была в недоступной нам {@code McTiersModule}-базе; сеть/кэш/
|
||||||
|
* парсинг написаны заново в {@link MCTiersCache}. Смотреть тир игрока:
|
||||||
|
* {@code %tier <ник>} (см. {@code TierCommand}) или {@link #tierFor(String)}
|
||||||
|
* для точечной интеграции в другие модули (например NameTags).
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "balancer",
|
||||||
|
displayName = "Balancer",
|
||||||
|
category = ModuleCategory.MISC,
|
||||||
|
description = "Тиры игроков с mctiers.com по выбранному игровому режиму"
|
||||||
|
)
|
||||||
|
public final class Balancer extends Module {
|
||||||
|
|
||||||
|
private final ModeValue fixType = modeSetting("fixType", "fix_type", "Vanilla",
|
||||||
|
"Vanilla", "UHC", "Pot", "NethOP", "SMP", "Sword", "Axe", "Mace");
|
||||||
|
|
||||||
|
public String fixType() {
|
||||||
|
return fixType.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return тир игрока по текущему выбранному режиму, либо null (нет данных ещё/вообще). */
|
||||||
|
public MCTiersTier tierFor(String playerName) {
|
||||||
|
return MCTiersCache.getTier(playerName, MCTiersTier.modeApiKey(fixType.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.items;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.core.Module;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.texture.TextureStorage;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.util.physics.TrinketPhysics;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.player.LocalPlayer;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Компаньон-шарик, подвешенный у ног игрока: падает под гравитацией,
|
||||||
|
* отскакивает, тянется обратно пружиной (см. {@link TrinketPhysics} — чистая
|
||||||
|
* симуляция, покрыта тестами), оставляет затухающий шлейф и звенит при
|
||||||
|
* отскоке. Референс: Soup other/Trinket.java — модуль-настройки без
|
||||||
|
* реализации (физика/рендер были в недоступной нам базе), физика и рендер
|
||||||
|
* написаны с нуля под наш движок. Библиотека из 10 иконок-спутников
|
||||||
|
* (Atom/Ball/Cat/...) и полный набор из 8 звуковых тем референса не
|
||||||
|
* портированы — нужны новые ассеты; вместо этого светящийся шарик тонируется
|
||||||
|
* через уже существующий {@link AnimatedRenderColors}, а звук — из ванильных
|
||||||
|
* {@link SoundEvents} (без новых .ogg).
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "trinket",
|
||||||
|
displayName = "Trinket",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Компаньон-шарик у ног игрока: физика прыжков, шлейф, звук отскока"
|
||||||
|
)
|
||||||
|
public final class Trinket extends Module {
|
||||||
|
|
||||||
|
private static final int TRAIL_CAPACITY = 24;
|
||||||
|
private static final double TICK_DT = 0.05;
|
||||||
|
|
||||||
|
private final NumberValue<Float> gravity = num("gravity", 20.0f, 5.0f, 40.0f);
|
||||||
|
private final NumberValue<Float> weight = num("weight", 1.0f, 0.2f, 2.0f);
|
||||||
|
private final NumberValue<Float> friction = num("friction", 0.98f, 0.8f, 1.0f);
|
||||||
|
private final NumberValue<Float> bounce = num("bounce", 0.7f, 0.0f, 1.0f);
|
||||||
|
private final NumberValue<Float> groundFriction = num("ground_friction", 0.9f, 0.7f, 1.0f);
|
||||||
|
private final NumberValue<Float> tetherRadius = num("tether_radius", 1.2f, 0.3f, 3.0f);
|
||||||
|
private final NumberValue<Float> iconSize = num("icon_size", 0.28f, 0.1f, 0.6f);
|
||||||
|
|
||||||
|
private final ModeValue colorMode = modeSetting("colorMode", "color_mode", "Static", AnimatedColorModeOptions.OPTIONS);
|
||||||
|
private final NumberValue<Integer> colorSpeed = num("color_speed", 18, 2, 54);
|
||||||
|
private final RGBAColorValue colorValue = color("color", "#FF7FE0FF");
|
||||||
|
private final RGBAColorValue colorValue2 = visibleWhen(color("color2", "#FFFF9F40"), this::usesSecondaryColor);
|
||||||
|
|
||||||
|
private final BooleanValue trailEnabled = bool("trail_enabled", true);
|
||||||
|
private final NumberValue<Integer> trailLength = visibleWhen(num("trail_length", 15, 5, TRAIL_CAPACITY), trailEnabled::get);
|
||||||
|
|
||||||
|
private final BooleanValue soundEnabled = bool("sound_enabled", true);
|
||||||
|
private final NumberValue<Float> soundVolume = visibleWhen(num("sound_volume", 0.5f, 0.0f, 1.5f), soundEnabled::get);
|
||||||
|
|
||||||
|
private final TrinketPhysics physics = new TrinketPhysics();
|
||||||
|
private final double[] trailX = new double[TRAIL_CAPACITY];
|
||||||
|
private final double[] trailY = new double[TRAIL_CAPACITY];
|
||||||
|
private final double[] trailZ = new double[TRAIL_CAPACITY];
|
||||||
|
private int trailHead;
|
||||||
|
private int trailCount;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
physics.reset();
|
||||||
|
trailHead = 0;
|
||||||
|
trailCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean usesSecondaryColor() {
|
||||||
|
return AnimatedRenderColors.usesSecondary(AnimatedColorModeOptions.parse(colorMode.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveColorArgb() {
|
||||||
|
return AnimatedRenderColors.resolve(
|
||||||
|
AnimatedColorModeOptions.parse(colorMode.get()), colorSpeed.get(), 0,
|
||||||
|
colorValue.getArgb(), colorValue2.getArgb());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
LocalPlayer player = mc.player;
|
||||||
|
if (!isEnabled() || player == null || mc.level == null) return;
|
||||||
|
|
||||||
|
boolean bounced = physics.tick(TICK_DT, gravity.get(), weight.get(), friction.get(),
|
||||||
|
bounce.get(), groundFriction.get(), tetherRadius.get());
|
||||||
|
|
||||||
|
if (trailEnabled.get()) {
|
||||||
|
trailX[trailHead] = physics.x();
|
||||||
|
trailY[trailHead] = physics.y();
|
||||||
|
trailZ[trailHead] = physics.z();
|
||||||
|
trailHead = (trailHead + 1) % trailX.length;
|
||||||
|
if (trailCount < trailX.length) trailCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounced && soundEnabled.get() && soundVolume.get() > 0.001f) {
|
||||||
|
Vec3 pos = player.position();
|
||||||
|
mc.level.playSound(player, pos.x, pos.y, pos.z,
|
||||||
|
SoundEvents.SLIME_JUMP, SoundSource.PLAYERS, soundVolume.get(), 1.4f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
physics.reset();
|
||||||
|
trailCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.END_MAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (!isEnabled() || mc.player == null) return;
|
||||||
|
|
||||||
|
Vec3 anchor = mc.player.getPosition(tickDelta);
|
||||||
|
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, TextureStorage.FIRE_FLY, Renderer3D.DepthMode.MAIN);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
Quaternionf camRot = RenderState.cameraRotation;
|
||||||
|
int color = resolveColorArgb();
|
||||||
|
|
||||||
|
if (trailEnabled.get() && trailCount > 1) {
|
||||||
|
int visible = Math.min(trailCount, Math.max(2, trailLength.get()));
|
||||||
|
for (int i = 0; i < visible; i++) {
|
||||||
|
int idx = Math.floorMod(trailHead - 1 - i, trailX.length);
|
||||||
|
float fade = 1.0f - (i / (float) visible);
|
||||||
|
if (fade <= 0.02f) continue;
|
||||||
|
int argb = applyOpacity(color, fade * 0.6f);
|
||||||
|
double x = anchor.x + trailX[idx];
|
||||||
|
double y = anchor.y + trailY[idx];
|
||||||
|
double z = anchor.z + trailZ[idx];
|
||||||
|
addBillboard(mesh, x, y, z, iconSize.get() * (0.4f + 0.4f * fade), camRot, argb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double x = anchor.x + physics.x();
|
||||||
|
double y = anchor.y + physics.y();
|
||||||
|
double z = anchor.z + physics.z();
|
||||||
|
addBillboard(mesh, x, y, z, iconSize.get(), camRot, applyOpacity(color, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addBillboard(MeshBuilder mesh, double cx, double cy, double cz,
|
||||||
|
float size, Quaternionf camRot, int argb) {
|
||||||
|
Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size);
|
||||||
|
Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(cx - right.x() - up.x(), cy - right.y() - up.y(), cz - right.z() - up.z()).vec2(0, 1).color(new RenderColor(argb)).next();
|
||||||
|
int i2 = mesh.vec3(cx + right.x() - up.x(), cy + right.y() - up.y(), cz + right.z() - up.z()).vec2(1, 1).color(new RenderColor(argb)).next();
|
||||||
|
int i3 = mesh.vec3(cx + right.x() + up.x(), cy + right.y() + up.y(), cz + right.z() + up.z()).vec2(1, 0).color(new RenderColor(argb)).next();
|
||||||
|
int i4 = mesh.vec3(cx - right.x() + up.x(), cy - right.y() + up.y(), cz - right.z() + up.z()).vec2(0, 0).color(new RenderColor(argb)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int applyOpacity(int argb, float opacity) {
|
||||||
|
int a = (argb >>> 24) & 0xFF;
|
||||||
|
int na = Mth.clamp((int) (a * Mth.clamp(opacity, 0f, 1f)), 0, 255);
|
||||||
|
return (na << 24) | (argb & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -129,7 +129,7 @@ public class Chams extends Module {
|
||||||
private static final float NOISE_BREATHE = 0.55f;
|
private static final float NOISE_BREATHE = 0.55f;
|
||||||
private static final float NOISE_DENSITY = 1.0f;
|
private static final float NOISE_DENSITY = 1.0f;
|
||||||
private final ModeValue mode =
|
private final ModeValue mode =
|
||||||
modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro");
|
modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert");
|
||||||
|
|
||||||
private final BooleanValue hands =
|
private final BooleanValue hands =
|
||||||
bool("chamsHands", SETTING_HANDS, true);
|
bool("chamsHands", SETTING_HANDS, true);
|
||||||
|
|
@ -198,9 +198,9 @@ public class Chams extends Module {
|
||||||
private final BooleanValue dualShader =
|
private final BooleanValue dualShader =
|
||||||
bool("chamsDualShader", SETTING_DUAL_SHADER, false);
|
bool("chamsDualShader", SETTING_DUAL_SHADER, false);
|
||||||
private final ModeValue shader1 =
|
private final ModeValue shader1 =
|
||||||
visibleWhen(modeSetting("chamsShader1", SETTING_SHADER_1, "Glow", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"), dualShader::get);
|
visibleWhen(modeSetting("chamsShader1", SETTING_SHADER_1, "Glow", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"), dualShader::get);
|
||||||
private final ModeValue shader2 =
|
private final ModeValue shader2 =
|
||||||
visibleWhen(modeSetting("chamsShader2", SETTING_SHADER_2, "Chroma", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"), dualShader::get);
|
visibleWhen(modeSetting("chamsShader2", SETTING_SHADER_2, "Chroma", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"), dualShader::get);
|
||||||
private final ModeValue blendMode =
|
private final ModeValue blendMode =
|
||||||
visibleWhen(modeSetting("chamsBlendMode", SETTING_BLEND_MODE, "Add", "Mix", "Add", "Multiply", "Screen", "Overlay", "Difference"), dualShader::get);
|
visibleWhen(modeSetting("chamsBlendMode", SETTING_BLEND_MODE, "Add", "Mix", "Add", "Multiply", "Screen", "Overlay", "Difference"), dualShader::get);
|
||||||
private final NumberValue<Float> blendIntensity =
|
private final NumberValue<Float> blendIntensity =
|
||||||
|
|
@ -845,6 +845,9 @@ public class Chams extends Module {
|
||||||
glowPowerV = glowShaderPower.get();
|
glowPowerV = glowShaderPower.get();
|
||||||
glowDisp = glowShaderDispersion.get();
|
glowDisp = glowShaderDispersion.get();
|
||||||
}
|
}
|
||||||
|
case "Invert" -> {
|
||||||
|
// шейдер сам ничего не читает из colorA..D — эффект чисто по u_Mix.x/u_Flags.x
|
||||||
|
}
|
||||||
default -> {
|
default -> {
|
||||||
colorA = balatroColor1.getArgb();
|
colorA = balatroColor1.getArgb();
|
||||||
colorB = balatroColor2.getArgb();
|
colorB = balatroColor2.getArgb();
|
||||||
|
|
@ -863,6 +866,7 @@ public class Chams extends Module {
|
||||||
case "Solid" -> LoVisualRenderPipelines.HAND_CHAMS_SOLID;
|
case "Solid" -> LoVisualRenderPipelines.HAND_CHAMS_SOLID;
|
||||||
case "Stripes" -> LoVisualRenderPipelines.HAND_CHAMS_STRIPES;
|
case "Stripes" -> LoVisualRenderPipelines.HAND_CHAMS_STRIPES;
|
||||||
case "Glow" -> LoVisualRenderPipelines.HAND_CHAMS_GLOW;
|
case "Glow" -> LoVisualRenderPipelines.HAND_CHAMS_GLOW;
|
||||||
|
case "Invert" -> LoVisualRenderPipelines.HAND_CHAMS_INVERT;
|
||||||
default -> LoVisualRenderPipelines.HAND_CHAMS_BALATRO;
|
default -> LoVisualRenderPipelines.HAND_CHAMS_BALATRO;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ public enum LoVisualRenderPipelines {
|
||||||
public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = shader("hand_chams_stripes.frag");
|
public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = shader("hand_chams_stripes.frag");
|
||||||
public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = shader("hand_chams_glow.frag");
|
public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = shader("hand_chams_glow.frag");
|
||||||
public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = shader("hand_chams_snow.frag");
|
public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = shader("hand_chams_snow.frag");
|
||||||
|
public static final Identifier SHADER_HAND_CHAMS_INVERT_FRAG = shader("hand_chams_invert.frag");
|
||||||
public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = shader("hand_chams_balatro.frag");
|
public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = shader("hand_chams_balatro.frag");
|
||||||
public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = shader("hand_chams_blend.frag");
|
public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = shader("hand_chams_blend.frag");
|
||||||
// Sky & menu backgrounds
|
// Sky & menu backgrounds
|
||||||
|
|
@ -303,6 +304,7 @@ public enum LoVisualRenderPipelines {
|
||||||
public static final RenderPipeline HAND_CHAMS_STRIPES = add(ScreenPipelines.handEffect("hand_chams_stripes", SHADER_HAND_CHAMS_STRIPES_FRAG, "HandChams"));
|
public static final RenderPipeline HAND_CHAMS_STRIPES = add(ScreenPipelines.handEffect("hand_chams_stripes", SHADER_HAND_CHAMS_STRIPES_FRAG, "HandChams"));
|
||||||
public static final RenderPipeline HAND_CHAMS_GLOW = add(ScreenPipelines.handEffect("hand_chams_glow", SHADER_HAND_CHAMS_GLOW_FRAG, "HandChams"));
|
public static final RenderPipeline HAND_CHAMS_GLOW = add(ScreenPipelines.handEffect("hand_chams_glow", SHADER_HAND_CHAMS_GLOW_FRAG, "HandChams"));
|
||||||
public static final RenderPipeline HAND_CHAMS_SNOW = add(ScreenPipelines.handEffect("hand_chams_snow", SHADER_HAND_CHAMS_SNOW_FRAG, "HandChams"));
|
public static final RenderPipeline HAND_CHAMS_SNOW = add(ScreenPipelines.handEffect("hand_chams_snow", SHADER_HAND_CHAMS_SNOW_FRAG, "HandChams"));
|
||||||
|
public static final RenderPipeline HAND_CHAMS_INVERT = add(ScreenPipelines.handEffect("hand_chams_invert", SHADER_HAND_CHAMS_INVERT_FRAG, "HandChams"));
|
||||||
public static final RenderPipeline HAND_CHAMS_BALATRO = add(ScreenPipelines.handEffect("hand_chams_balatro", SHADER_HAND_CHAMS_BALATRO_FRAG, "HandChams"));
|
public static final RenderPipeline HAND_CHAMS_BALATRO = add(ScreenPipelines.handEffect("hand_chams_balatro", SHADER_HAND_CHAMS_BALATRO_FRAG, "HandChams"));
|
||||||
public static final RenderPipeline HAND_CHAMS_BLEND = add(ScreenPipelines.handBlendEffect("hand_chams_blend", SHADER_HAND_CHAMS_BLEND_FRAG));
|
public static final RenderPipeline HAND_CHAMS_BLEND = add(ScreenPipelines.handBlendEffect("hand_chams_blend", SHADER_HAND_CHAMS_BLEND_FRAG));
|
||||||
/** No depth test; translucent; textured skybox with vanilla sky/fog blending. */
|
/** No depth test; translucent; textured skybox with vanilla sky/fog blending. */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.util.mctiers;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Асинхронный кэш профилей mctiers.com (публичный REST API, без ключа):
|
||||||
|
* {@code GET https://mctiers.com/api/search_profile/<username>} возвращает
|
||||||
|
* {@code rankings.<mode>.{tier,retired}} на игровой режим (vanilla/sword/
|
||||||
|
* uhc/pot/neth_pot/smp/axe/mace). Паттерн кэша и HTTP-вызова — как у
|
||||||
|
* {@link dev.loki.lovisual.features.account.SkinManager} (та же ниша:
|
||||||
|
* не блокировать рендер, тихо ронять сеть).
|
||||||
|
*/
|
||||||
|
public enum MCTiersCache {
|
||||||
|
;
|
||||||
|
|
||||||
|
private static final String API_URL = "https://mctiers.com/api/search_profile/%s";
|
||||||
|
private static final long TTL_MS = 5 * 60_000L;
|
||||||
|
|
||||||
|
private static final Map<String, CachedProfile> CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, Boolean> LOADING = new ConcurrentHashMap<>();
|
||||||
|
private static final Executor EXECUTOR = Executors.newFixedThreadPool(2, runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "LoVisual-MCTiersLookup");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @return тир для режима, либо null если ещё не загружен/нет ранга — запрос уходит в фон сам. */
|
||||||
|
public static MCTiersTier getTier(String playerName, String apiMode) {
|
||||||
|
String key = normalize(playerName);
|
||||||
|
if (key.isEmpty()) return null;
|
||||||
|
|
||||||
|
CachedProfile cached = CACHE.get(key);
|
||||||
|
if (cached != null && System.currentTimeMillis() - cached.fetchedAtMs < TTL_MS) {
|
||||||
|
return cached.rankings.get(apiMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOADING.putIfAbsent(key, Boolean.TRUE) == null) {
|
||||||
|
CompletableFuture.runAsync(() -> load(playerName, key), EXECUTOR);
|
||||||
|
}
|
||||||
|
return cached != null ? cached.rankings.get(apiMode) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearCache() {
|
||||||
|
CACHE.clear();
|
||||||
|
LOADING.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void load(String playerName, String key) {
|
||||||
|
try {
|
||||||
|
JsonObject profile = fetchProfile(playerName);
|
||||||
|
if (profile == null) return;
|
||||||
|
|
||||||
|
Map<String, MCTiersTier> rankings = new ConcurrentHashMap<>();
|
||||||
|
if (profile.has("rankings") && profile.get("rankings").isJsonObject()) {
|
||||||
|
JsonObject rankingsJson = profile.getAsJsonObject("rankings");
|
||||||
|
for (String mode : rankingsJson.keySet()) {
|
||||||
|
JsonObject ranking = rankingsJson.getAsJsonObject(mode);
|
||||||
|
if (ranking == null || !ranking.has("tier")) continue;
|
||||||
|
int tier = ranking.get("tier").getAsInt();
|
||||||
|
boolean retired = ranking.has("retired") && ranking.get("retired").getAsBoolean();
|
||||||
|
rankings.put(mode, new MCTiersTier(tier, retired));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CACHE.put(key, new CachedProfile(rankings, System.currentTimeMillis()));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
} finally {
|
||||||
|
LOADING.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject fetchProfile(String playerName) {
|
||||||
|
try {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) URI
|
||||||
|
.create(String.format(API_URL, playerName))
|
||||||
|
.toURL()
|
||||||
|
.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setInstanceFollowRedirects(true);
|
||||||
|
connection.setConnectTimeout(5000);
|
||||||
|
connection.setReadTimeout(5000);
|
||||||
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
connection.setRequestProperty("User-Agent", "LoVisual");
|
||||||
|
|
||||||
|
if (connection.getResponseCode() != 200) {
|
||||||
|
connection.disconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) {
|
||||||
|
return JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String name) {
|
||||||
|
return name == null ? "" : name.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CachedProfile(Map<String, MCTiersTier> rankings, long fetchedAtMs) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.util.mctiers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Чистое форматирование одной строки рейтинга mctiers.com (тир 1..5,
|
||||||
|
* retired-флаг) в текст/цвет — вынесено из парсинга JSON, чтобы тестить
|
||||||
|
* без сети. Соответствие тиров цветам/подписям взято из публичного формата
|
||||||
|
* mctiers.com (HT1/LT2/LT3/LT4, T5+ как запасной вариант).
|
||||||
|
*/
|
||||||
|
public record MCTiersTier(int tier, boolean retired) {
|
||||||
|
|
||||||
|
public String label() {
|
||||||
|
return switch (tier) {
|
||||||
|
case 1 -> "HT1";
|
||||||
|
case 2 -> "LT2";
|
||||||
|
case 3 -> "LT3";
|
||||||
|
case 4 -> "LT4";
|
||||||
|
default -> "T" + tier;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ARGB, непрозрачный. */
|
||||||
|
public int colorArgb() {
|
||||||
|
return switch (tier) {
|
||||||
|
case 1 -> 0xFFFF5555;
|
||||||
|
case 2 -> 0xFFFF55FF;
|
||||||
|
case 3 -> 0xFF5555FF;
|
||||||
|
case 4 -> 0xFF55FF55;
|
||||||
|
default -> 0xFFAAAAAA;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String modeApiKey(String fixType) {
|
||||||
|
if (fixType == null) return "vanilla";
|
||||||
|
return switch (fixType) {
|
||||||
|
case "UHC" -> "uhc";
|
||||||
|
case "Pot" -> "pot";
|
||||||
|
case "NethOP" -> "neth_pot";
|
||||||
|
case "SMP" -> "smp";
|
||||||
|
case "Sword" -> "sword";
|
||||||
|
case "Axe" -> "axe";
|
||||||
|
case "Mace" -> "mace";
|
||||||
|
default -> "vanilla";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String modeDisplayName(String fixType) {
|
||||||
|
return fixType == null ? "Vanilla" : fixType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.util.physics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Чистая физика подвесной "игрушки"-компаньона (идея Soup Trinket): шарик на
|
||||||
|
* невидимой пружине у ног игрока, падает под гравитацией, отскакивает от
|
||||||
|
* условной земли (offsetY == 0 — уровень ног владельца) и притягивается
|
||||||
|
* обратно, если укатился дальше {@code tetherRadius}. Все координаты — смещение
|
||||||
|
* от игрока, не мировые. Без Minecraft-типов, покрыто тестами.
|
||||||
|
*/
|
||||||
|
public final class TrinketPhysics {
|
||||||
|
|
||||||
|
private static final double SPRING_STRENGTH = 6.0;
|
||||||
|
|
||||||
|
private double x, y, z;
|
||||||
|
private double vx, vy, vz;
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
x = y = z = 0.0;
|
||||||
|
vx = vy = vz = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Один шаг симуляции.
|
||||||
|
*
|
||||||
|
* @return true, если в этом шаге произошёл отскок от земли (offsetY достиг 0 сверху)
|
||||||
|
*/
|
||||||
|
public boolean tick(double dt, double gravity, double weight, double friction,
|
||||||
|
double bounce, double groundFriction, double tetherRadius) {
|
||||||
|
vy -= gravity * weight * dt;
|
||||||
|
|
||||||
|
x += vx * dt;
|
||||||
|
y += vy * dt;
|
||||||
|
z += vz * dt;
|
||||||
|
|
||||||
|
boolean bounced = false;
|
||||||
|
if (y <= 0.0 && vy < 0.0) {
|
||||||
|
y = 0.0;
|
||||||
|
vy = -vy * bounce;
|
||||||
|
vx *= groundFriction;
|
||||||
|
vz *= groundFriction;
|
||||||
|
bounced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double horizontalDist = Math.sqrt(x * x + z * z);
|
||||||
|
if (horizontalDist > tetherRadius && horizontalDist > 1.0e-6) {
|
||||||
|
double pull = (horizontalDist - tetherRadius) * SPRING_STRENGTH * dt;
|
||||||
|
double nx = x / horizontalDist;
|
||||||
|
double nz = z / horizontalDist;
|
||||||
|
vx -= nx * pull;
|
||||||
|
vz -= nz * pull;
|
||||||
|
}
|
||||||
|
|
||||||
|
vx *= friction;
|
||||||
|
vz *= friction;
|
||||||
|
|
||||||
|
return bounced;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double x() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double y() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double z() {
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double speed() {
|
||||||
|
return Math.sqrt(vx * vx + vy * vy + vz * vz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3231,6 +3231,7 @@
|
||||||
"command.coordinates.description": "Shows the current player coordinates.",
|
"command.coordinates.description": "Shows the current player coordinates.",
|
||||||
"command.enemy.description": "Manages the client enemy list.",
|
"command.enemy.description": "Manages the client enemy list.",
|
||||||
"command.gps.description": "Sets or clears the Gps compass target.",
|
"command.gps.description": "Sets or clears the Gps compass target.",
|
||||||
|
"command.tier.description": "Looks up a player's mctiers.com tier.",
|
||||||
"command.friend.description": "Manages the client friend list.",
|
"command.friend.description": "Manages the client friend list.",
|
||||||
"command.help.description": "Shows every available client command with clickable pages. Prefix: % (legacy @ also works).",
|
"command.help.description": "Shows every available client command with clickable pages. Prefix: % (legacy @ also works).",
|
||||||
"command.hide.description": "Hides or shows a module in ModuleList without disabling it.",
|
"command.hide.description": "Hides or shows a module in ModuleList without disabling it.",
|
||||||
|
|
|
||||||
|
|
@ -3026,6 +3026,7 @@
|
||||||
"command.coordinates.description": "Показывает текущие координаты игрока.",
|
"command.coordinates.description": "Показывает текущие координаты игрока.",
|
||||||
"command.enemy.description": "Управляет списком врагов клиента.",
|
"command.enemy.description": "Управляет списком врагов клиента.",
|
||||||
"command.gps.description": "Задаёт или сбрасывает цель GPS-компаса.",
|
"command.gps.description": "Задаёт или сбрасывает цель GPS-компаса.",
|
||||||
|
"command.tier.description": "Показывает тир игрока с mctiers.com.",
|
||||||
"command.friend.description": "Управляет списком друзей клиента.",
|
"command.friend.description": "Управляет списком друзей клиента.",
|
||||||
"command.help.description": "Показывает все доступные команды клиента с кликабельными страницами. Префикс: % (старый @ тоже работает).",
|
"command.help.description": "Показывает все доступные команды клиента с кликабельными страницами. Префикс: % (старый @ тоже работает).",
|
||||||
"command.hide.description": "Скрывает или показывает модуль в ModuleList, не отключая его.",
|
"command.hide.description": "Скрывает или показывает модуль в ModuleList, не отключая его.",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
in vec2 v_TexCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
uniform sampler2D u_Src;
|
||||||
|
uniform sampler2D u_Mask;
|
||||||
|
|
||||||
|
layout (std140) uniform HandChams {
|
||||||
|
vec4 u_View; // x = resX, y = resY, z = time
|
||||||
|
vec4 u_Mix; // x = effectAlpha
|
||||||
|
vec4 u_ColorA;
|
||||||
|
vec4 u_ColorB;
|
||||||
|
vec4 u_ColorC;
|
||||||
|
vec4 u_ColorD;
|
||||||
|
vec4 u_Common;
|
||||||
|
vec4 u_Glow;
|
||||||
|
vec4 u_Flags; // x = hideHand
|
||||||
|
};
|
||||||
|
|
||||||
|
float handMaskAt(vec2 uv) {
|
||||||
|
vec4 m = texture(u_Mask, uv);
|
||||||
|
return max(m.a, max(m.r, max(m.g, m.b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 base = texture(u_Src, v_TexCoord);
|
||||||
|
float mask = handMaskAt(v_TexCoord);
|
||||||
|
if (mask <= 0.02 || u_Flags.x > 0.5) {
|
||||||
|
fragColor = base;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float maskGain = smoothstep(0.02, 0.3, mask);
|
||||||
|
vec3 inverted = vec3(1.0) - base.rgb;
|
||||||
|
vec3 finalColor = mix(base.rgb, inverted, u_Mix.x * maskGain);
|
||||||
|
fragColor = vec4(finalColor, 1.0);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.util.mctiers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
final class MCTiersTierTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void labelsMatchKnownTiers() {
|
||||||
|
assertEquals("HT1", new MCTiersTier(1, false).label());
|
||||||
|
assertEquals("LT2", new MCTiersTier(2, false).label());
|
||||||
|
assertEquals("LT3", new MCTiersTier(3, false).label());
|
||||||
|
assertEquals("LT4", new MCTiersTier(4, false).label());
|
||||||
|
assertEquals("T5", new MCTiersTier(5, false).label());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void colorsAreDistinctPerTier() {
|
||||||
|
int t1 = new MCTiersTier(1, false).colorArgb();
|
||||||
|
int t2 = new MCTiersTier(2, false).colorArgb();
|
||||||
|
int t3 = new MCTiersTier(3, false).colorArgb();
|
||||||
|
int t4 = new MCTiersTier(4, false).colorArgb();
|
||||||
|
assertEquals(4, java.util.Set.of(t1, t2, t3, t4).size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void modeApiKeyMapsFixTypeToApiMode() {
|
||||||
|
assertEquals("vanilla", MCTiersTier.modeApiKey("Vanilla"));
|
||||||
|
assertEquals("uhc", MCTiersTier.modeApiKey("UHC"));
|
||||||
|
assertEquals("pot", MCTiersTier.modeApiKey("Pot"));
|
||||||
|
assertEquals("neth_pot", MCTiersTier.modeApiKey("NethOP"));
|
||||||
|
assertEquals("smp", MCTiersTier.modeApiKey("SMP"));
|
||||||
|
assertEquals("sword", MCTiersTier.modeApiKey("Sword"));
|
||||||
|
assertEquals("axe", MCTiersTier.modeApiKey("Axe"));
|
||||||
|
assertEquals("mace", MCTiersTier.modeApiKey("Mace"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void modeApiKeyDefaultsToVanillaForUnknownOrNull() {
|
||||||
|
assertEquals("vanilla", MCTiersTier.modeApiKey(null));
|
||||||
|
assertEquals("vanilla", MCTiersTier.modeApiKey("Nonsense"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 loki5512344.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.util.physics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
final class TrinketPhysicsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restIsZeroAfterReset() {
|
||||||
|
TrinketPhysics p = new TrinketPhysics();
|
||||||
|
p.tick(0.05, 20.0, 1.0, 0.98, 0.7, 0.9, 1.0);
|
||||||
|
p.reset();
|
||||||
|
assertEquals(0.0, p.x());
|
||||||
|
assertEquals(0.0, p.y());
|
||||||
|
assertEquals(0.0, p.z());
|
||||||
|
assertEquals(0.0, p.speed());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restingBallBouncesImmediatelyUnderGravity() {
|
||||||
|
// y=0 — это уровень земли (у ног владельца), поэтому шарик из состояния покоя
|
||||||
|
// на первом же тике проваливается под гравитацией и тут же отскакивает обратно —
|
||||||
|
// это и есть наблюдаемый эффект гравитации в модели "ноль высоты = земля".
|
||||||
|
TrinketPhysics p = new TrinketPhysics();
|
||||||
|
assertTrue(p.tick(0.05, 20.0, 1.0, 1.0, 0.7, 0.9, 5.0), "гравитация должна вжать шарик в землю на первом тике");
|
||||||
|
assertEquals(0.0, p.y(), 1.0e-9);
|
||||||
|
assertTrue(p.speed() > 0.0, "после отскока есть исходящая от земли скорость");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bouncesWhenHittingGroundFromAbove() {
|
||||||
|
TrinketPhysics p = new TrinketPhysics();
|
||||||
|
boolean bouncedAtLeastOnce = false;
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
if (p.tick(0.05, 20.0, 1.0, 0.999, 0.7, 0.9, 5.0)) {
|
||||||
|
bouncedAtLeastOnce = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(bouncedAtLeastOnce, "падение с гравитацией должно рано или поздно удариться о землю (y=0)");
|
||||||
|
assertEquals(0.0, p.y(), 1.0e-9, "после отскока шарик стоит ровно на земле");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatedBouncesLoseEnergyOverTime() {
|
||||||
|
// Каждый отскок домножает вертикальную скорость на bounce=0.5 — пик высоты после
|
||||||
|
// каждого следующего отскока должен быть меньше предыдущего (затухающий мяч).
|
||||||
|
TrinketPhysics p = new TrinketPhysics();
|
||||||
|
double firstBounceSpeed = -1;
|
||||||
|
double secondBounceSpeed = -1;
|
||||||
|
int bounces = 0;
|
||||||
|
for (int i = 0; i < 1000 && bounces < 2; i++) {
|
||||||
|
if (p.tick(0.02, 20.0, 1.0, 1.0, 0.5, 1.0, 100.0)) {
|
||||||
|
bounces++;
|
||||||
|
if (bounces == 1) firstBounceSpeed = p.speed();
|
||||||
|
else secondBounceSpeed = p.speed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(firstBounceSpeed > 0 && secondBounceSpeed > 0, "должно было произойти минимум два отскока");
|
||||||
|
assertTrue(secondBounceSpeed < firstBounceSpeed,
|
||||||
|
"второй отскок должен быть слабее первого: " + secondBounceSpeed + " >= " + firstBounceSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tetherKeepsBallWithinBoundedDistance() {
|
||||||
|
// Прямой доступ к полям недоступен (инкапсуляция) — проверяем инвариант на реальной
|
||||||
|
// траектории: с гравитацией/отскоками и малым tetherRadius шарик не должен улетать
|
||||||
|
// в бесконечность, пружина стабилизирует расстояние от центра.
|
||||||
|
TrinketPhysics p = new TrinketPhysics();
|
||||||
|
double maxDist = 0;
|
||||||
|
for (int i = 0; i < 2000; i++) {
|
||||||
|
p.tick(0.05, 20.0, 1.0, 0.98, 0.6, 0.9, 1.5);
|
||||||
|
double dist = Math.sqrt(p.x() * p.x() + p.z() * p.z());
|
||||||
|
maxDist = Math.max(maxDist, dist);
|
||||||
|
}
|
||||||
|
assertTrue(maxDist < 50.0, "пружина должна удерживать шарик в разумных пределах, было " + maxDist);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue