feat(mod): ProjectileTrails, KillEffect death modes (Shatter/Ascend/Explosion/Disintegrate), TNT fuse bar; compact TODO

Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
This commit is contained in:
loki5512344 2026-10-02 10:01:37 +02:00
parent 483d6bfb93
commit 8d9a2ad0d6
Signed by: boba
GPG key ID: 253067914055423B
17 changed files with 1097 additions and 800 deletions

View file

@ -62,6 +62,8 @@ public class Predictions extends Module {
private final BooleanValue glowWallsValue = bool("pred_glow_walls", SETTING_GLOW_WALLS, false);
private final BooleanValue tntTimerValue = bool("pred_tnt_timer", SETTING_TNT_TIMER, true);
private final RGBColorValue tntColorValue = visibleWhen(colorNoAlpha("pred_tnt_color", SETTING_TNT_COLOR, "#FF5555"), tntTimerValue::get);
private final BooleanValue tntIconValue = visibleWhen(bool("pred_tnt_icon", "tnt_icon", true), tntTimerValue::get);
private final BooleanValue tntBarValue = visibleWhen(bool("pred_tnt_bar", "tnt_fuse_bar", true), tntTimerValue::get);
private final BooleanMapValue kindToggles = group("pred_kind_toggles", SETTING_PROJECTILES, new java.util.LinkedHashMap<>() {{
put("arrow", true); put("trident", true); put("fireball", true); put("wind_charge", true);
put("firework", true); put("snowball", true); put("egg", true);
@ -139,7 +141,9 @@ public class Predictions extends Module {
public BooleanValue getTargetHudOnAimValue() { return targetHudOnAimValue; }
public RGBColorValue getTntColorValue() { return tntColorValue; }
public List<TimerPlate> getTimerPlates() { return timerPlates; }
public boolean isTntBarEnabled() { return tntBarValue.get(); }
public ItemStack getTntTimerIcon() {
if (!tntIconValue.get()) return ItemStack.EMPTY;
if (tntTimerIcon.isEmpty()) tntTimerIcon = new ItemStack(Items.TNT);
return tntTimerIcon;
}

View file

@ -27,6 +27,7 @@ public final class PredictionsHudRenderer {
private static final float PLATE_TEXT_SCALE = 0.68f;
private static final float PLATE_TEXT_PADDING = 24.0f;
private static final float PLATE_HEIGHT = 16.0f;
private static final float TNT_FUSE_TICKS = 80.0f;
private static final float PLATE_BG_OFFSET_X = 2.0f;
private static final float PLATE_BG_OFFSET_Y = 0.0f;
private static final float PLATE_BG_SHRINK = 3.0f;
@ -62,9 +63,19 @@ public final class PredictionsHudRenderer {
double seconds = Math.max(0.0, (fuse - tickDelta) / 20.0);
drawTimerPlate(facade, renderer, textRenderer, formatTime(seconds),
(float) screen.x, (float) screen.y, plateColor, facade.getTntTimerIcon(), null);
if (facade.isTntBarEnabled()) drawFuseBar(renderer, (float) screen.x, (float) screen.y, (fuse - tickDelta) / TNT_FUSE_TICKS, plateColor);
}
}
private static void drawFuseBar(Renderer2D renderer, float cx, float plateY, float progress, int argb) {
float w = 30.0f;
float x = cx - w * 0.5f;
float y = plateY + PLATE_HEIGHT + 2.0f;
renderer.roundedRect(x, y, w, 2.5f, 1.2f, 0xA0000000);
float fill = w * Math.max(0.0f, Math.min(1.0f, progress));
if (fill > 1.0f) renderer.roundedRect(x, y, fill, 2.5f, 1.2f, argb | 0xFF000000);
}
public static void drawTimerPlate(Predictions facade, Renderer2D renderer, TextRenderer textRenderer,
String txt, float sx, float sy, int argb,
ItemStack iconStack, Identifier iconId) {

View file

@ -0,0 +1,39 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits;
import dev.loki.lovisual.config.values.mode.ModeValue;
import dev.loki.lovisual.config.values.primitive.BooleanValue;
import dev.loki.lovisual.config.values.primitive.NumberValue;
import dev.loki.lovisual.features.module.core.Module;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathEffectHelper;
/** Настройки режимов смерти KillEffect (Shatter, Ascend, Explosion, Disintegrate); вынесены из модуля ради лимита строк. */
abstract class DeathEffectSettings extends Module {
protected abstract String currentMode();
protected final boolean isMode(String value) {
return value.equals(currentMode());
}
protected final boolean isDeathMode() {
return DeathEffectHelper.handles(currentMode());
}
protected final NumberValue<Float> deathPower = visibleWhen(
num("killEffectDeathPower", "death_power", 1.0f, 0.3f, 3.0f), this::isDeathMode);
protected final NumberValue<Float> deathDuration = visibleWhen(
num("killEffectDeathDuration", "death_duration", 2.5f, 1.0f, 6.0f), this::isDeathMode);
protected final NumberValue<Float> deathSpin = visibleWhen(
num("killEffectDeathSpin", "death_spin", 1.0f, 0.2f, 3.0f), () -> isMode("Shatter") || isMode("Ascend"));
protected final BooleanValue deathBounce = visibleWhen(
bool("killEffectDeathBounce", "death_bounce", true), () -> isMode("Shatter"));
protected final NumberValue<Integer> deathSparks = visibleWhen(
num("killEffectDeathSparks", "death_sparks", 70, 10, 150), () -> isMode("Explosion"));
protected final ModeValue blastColorMode = visibleWhen(
modeSetting("killEffectBlastColor", "blast_color", "Natural", "Natural", "Client", "Custom"),
() -> isMode("Explosion"));
protected final boolean customBlastColor() {
return isMode("Explosion") && "Custom".equals(blastColorMode.get());
}
}

View file

@ -5,16 +5,16 @@ import dev.loki.lovisual.config.values.color.RGBAColorValue;
import dev.loki.lovisual.config.values.mode.ModeValue;
import dev.loki.lovisual.config.values.primitive.BooleanValue;
import dev.loki.lovisual.config.values.primitive.NumberValue;
import dev.loki.lovisual.features.module.core.Module;
import dev.loki.lovisual.features.module.core.ModuleCategory;
import dev.loki.lovisual.features.module.core.ModuleInfo;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.blood.BloodKillHelper;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathEffectHelper;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.effects.FallingLavaHelper;
import dev.loki.lovisual.features.theme.Theme;
import dev.loki.lovisual.features.module.phase.WorldPhase;
import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.util.wav.engine.CustomSoundEngine;
import net.minecraft.client.Minecraft;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
@ -25,12 +25,13 @@ import java.util.Map;
//todo Description
@ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS)
public class KillEffect extends Module {
public class KillEffect extends DeathEffectSettings {
private static final Identifier ORTHODOX_SOUND =
Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav");
private final ModeValue mode = modeSetting("killEffectMode", "mode", "Orthodox",
"Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado", "Blood");
"Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado", "Blood",
"Shatter", "Ascend", "Explosion", "Disintegrate");
private final NumberValue<Integer> ySpeed = visibleWhen(
num("killEffectYSpeed", "y_speed", 0, -10, 10), () -> isMode("Orthodox"));
private final BooleanValue playSound = visibleWhen(
@ -66,13 +67,15 @@ public class KillEffect extends Module {
private final KillLightningHelper lightningHelper = new KillLightningHelper();
private final KillBeamHelper beamHelper = new KillBeamHelper();
private final BloodKillHelper bloodHelper = new BloodKillHelper();
private final DeathEffectHelper deathHelper = new DeathEffectHelper();
private boolean isMode(String value) {
return value.equals(mode.get());
@Override
protected String currentMode() {
return mode.get();
}
private boolean usesColorSetting() {
return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings();
return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings() || customBlastColor();
}
private boolean usesBeamSettings() {
@ -126,7 +129,7 @@ public class KillEffect extends Module {
CustomSoundEngine.get().play(ORTHODOX_SOUND, soundVolume.get(), 1.0, false, true);
}
} else if ("FallingLava".equals(current)) {
spawnFallingLava(entity);
FallingLavaHelper.spawn(entity);
} else if ("EmbersArc".equals(current)) {
emberHelper.spawn(pos);
} else if ("Lightning".equals(current)) {
@ -135,6 +138,9 @@ public class KillEffect extends Module {
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.PORTAL);
} else if ("Tornado".equals(current)) {
beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.TORNADO);
} else if (DeathEffectHelper.handles(current)) {
deathHelper.spawn(entity, DeathEffectHelper.styleOf(current), deathPower.get(), deathDuration.get(),
deathSpin.get(), deathBounce.get(), deathSparks.get(), blastColor());
} else if ("Blood".equals(current)) {
bloodHelper.spawn(new Vec3(pos.x, pos.y + entity.getBbHeight() * 0.6, pos.z),
Math.clamp(entity.getBbWidth(), 0.3f, 1.6f), Math.clamp(entity.getBbHeight(), 0.3f, 2.6f),
@ -151,6 +157,7 @@ public class KillEffect extends Module {
lightningHelper.clear();
beamHelper.clear();
bloodHelper.clear();
deathHelper.clear();
}
@Override
@ -167,28 +174,16 @@ public class KillEffect extends Module {
} else if ("EmbersArc".equals(current)) {
emberHelper.render(renderer, tickDelta);
}
deathHelper.render(renderer, tickDelta);
beamHelper.render(renderer, mc, beamIntensity.get(), beamSize.get());
bloodHelper.render(renderer, mc.gameRenderer.mainCamera().position(), tickDelta, bloodTime.get());
}
private void spawnFallingLava(LivingEntity entity) {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) {
return;
}
int height = (int) (entity.getBbHeight() * 10);
int width = (int) (entity.getBbWidth() * 10);
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
for (int k = 0; k < width; k++) {
((ClientLevelAccessor) mc.level).invokeAddParticle(
ParticleTypes.FALLING_LAVA, false, true,
entity.getX() + j * 0.1,
entity.getY() + i * 0.1,
entity.getZ() + k * 0.1,
0, 0, 0);
}
}
}
private int blastColor() {
return switch (blastColorMode.get()) {
case "Client" -> Theme.theme().accent() | 0xFF000000;
case "Custom" -> color.getArgb();
default -> 0xFFFFA030;
};
}
}

View file

@ -0,0 +1,54 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death;
import net.minecraft.world.entity.LivingEntity;
import java.util.ArrayList;
import java.util.List;
/** Разбиение тела на 6 частей (голова, торс, две руки, две ноги) с UV-прямоугольниками стандартного скина 64x64. */
public final class DeathBody {
/** Размеры в пикселях (w,h,d), начало UV (u,v) и смещение центра в блоках: kind -> {w,h,d,u,v,cx,cy}. */
private static final float[][] LAYOUT = {
{8, 8, 8, 0, 0, 0.0f, 1.75f},
{8, 12, 4, 16, 16, 0.0f, 1.125f},
{4, 12, 4, 40, 16, -0.375f, 1.125f},
{4, 12, 4, 32, 48, 0.375f, 1.125f},
{4, 12, 4, 0, 16, -0.125f, 0.375f},
{4, 12, 4, 16, 48, 0.125f, 0.375f},
};
private DeathBody() {
}
public static float[] layout(int kind) {
return LAYOUT[kind];
}
/**
* Части в мировых координатах: поза «стоя», повёрнутая по yaw тела, масштаб по росту сущности.
* Центр тела считается на высоте груди.
*/
public static List<DeathPhysics.Part> build(double px, double py, double pz, float bodyYawDeg, float height) {
float scale = Math.max(0.3f, height / 1.8f);
double yaw = Math.toRadians(-bodyYawDeg);
float cos = (float) Math.cos(yaw);
float sin = (float) Math.sin(yaw);
List<DeathPhysics.Part> parts = new ArrayList<>(LAYOUT.length);
for (int i = 0; i < LAYOUT.length; i++) {
float[] l = LAYOUT[i];
float lx = l[5] * scale;
float wx = lx * cos;
float wz = -lx * sin;
DeathPhysics.Part p = new DeathPhysics.Part(i, (float) px + wx, (float) py + l[6] * scale, (float) pz + wz,
l[0] / 32.0f * scale, l[1] / 32.0f * scale, l[2] / 32.0f * scale);
p.ry = (float) yaw;
parts.add(p);
}
return parts;
}
public static float height(LivingEntity e) {
return e.getBbHeight();
}
}

View file

@ -0,0 +1,178 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Part;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Style;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.texture.TextureStorage;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import dev.loki.lovisual.util.player.health.PlayerSkinResolver;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.LivingEntity;
import org.joml.Vector3f;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.addBillboardVertices;
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.applyOpacity;
/** Режимы смерти из evo: тело рассыпается на части (Shatter/Explosion), взлетает (Ascend) или тает (Disintegrate). */
public final class DeathEffectHelper {
private static final int FALLBACK_TINT = 0xFF9A9A9A;
private static final float SPARK_LIFE = 0.9f;
private static final float FLASH_LIFE = 0.45f;
private static final class Spark {
float x, y, z, vx, vy, vz;
}
private static final class Corpse {
final List<Part> parts;
final List<Spark> sparks = new ArrayList<>();
final Style style;
final long start;
final float duration;
final float groundY;
final boolean bounce;
final Identifier skin;
final int sparkColor;
final float[] center;
long last;
Corpse(List<Part> parts, Style style, long start, float duration, float groundY, boolean bounce,
Identifier skin, int sparkColor, float[] center) {
this.parts = parts;
this.style = style;
this.start = start;
this.last = start;
this.duration = duration;
this.groundY = groundY;
this.bounce = bounce;
this.skin = skin;
this.sparkColor = sparkColor;
this.center = center;
}
}
private final List<Corpse> corpses = new ArrayList<>();
private final Random random = new Random();
public static Style styleOf(String mode) {
return switch (mode) {
case "Ascend" -> Style.ASCEND;
case "Explosion" -> Style.EXPLOSION;
case "Disintegrate" -> Style.DISINTEGRATE;
default -> Style.SHATTER;
};
}
public static boolean handles(String mode) {
return "Shatter".equals(mode) || "Ascend".equals(mode) || "Explosion".equals(mode) || "Disintegrate".equals(mode);
}
public void clear() {
corpses.clear();
}
public void spawn(LivingEntity e, Style style, float power, float duration, float spin, boolean bounce,
int sparks, int sparkColor) {
float height = DeathBody.height(e);
List<Part> parts = DeathBody.build(e.getX(), e.getY(), e.getZ(), e.yBodyRot, height);
float cx = (float) e.getX(), cy = (float) e.getY() + height * 0.6f, cz = (float) e.getZ();
for (Part p : parts) DeathPhysics.launch(p, style, power, spin, duration, cx, cy, cz, random);
Identifier skin = e instanceof AbstractClientPlayer player ? PlayerSkinResolver.resolvePlayerSkin(player) : null;
Corpse c = new Corpse(parts, style, GameClock.millis(), duration, (float) e.getY(), bounce, skin, sparkColor,
new float[]{cx, cy, cz});
if (style == Style.EXPLOSION) {
for (int i = 0; i < sparks; i++) c.sparks.add(spark(cx, cy, cz, power));
}
corpses.add(c);
}
private Spark spark(float cx, float cy, float cz, float power) {
Spark s = new Spark();
s.x = cx;
s.y = cy;
s.z = cz;
float theta = random.nextFloat() * 6.2831855f;
float up = random.nextFloat() * 2.0f - 0.6f;
float speed = (2.0f + random.nextFloat() * 5.0f) * power;
float horiz = (float) Math.sqrt(Math.max(0.0, 1.0 - up * up * 0.25));
s.vx = (float) Math.cos(theta) * horiz * speed;
s.vz = (float) Math.sin(theta) * horiz * speed;
s.vy = up * speed;
return s;
}
public void render(Renderer3D renderer, float tickDelta) {
if (corpses.isEmpty() || renderer == null) return;
long now = GameClock.millis();
for (Iterator<Corpse> it = corpses.iterator(); it.hasNext(); ) {
Corpse c = it.next();
float t = (now - c.start) / 1000.0f;
if (t > c.duration + 0.2f) {
it.remove();
continue;
}
advance(c, now, t);
drawParts(renderer, c, t);
if (c.style == Style.EXPLOSION) drawSparks(renderer, c, t);
}
}
private void advance(Corpse c, long now, float t) {
float dt = Math.min(0.1f, (now - c.last) / 1000.0f);
c.last = now;
int steps = Math.max(1, (int) Math.ceil(dt / 0.02f));
float h = dt / steps;
for (int i = 0; i < steps; i++) {
for (Part p : c.parts) DeathPhysics.step(p, c.style, h, t, c.groundY, c.bounce);
}
for (Spark s : c.sparks) {
s.vy -= 9.0f * dt;
s.x += s.vx * dt;
s.y += s.vy * dt;
s.z += s.vz * dt;
}
}
private void drawParts(Renderer3D renderer, Corpse c, float t) {
boolean textured = c.skin != null;
MeshBuilder mesh = textured
? renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_DEPTH, c.skin, Renderer3D.DepthMode.MAIN)
: renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_DEPTH, Renderer3D.DepthMode.MAIN);
if (mesh == null) return;
for (Part p : c.parts) {
float alpha = DeathPhysics.alpha(p, c.style, t, c.duration);
if (alpha > 0.01f) DeathMesh.draw(mesh, p, DeathBody.layout(p.kind), textured ? 0xFFFFFF : FALLBACK_TINT & 0xFFFFFF, alpha, textured);
}
}
private void drawSparks(Renderer3D renderer, Corpse c, float t) {
MeshBuilder glow = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, TextureStorage.BLOOM,
Renderer3D.DepthMode.NONE);
if (glow == null) return;
Vector3f right = new Vector3f(1, 0, 0).rotate(RenderState.cameraRotation);
Vector3f up = new Vector3f(0, 1, 0).rotate(RenderState.cameraRotation);
if (t < FLASH_LIFE) {
float k = t / FLASH_LIFE;
float size = 0.6f + k * 3.2f;
int col = applyOpacity(c.sparkColor, (1.0f - k) * (1.0f - k));
addBillboardVertices(glow, c.center[0], c.center[1], c.center[2], new Vector3f(right).mul(size),
new Vector3f(up).mul(size), col, col, col, col);
}
float fade = Math.max(0.0f, 1.0f - t / SPARK_LIFE);
if (fade <= 0.0f) return;
int col = applyOpacity(c.sparkColor, fade);
Vector3f r = new Vector3f(right).mul(0.07f);
Vector3f u = new Vector3f(up).mul(0.07f);
for (Spark s : c.sparks) addBillboardVertices(glow, s.x, s.y, s.z, r, u, col, col, col, col);
}
}

View file

@ -0,0 +1,94 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
/**
* Рисует часть тела как куб с UV скина. Мировые пайплайны рисуют без отсечения задних граней,
* поэтому порядок обхода вершин не важен.
*/
final class DeathMesh {
private static final float TEX = 64.0f;
/** Яркость граней: +x, -x, +y, -y, +z, -z. */
private static final float[] SHADE = {0.8f, 0.8f, 1.0f, 0.55f, 0.9f, 0.7f};
private DeathMesh() {
}
static void draw(MeshBuilder mesh, DeathPhysics.Part p, float[] layout, int rgb, float alpha, boolean textured) {
float w = p.hw, h = p.hh, d = p.hd;
float u = layout[3], v = layout[4];
float pw = layout[0], ph = layout[1], pd = layout[2];
float[][] c = corners(p, w, h, d);
// порядок углов: 0:-x-y-z 1:+x-y-z 2:+x+y-z 3:-x+y-z 4:-x-y+z 5:+x-y+z 6:+x+y+z 7:-x+y+z
face(mesh, c, 4, 5, 6, 7, u + pd, v + pd, pw, ph, rgb, alpha, SHADE[4], textured); // +z (перед)
face(mesh, c, 1, 0, 3, 2, u + pd + pw + pd, v + pd, pw, ph, rgb, alpha, SHADE[5], textured); // -z (спина)
face(mesh, c, 0, 4, 7, 3, u, v + pd, pd, ph, rgb, alpha, SHADE[1], textured); // -x (правая сторона)
face(mesh, c, 5, 1, 2, 6, u + pd + pw, v + pd, pd, ph, rgb, alpha, SHADE[0], textured); // +x (левая сторона)
face(mesh, c, 7, 6, 2, 3, u + pd, v, pw, pd, rgb, alpha, SHADE[2], textured); // +y (верх)
face(mesh, c, 0, 1, 5, 4, u + pd + pw, v, pw, pd, rgb, alpha, SHADE[3], textured); // -y (низ)
}
/** Углы куба после поворота Эйлера (Y, затем X, затем Z) и переноса в центр части. */
private static float[][] corners(DeathPhysics.Part p, float w, float h, float d) {
float[][] out = new float[8][3];
for (int i = 0; i < 8; i++) {
float x = (i & 1) == 0 ? -w : w;
float y = (i & 2) == 0 ? -h : h;
float z = (i & 4) == 0 ? -d : d;
// индексация: бит0 = x, бит1 = y, бит2 = z; пересобираем в порядок из draw()
out[i] = rotate(p, x, y, z);
}
return remap(out);
}
private static float[][] remap(float[][] bits) {
// bits[i]: i = x | y<<1 | z<<2. Нужен порядок 0:(-,-,-) 1:(+,-,-) 2:(+,+,-) 3:(-,+,-) 4:(-,-,+) 5:(+,-,+) 6:(+,+,+) 7:(-,+,+)
return new float[][]{bits[0], bits[1], bits[3], bits[2], bits[4], bits[5], bits[7], bits[6]};
}
private static float[] rotate(DeathPhysics.Part p, float x, float y, float z) {
float cy = (float) Math.cos(p.ry), sy = (float) Math.sin(p.ry);
float x1 = x * cy + z * sy, z1 = -x * sy + z * cy;
float cx = (float) Math.cos(p.rx), sx = (float) Math.sin(p.rx);
float y2 = y * cx - z1 * sx, z2 = y * sx + z1 * cx;
float cz = (float) Math.cos(p.rz), sz = (float) Math.sin(p.rz);
float x3 = x1 * cz - y2 * sz, y3 = x1 * sz + y2 * cz;
return new float[]{p.x + x3, p.y + y3, p.z + z2};
}
private static void face(MeshBuilder m, float[][] c, int a, int b, int cc, int dd, float u, float v,
float uw, float vh, int rgb, float alpha, float shade, boolean textured) {
int argb = argb(rgb, alpha, shade);
float u0 = u / TEX, u1 = (u + uw) / TEX, v0 = v / TEX, v1 = (v + vh) / TEX;
// a: низ-слева, b: низ-справа, cc: верх-справа, dd: верх-слева (для боковых граней)
quad(m, c[a], c[b], c[cc], c[dd], u0, v1, u1, v1, u1, v0, u0, v0, argb, textured);
}
private static void quad(MeshBuilder m, float[] p1, float[] p2, float[] p3, float[] p4,
float ua, float va, float ub, float vb, float uc, float vc, float ud, float vd,
int argb, boolean textured) {
m.ensureQuadCapacity();
RenderColor col = new RenderColor(argb);
int i1 = vertex(m, p1, ua, va, col, textured);
int i2 = vertex(m, p2, ub, vb, col, textured);
int i3 = vertex(m, p3, uc, vc, col, textured);
int i4 = vertex(m, p4, ud, vd, col, textured);
m.quad(i1, i2, i3, i4);
}
private static int vertex(MeshBuilder m, float[] p, float u, float v, RenderColor col, boolean textured) {
var b = m.vec3(p[0], p[1], p[2]);
if (textured) b = b.vec2(u, v);
return b.color(col).next();
}
private static int argb(int rgb, float alpha, float shade) {
int a = Math.max(0, Math.min(255, Math.round(alpha * 255.0f)));
int r = Math.round(((rgb >> 16) & 0xFF) * shade);
int g = Math.round(((rgb >> 8) & 0xFF) * shade);
int bl = Math.round((rgb & 0xFF) * shade);
return (a << 24) | (r << 16) | (g << 8) | bl;
}
}

View file

@ -0,0 +1,137 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death;
import java.util.Random;
/** Чистая физика частей тела для режимов смерти (Shatter, Ascend, Explosion, Disintegrate); без Minecraft. */
public final class DeathPhysics {
public enum Style { SHATTER, ASCEND, EXPLOSION, DISINTEGRATE }
/** Часть тела: центр, скорость, углы Эйлера (рад), угловая скорость, половинные размеры, задержка. */
public static final class Part {
public float x, y, z, vx, vy, vz;
public float rx, ry, rz, wx, wy, wz;
public final float hw, hh, hd;
public final int kind;
public float delay;
public float phase;
boolean resting;
public Part(int kind, float x, float y, float z, float hw, float hh, float hd) {
this.kind = kind;
this.x = x;
this.y = y;
this.z = z;
this.hw = hw;
this.hh = hh;
this.hd = hd;
}
float radius() {
return Math.max(hw, Math.max(hh, hd));
}
}
private static final float GRAVITY = 16.0f;
private DeathPhysics() {
}
/** Начальный импульс части относительно центра тела (cx, cy, cz). */
public static void launch(Part p, Style style, float power, float spin, float duration,
float cx, float cy, float cz, Random r) {
float dx = p.x - cx + (r.nextFloat() - 0.5f) * 0.2f;
float dy = p.y - cy;
float dz = p.z - cz + (r.nextFloat() - 0.5f) * 0.2f;
float len = Math.max(0.05f, (float) Math.sqrt(dx * dx + dy * dy + dz * dz));
p.phase = r.nextFloat() * 6.2831855f;
switch (style) {
case SHATTER -> {
float k = (1.5f + r.nextFloat() * 1.5f) * power;
set(p, dx / len * k, 2.5f * power + dy / len * k, dz / len * k, spin * 7.0f, r);
}
case EXPLOSION -> {
float k = (5.0f + r.nextFloat() * 3.0f) * power;
set(p, dx / len * k, 3.0f * power + dy / len * k * 0.6f, dz / len * k, spin * 12.0f, r);
}
case ASCEND -> set(p, 0.0f, (0.5f + r.nextFloat() * 0.4f) * power, 0.0f, spin * 1.2f, r);
default -> {
set(p, 0.0f, 0.0f, 0.0f, spin * 0.5f, r);
p.delay = Math.max(0.0f, (2.0f - p.y) / 2.0f) * duration * 0.45f;
}
}
}
private static void set(Part p, float vx, float vy, float vz, float spin, Random r) {
p.vx = vx;
p.vy = vy;
p.vz = vz;
p.wx = (r.nextFloat() - 0.5f) * 2.0f * spin;
p.wy = (r.nextFloat() - 0.5f) * 2.0f * spin;
p.wz = (r.nextFloat() - 0.5f) * 2.0f * spin;
}
/** Шаг физики; t: секунды от начала эффекта, groundY: уровень пола, bounce: отскок от земли. */
public static void step(Part p, Style style, float dt, float t, float groundY, boolean bounce) {
if (p.resting) return;
switch (style) {
case ASCEND -> {
p.x += (float) Math.sin(t * 2.0f + p.phase) * 0.25f * dt;
p.y += p.vy * dt;
spinStep(p, dt);
}
case DISINTEGRATE -> {
if (t > p.delay) p.y += 0.6f * dt;
spinStep(p, dt);
}
default -> ballistic(p, dt, groundY, bounce);
}
}
private static void ballistic(Part p, float dt, float groundY, boolean bounce) {
p.vy -= GRAVITY * dt;
float drag = (float) Math.pow(0.985, dt * 60.0);
p.vx *= drag;
p.vz *= drag;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
spinStep(p, dt);
float floor = groundY + p.radius();
if (p.y >= floor) return;
p.y = floor;
if (bounce && Math.abs(p.vy) > 1.0f) {
p.vy = -p.vy * 0.45f;
p.vx *= 0.8f;
p.vz *= 0.8f;
p.wx *= 0.7f;
p.wy *= 0.7f;
p.wz *= 0.7f;
} else {
p.vx = p.vy = p.vz = 0.0f;
p.wx = p.wy = p.wz = 0.0f;
p.resting = true;
}
}
private static void spinStep(Part p, float dt) {
p.rx += p.wx * dt;
p.ry += p.wy * dt;
p.rz += p.wz * dt;
}
/** Непрозрачность части в момент t (0..1). */
public static float alpha(Part p, Style style, float t, float duration) {
float a = switch (style) {
case SHATTER -> 1.0f - ramp(t, duration * 0.6f, duration * 0.4f);
case EXPLOSION -> 1.0f - ramp(t, duration * 0.4f, duration * 0.6f);
case ASCEND -> 1.0f - ramp(t, duration * 0.3f, duration * 0.7f);
default -> 1.0f - ramp(t, p.delay, duration * 0.35f);
};
return Math.max(0.0f, Math.min(1.0f, a));
}
private static float ramp(float t, float start, float length) {
return Math.max(0.0f, Math.min(1.0f, (t - start) / Math.max(0.001f, length)));
}
}

View file

@ -0,0 +1,28 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.effects;
import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor;
import net.minecraft.client.Minecraft;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.world.entity.LivingEntity;
/** Лавовые капли по объёму тела (режим FallingLava у KillEffect). */
public final class FallingLavaHelper {
private FallingLavaHelper() {
}
public static void spawn(LivingEntity entity) {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) return;
int height = (int) (entity.getBbHeight() * 10);
int width = (int) (entity.getBbWidth() * 10);
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
for (int k = 0; k < width; k++) {
((ClientLevelAccessor) mc.level).invokeAddParticle(ParticleTypes.FALLING_LAVA, false, true,
entity.getX() + j * 0.1, entity.getY() + i * 0.1, entity.getZ() + k * 0.1, 0, 0, 0);
}
}
}
}
}

View file

@ -0,0 +1,95 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails;
import dev.loki.lovisual.config.values.color.RGBAColorValue;
import dev.loki.lovisual.config.values.mode.ModeValue;
import dev.loki.lovisual.config.values.primitive.BooleanMapValue;
import dev.loki.lovisual.config.values.primitive.BooleanValue;
import dev.loki.lovisual.config.values.primitive.NumberValue;
import dev.loki.lovisual.features.module.core.Module;
import dev.loki.lovisual.features.module.core.ModuleCategory;
import dev.loki.lovisual.features.module.core.ModuleInfo;
import dev.loki.lovisual.features.module.phase.WorldPhase;
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** Затухающие ленты за стрелами, жемчугом, трезубцами и другими снарядами. */
@ModuleInfo(
id = "projectiletrails",
displayName = "ProjectileTrails",
category = ModuleCategory.VISUALS,
description = "Draws fading trails behind arrows, pearls and other projectiles"
)
public class ProjectileTrails extends Module {
private final TrailStore store = new TrailStore();
private final NumberValue<Integer> duration = num("projectileTrailsDuration", "duration", 500, 100, 2000);
private final NumberValue<Float> width = num("projectileTrailsWidth", "width", 0.15f, 0.05f, 1.5f);
private final BooleanValue fill = bool("projectileTrailsFill", "fill", true);
private final ModeValue fillType = visibleWhen(modeSetting("projectileTrailsFillType", "fill_type", "Solid", "Solid", "Glow"), fill::get);
private final BooleanValue lines = bool("projectileTrailsLines", "lines", true);
private final NumberValue<Float> lineWidth = visibleWhen(num("projectileTrailsLineWidth", "line_width", 2.0f, 1.0f, 5.0f), lines::get);
private final BooleanValue depthTest = bool("projectileTrailsDepth", "depth_test", true);
private final ModeValue colorMode = modeSetting("projectileTrailsColorMode", "color_mode", "Theme",
"Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme");
private final RGBAColorValue color = color("projectileTrailsColor", "color", "#CCFF55FF");
private final RGBAColorValue color2 = color("projectileTrailsColor2", "color2", "#CC55FFFF");
private final NumberValue<Integer> colorSpeed = num("projectileTrailsColorSpeed", "color_speed", 18, 2, 54);
private final BooleanMapValue kinds = group("projectileTrailsKinds", "projectiles", Map.of(
"arrow", true, "trident", true, "pearl", true, "snowball", true, "egg", true,
"potion", true, "fireball", true, "wind_charge", true, "firework", false));
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
}
@Override
public void onDisable() {
store.clear();
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
Minecraft mc = Minecraft.getInstance();
if (!isEnabled() || mc.level == null || mc.gameRenderer.mainCamera() == null) return;
long now = GameClock.millis();
long life = duration.get();
for (Entity e : mc.level.entitiesForRendering()) {
String kind = TrailStore.kindOf(e);
if (kind != null && kinds.get(kind) && e.isAlive()) store.sample(e, tickDelta, now, life);
}
store.prune(now, life, id -> mc.level.getEntity(id) != null);
if (store.tracks().isEmpty()) return;
int phase = Math.round((mc.player == null ? 0 : mc.player.tickCount + tickDelta) * 4.0f);
AnimatedRenderColors.Mode mode = parse(colorMode.get());
int primary = color.getArgb();
int secondary = color2.getArgb();
int speed = colorSpeed.get();
List<TrailStore.Track> tracks = new ArrayList<>(store.tracks().values());
Vec3 cam = mc.gameRenderer.mainCamera().position();
TrailRibbon.render(renderer, tracks, now, life, width.get(), fill.get(), "Glow".equals(fillType.get()),
lines.get(), lineWidth.get(), depthTest.get(), cam,
(i, n) -> AnimatedRenderColors.resolve(mode, speed, phase + Math.round(i / (float) Math.max(1, n - 1) * 180), primary, secondary, true));
}
private static AnimatedRenderColors.Mode parse(String mode) {
return switch (mode) {
case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW;
case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
case "Sky" -> AnimatedRenderColors.Mode.SKY;
case "Fade" -> AnimatedRenderColors.Mode.FADE;
case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS;
case "Theme" -> AnimatedRenderColors.Mode.THEME;
default -> AnimatedRenderColors.Mode.STATIC;
};
}
}

View file

@ -0,0 +1,99 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails;
import dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails.TrailStore.Point;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntBinaryOperator;
/** Строит ленту, повёрнутую к камере, с затуханием и сужением к хвосту, плюс контурные линии по краям. */
final class TrailRibbon {
private TrailRibbon() {
}
/**
* @param color цвет точки: (индекс от хвоста, число точек) -> ARGB
*/
static void render(Renderer3D renderer, List<TrailStore.Track> tracks, long now, long lifeMs, float width,
boolean fill, boolean glow, boolean lines, float lineWidth, boolean depth, Vec3 cam,
IntBinaryOperator color) {
MeshBuilder fillMesh = null;
if (fill) {
var pipeline = glow
? (depth ? LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH : LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE)
: (depth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED);
fillMesh = renderer.batch(pipeline, depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN);
}
MeshBuilder lineMesh = null;
if (lines) {
float prev = RenderState.lineWidth;
try {
RenderState.lineWidth = Math.max(0.5f, lineWidth);
lineMesh = renderer.batch(depth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE
: LoVisualRenderPipelines.WORLD_COLORED_LINES, depth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN);
} finally {
RenderState.lineWidth = prev;
}
}
if (fillMesh == null && lineMesh == null) return;
for (TrailStore.Track track : tracks) {
List<Point> pts = new ArrayList<>(track.points);
if (pts.size() >= 2) draw(pts, now, lifeMs, width, fillMesh, lineMesh, cam, color);
}
}
private static void draw(List<Point> pts, long now, long lifeMs, float width, MeshBuilder fill, MeshBuilder line,
Vec3 cam, IntBinaryOperator color) {
int n = pts.size();
double[][] left = new double[n][3];
double[][] right = new double[n][3];
int[] argb = new int[n];
for (int i = 0; i < n; i++) {
Point p = pts.get(i);
Point a = pts.get(Math.max(0, i - 1));
Point b = pts.get(Math.min(n - 1, i + 1));
double dx = b.x() - a.x(), dy = b.y() - a.y(), dz = b.z() - a.z();
double vx = cam.x - p.x(), vy = cam.y - p.y(), vz = cam.z - p.z();
double sx = dy * vz - dz * vy, sy = dz * vx - dx * vz, sz = dx * vy - dy * vx;
double len = Math.sqrt(sx * sx + sy * sy + sz * sz);
if (len < 1e-6) len = 1.0;
float life = 1.0f - Math.min(1.0f, (now - p.ms()) / (float) lifeMs);
double half = width * 0.5 * (0.25 + 0.75 * life) / len;
left[i] = new double[]{p.x() + sx * half, p.y() + sy * half, p.z() + sz * half};
right[i] = new double[]{p.x() - sx * half, p.y() - sy * half, p.z() - sz * half};
int base = color.applyAsInt(i, n);
int alpha = Math.round(((base >>> 24) & 0xFF) * life * life);
argb[i] = (alpha << 24) | (base & 0x00FFFFFF);
}
for (int i = 1; i < n; i++) {
if (fill != null) quad(fill, left[i - 1], right[i - 1], right[i], left[i], argb[i - 1], argb[i]);
if (line != null) {
segment(line, left[i - 1], left[i], argb[i - 1], argb[i]);
segment(line, right[i - 1], right[i], argb[i - 1], argb[i]);
}
}
}
private static void quad(MeshBuilder m, double[] l0, double[] r0, double[] r1, double[] l1, int c0, int c1) {
m.ensureQuadCapacity();
int a = m.vec3(l0[0], l0[1], l0[2]).color(new RenderColor(c0)).next();
int b = m.vec3(r0[0], r0[1], r0[2]).color(new RenderColor(c0)).next();
int c = m.vec3(r1[0], r1[1], r1[2]).color(new RenderColor(c1)).next();
int d = m.vec3(l1[0], l1[1], l1[2]).color(new RenderColor(c1)).next();
m.quad(a, b, c, d);
}
private static void segment(MeshBuilder m, double[] p0, double[] p1, int c0, int c1) {
m.ensureLineCapacity();
int a = m.vec3(p0[0], p0[1], p0[2]).color(new RenderColor(c0)).next();
int b = m.vec3(p1[0], p1[1], p1[2]).color(new RenderColor(c1)).next();
m.line(a, b);
}
}

View file

@ -0,0 +1,83 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.projectiletrails;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.function.Predicate;
/** История позиций летящих снарядов: точки с метками времени, устаревшие и пропавшие следы вычищаются. */
final class TrailStore {
/** Минимальный сдвиг между соседними точками (блоки), чтобы лента не копила шум. */
private static final double MIN_STEP = 0.04;
record Point(double x, double y, double z, long ms) {
}
static final class Track {
final ArrayDeque<Point> points = new ArrayDeque<>();
long lastSeen;
}
private final Map<Integer, Track> tracks = new HashMap<>();
Map<Integer, Track> tracks() {
return tracks;
}
void clear() {
tracks.clear();
}
/** Ключ вида снаряда для настроек: arrow, trident, pearl, snowball, egg, potion, fireball, wind_charge, firework. */
static String kindOf(Entity entity) {
if (!(entity instanceof Projectile)) return null;
String path = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).getPath();
if (path.contains("arrow")) return "arrow";
if (path.contains("trident")) return "trident";
if (path.contains("pearl")) return "pearl";
if (path.contains("snowball")) return "snowball";
if (path.contains("egg")) return "egg";
if (path.contains("potion")) return "potion";
if (path.contains("wind_charge")) return "wind_charge";
if (path.contains("fireball") || path.contains("fire_charge")) return "fireball";
if (path.contains("firework")) return "firework";
return null;
}
/** Добавляет текущую позицию снаряда (интерполированную) и отбрасывает старые точки. */
void sample(Entity entity, float partialTicks, long now, long lifeMs) {
Track track = tracks.computeIfAbsent(entity.getId(), id -> new Track());
track.lastSeen = now;
Vec3 p = entity.getPosition(partialTicks);
Point last = track.points.peekLast();
if (last == null || last.ms() != now && dist(last, p) >= MIN_STEP) {
track.points.addLast(new Point(p.x, p.y, p.z, now));
}
trim(track, now, lifeMs);
}
/** Чистит следы: пропавшие из мира снаряды дотлевают, потом удаляются. */
void prune(long now, long lifeMs, Predicate<Integer> alive) {
for (Iterator<Map.Entry<Integer, Track>> it = tracks.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Integer, Track> e = it.next();
if (!alive.test(e.getKey())) trim(e.getValue(), now, lifeMs);
if (e.getValue().points.isEmpty() || now - e.getValue().lastSeen > lifeMs * 2) it.remove();
}
}
private static void trim(Track track, long now, long lifeMs) {
while (!track.points.isEmpty() && now - track.points.peekFirst().ms() > lifeMs) track.points.pollFirst();
}
private static double dist(Point a, Vec3 b) {
double dx = a.x() - b.x, dy = a.y() - b.y, dz = a.z() - b.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
}

View file

@ -1042,6 +1042,8 @@
"setting.predictions.aim_trajectory": "Aim Trajectory",
"setting.predictions.aim_indicator": "Aim Indicator",
"setting.predictions.tnt_timer": "TNT Timer",
"setting.predictions.tnt_icon": "TNT icon",
"setting.predictions.tnt_fuse_bar": "TNT fuse bar",
"setting.predictions.tnt_color": "TNT Color",
"setting.predictions.show_plate": "Show Impact Plate",
"setting.reimaginedvisual.night_mode.option.NIGHT": "Dark night",
@ -3632,5 +3634,50 @@
"module.rain.description": "Customizes rain visuals and sounds.",
"module.spatialgui.description": "Renders the interface as a floating panel in 3D space.",
"module.trinket.description": "Attaches decorative trinkets to your character.",
"module.wet_world.description": "Makes surfaces look wet during rain."
"module.wet_world.description": "Makes surfaces look wet during rain.",
"module.projectiletrails.description": "Draws fading trails behind arrows, pearls and other projectiles.",
"setting.projectiletrails.duration": "Duration (ms)",
"setting.projectiletrails.width": "Width",
"setting.projectiletrails.fill": "Fill",
"setting.projectiletrails.fill_type": "Fill type",
"setting.projectiletrails.fill_type.option.Solid": "Solid",
"setting.projectiletrails.fill_type.option.Glow": "Glow",
"setting.projectiletrails.lines": "Outline",
"setting.projectiletrails.line_width": "Outline width",
"setting.projectiletrails.depth_test": "Depth test",
"setting.projectiletrails.color_mode": "Color mode",
"setting.projectiletrails.color": "Color",
"setting.projectiletrails.color2": "Second color",
"setting.projectiletrails.color_speed": "Color speed",
"setting.projectiletrails.projectiles": "Projectiles",
"setting.projectiletrails.color_mode.option.Static": "Static",
"setting.projectiletrails.color_mode.option.Rainbow": "Rainbow",
"setting.projectiletrails.color_mode.option.LightRainbow": "Light Rainbow",
"setting.projectiletrails.color_mode.option.Sky": "Sky",
"setting.projectiletrails.color_mode.option.Fade": "Fade",
"setting.projectiletrails.color_mode.option.DoubleColor": "Double Color",
"setting.projectiletrails.color_mode.option.Analogous": "Analogous",
"setting.projectiletrails.color_mode.option.Theme": "Theme",
"setting.projectiletrails.projectiles.option.arrow": "Arrows",
"setting.projectiletrails.projectiles.option.trident": "Tridents",
"setting.projectiletrails.projectiles.option.pearl": "Ender pearls",
"setting.projectiletrails.projectiles.option.snowball": "Snowballs",
"setting.projectiletrails.projectiles.option.egg": "Eggs",
"setting.projectiletrails.projectiles.option.potion": "Potions",
"setting.projectiletrails.projectiles.option.fireball": "Fireballs",
"setting.projectiletrails.projectiles.option.wind_charge": "Wind charges",
"setting.projectiletrails.projectiles.option.firework": "Fireworks",
"setting.killeffect.mode.option.Shatter": "Shatter",
"setting.killeffect.mode.option.Ascend": "Ascend",
"setting.killeffect.mode.option.Explosion": "Explosion",
"setting.killeffect.mode.option.Disintegrate": "Disintegrate",
"setting.killeffect.death_power": "Power",
"setting.killeffect.death_duration": "Duration (s)",
"setting.killeffect.death_spin": "Spin",
"setting.killeffect.death_bounce": "Bounce off the ground",
"setting.killeffect.death_sparks": "Sparks",
"setting.killeffect.blast_color": "Blast color",
"setting.killeffect.blast_color.option.Natural": "Natural",
"setting.killeffect.blast_color.option.Client": "Client theme",
"setting.killeffect.blast_color.option.Custom": "Custom"
}

View file

@ -976,6 +976,8 @@
"setting.predictions.aim_trajectory": "Траектория для прицеливания",
"setting.predictions.aim_indicator": "Индикатор прицеливания",
"setting.predictions.tnt_timer": "Таймер TNT",
"setting.predictions.tnt_icon": "Иконка TNT",
"setting.predictions.tnt_fuse_bar": "Полоска фитиля TNT",
"setting.predictions.tnt_color": "Цвет TNT",
"setting.predictions.show_plate": "Показывать точку приземления",
"setting.reimaginedvisual.night_mode.option.NIGHT": "Темная ночь",
@ -3423,5 +3425,50 @@
"module.rain.description": "Настраивает вид и звук дождя.",
"module.spatialgui.description": "Рисует интерфейс плавающей панелью в 3D-пространстве.",
"module.trinket.description": "Крепит декоративные безделушки к вашему персонажу.",
"module.wet_world.description": "Делает поверхности мокрыми во время дождя."
"module.wet_world.description": "Делает поверхности мокрыми во время дождя.",
"module.projectiletrails.description": "Рисует затухающие ленты за стрелами, жемчугом и другими снарядами.",
"setting.projectiletrails.duration": "Длительность (мс)",
"setting.projectiletrails.width": "Ширина",
"setting.projectiletrails.fill": "Заливка",
"setting.projectiletrails.fill_type": "Тип заливки",
"setting.projectiletrails.fill_type.option.Solid": "Сплошная",
"setting.projectiletrails.fill_type.option.Glow": "Свечение",
"setting.projectiletrails.lines": "Контур",
"setting.projectiletrails.line_width": "Толщина контура",
"setting.projectiletrails.depth_test": "Проверка глубины",
"setting.projectiletrails.color_mode": "Режим цвета",
"setting.projectiletrails.color": "Цвет",
"setting.projectiletrails.color2": "Второй цвет",
"setting.projectiletrails.color_speed": "Скорость цвета",
"setting.projectiletrails.projectiles": "Снаряды",
"setting.projectiletrails.color_mode.option.Static": "Статичный",
"setting.projectiletrails.color_mode.option.Rainbow": "Радуга",
"setting.projectiletrails.color_mode.option.LightRainbow": "Светлая радуга",
"setting.projectiletrails.color_mode.option.Sky": "Небо",
"setting.projectiletrails.color_mode.option.Fade": "Плавный",
"setting.projectiletrails.color_mode.option.DoubleColor": "Два цвета",
"setting.projectiletrails.color_mode.option.Analogous": "Аналоговый",
"setting.projectiletrails.color_mode.option.Theme": "Тема",
"setting.projectiletrails.projectiles.option.arrow": "Стрелы",
"setting.projectiletrails.projectiles.option.trident": "Трезубцы",
"setting.projectiletrails.projectiles.option.pearl": "Жемчуг Края",
"setting.projectiletrails.projectiles.option.snowball": "Снежки",
"setting.projectiletrails.projectiles.option.egg": "Яйца",
"setting.projectiletrails.projectiles.option.potion": "Зелья",
"setting.projectiletrails.projectiles.option.fireball": "Огненные шары",
"setting.projectiletrails.projectiles.option.wind_charge": "Ветряные заряды",
"setting.projectiletrails.projectiles.option.firework": "Фейерверки",
"setting.killeffect.mode.option.Shatter": "Раскол",
"setting.killeffect.mode.option.Ascend": "Вознесение",
"setting.killeffect.mode.option.Explosion": "Взрыв",
"setting.killeffect.mode.option.Disintegrate": "Распад",
"setting.killeffect.death_power": "Сила",
"setting.killeffect.death_duration": "Длительность (с)",
"setting.killeffect.death_spin": "Вращение",
"setting.killeffect.death_bounce": "Отскок от земли",
"setting.killeffect.death_sparks": "Искры",
"setting.killeffect.blast_color": "Цвет взрыва",
"setting.killeffect.blast_color.option.Natural": "Природный",
"setting.killeffect.blast_color.option.Client": "Цвет темы",
"setting.killeffect.blast_color.option.Custom": "Свой"
}

View file

@ -0,0 +1,81 @@
package dev.loki.lovisual.features.module.modules.visuals.effects.hits.death;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Part;
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.death.DeathPhysics.Style;
import org.junit.jupiter.api.Test;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
class DeathPhysicsTest {
private static Part part(float y) {
return new Part(1, 0, y, 0, 0.25f, 0.375f, 0.125f);
}
@Test
void shatterFallsAndRestsOnGround() {
Part p = part(1.1f);
DeathPhysics.launch(p, Style.SHATTER, 1.0f, 1.0f, 2.5f, 0, 1.0f, 0, new Random(1));
for (int i = 0; i < 400; i++) DeathPhysics.step(p, Style.SHATTER, 0.02f, i * 0.02f, 0.0f, false);
assertEquals(0.375f, p.y, 1e-4f);
assertEquals(0.0f, p.vy, 1e-6f);
}
@Test
void bounceKeepsPartAboveFloorAndLosesEnergy() {
Part p = part(1.5f);
p.vy = -6.0f;
float maxAfterFirst = 0;
boolean bounced = false;
for (int i = 0; i < 100; i++) {
DeathPhysics.step(p, Style.SHATTER, 0.02f, i * 0.02f, 0.0f, true);
assertTrue(p.y >= 0.375f - 1e-4f);
if (p.vy > 0) bounced = true;
if (bounced) maxAfterFirst = Math.max(maxAfterFirst, p.y);
}
assertTrue(bounced);
assertTrue(maxAfterFirst < 1.5f);
}
@Test
void ascendRisesAndFadesOut() {
Part p = part(1.0f);
DeathPhysics.launch(p, Style.ASCEND, 1.0f, 1.0f, 2.5f, 0, 1.0f, 0, new Random(2));
for (int i = 0; i < 50; i++) DeathPhysics.step(p, Style.ASCEND, 0.02f, i * 0.02f, 0.0f, false);
assertTrue(p.y > 1.4f);
assertEquals(1.0f, DeathPhysics.alpha(p, Style.ASCEND, 0.1f, 2.5f), 1e-6f);
assertEquals(0.0f, DeathPhysics.alpha(p, Style.ASCEND, 2.5f, 2.5f), 1e-6f);
}
@Test
void disintegrateDissolvesTopFirst() {
Part head = new Part(0, 0, 1.75f, 0, 0.25f, 0.25f, 0.25f);
Part leg = new Part(4, 0, 0.375f, 0, 0.125f, 0.375f, 0.125f);
Random r = new Random(3);
DeathPhysics.launch(head, Style.DISINTEGRATE, 1f, 1f, 2.5f, 0, 1f, 0, r);
DeathPhysics.launch(leg, Style.DISINTEGRATE, 1f, 1f, 2.5f, 0, 1f, 0, r);
assertTrue(head.delay < leg.delay);
float t = (head.delay + leg.delay) * 0.5f + 0.3f;
assertTrue(DeathPhysics.alpha(head, Style.DISINTEGRATE, t, 2.5f) < DeathPhysics.alpha(leg, Style.DISINTEGRATE, t, 2.5f));
}
@Test
void explosionLaunchesFasterThanShatter() {
Part a = part(1.1f), b = part(1.1f);
a.x = 0.4f;
b.x = 0.4f;
DeathPhysics.launch(a, Style.SHATTER, 1f, 1f, 2.5f, 0, 1f, 0, new Random(5));
DeathPhysics.launch(b, Style.EXPLOSION, 1f, 1f, 2.5f, 0, 1f, 0, new Random(5));
assertTrue(Math.abs(b.vx) > Math.abs(a.vx));
}
@Test
void bodyHasSixPartsAroundFeet() {
var parts = DeathBody.build(10, 64, -5, 0, 1.8f);
assertEquals(6, parts.size());
assertEquals(64 + 1.75f, parts.get(0).y, 1e-4f);
assertTrue(parts.get(2).x < parts.get(3).x);
}
}