From 8584bf771fa14a4342a01fa190f6ad700d264661 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Sat, 9 May 2026 19:04:09 +0200 Subject: [PATCH] Restore original explosion visuals --- .../client/ClientEffects.java | 462 ++++++++++++------ .../client/CustomGlowParticle.java | 353 +++++++++++-- .../client/ExplosionTextureManager.java | 52 +- .../client/ExplosionWindController.java | 65 ++- .../client/GroundDustEffect.java | 176 ++++++- .../client/GroundMistEffect.java | 128 ++++- .../client/PhysicsBasedExplosionEffect.java | 275 ++++++----- .../client/ShockwaveEffect.java | 74 +-- .../client/SmokeParticle.java | 78 ++- 9 files changed, 1267 insertions(+), 396 deletions(-) diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java index f306b49..1840f27 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java @@ -1,35 +1,51 @@ package com.vinlanx.explosionoverhaul.client; +import com.mojang.blaze3d.systems.RenderSystem; import com.vinlanx.explosionoverhaul.Config; -import com.vinlanx.explosionoverhaul.CustomGlowParticleOptions; import com.vinlanx.explosionoverhaul.ModParticles; import com.vinlanx.explosionoverhaul.PlayTrackedSoundPacket; -import com.vinlanx.explosionoverhaul.PlasmaParticleOptions; -import com.vinlanx.explosionoverhaul.SmokeParticleOptions; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Random; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.particles.SimpleParticleType; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import net.neoforged.neoforge.client.event.RenderGuiEvent; public class ClientEffects { + private static final Random RANDOM = new Random(); private static final List TRACKED_SOUNDS = new ArrayList<>(); private static final List DELAYED_SHAKES = new ArrayList<>(); private static final List EXPLOSION_EFFECTS = new ArrayList<>(); private static final List SHOCKWAVE_EFFECTS = new ArrayList<>(); - private static final List LINGERING_FOGS = new ArrayList<>(); - private static int shakeTicks; + private static final List DUST_EFFECTS = new ArrayList<>(); + private static final List MIST_EFFECTS = new ArrayList<>(); + private static final List FLASH_EFFECTS = new ArrayList<>(); private static float shakeIntensity; + private static int shakeTicks; private static float pushIntensity; - private static int flashTicks; - private static float flashPower; + private static float lastYawOffset; + private static float lastPitchOffset; public static void addTrackedSound(PlayTrackedSoundPacket msg) { synchronized (TRACKED_SOUNDS) { @@ -38,30 +54,53 @@ public class ClientEffects { } public static Vec3 calculateSoundPosition(Player player, Vec3 explosionPos, boolean isPlayerInHouse) { - if (player == null || !isPlayerInHouse) { + if (player == null) { return explosionPos; } - Vec3 direction = explosionPos.subtract(player.position()); - if (direction.lengthSqr() < 0.0001) { - return explosionPos; + Vec3 playerPos = player.position(); + Vec3 start = isPlayerInHouse ? explosionPos : playerPos; + Vec3 end = isPlayerInHouse ? playerPos : explosionPos; + BlockHitResult hit = player.level().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, (Entity)player)); + if (hit.getType() == HitResult.Type.MISS) { + Vec3 direction = explosionPos.subtract(playerPos).normalize(); + if (direction.lengthSqr() < 0.001) { + direction = player.getLookAngle(); + } + return playerPos.add(direction.scale(45.0)); } - return player.position().add(direction.normalize().scale(Math.min(16.0, direction.length()))); + Vec3 rayDir = end.subtract(start).normalize(); + return hit.getLocation().subtract(rayDir.scale(0.1)); } public static void triggerLocalCameraShake(float intensity, int durationTicks, float pushIntensity) { - shakeIntensity = Math.max(shakeIntensity, intensity); - shakeTicks = Math.max(shakeTicks, durationTicks); - ClientEffects.pushIntensity = Math.max(ClientEffects.pushIntensity, pushIntensity); + if (!Config.CLIENT.enableCameraShake.get() && !Config.COMMON.enablePlayerShake.get()) { + return; + } + float amplifiedIntensity = Config.CLIENT.enableCameraShake.get() + ? intensity * (Config.CLIENT.cameraShakeAmplifier.get().floatValue() * 10.0f) + : 0.0f; + float amplifiedPush = Config.COMMON.enablePlayerShake.get() + ? pushIntensity * (Config.COMMON.playerShakeAmplifier.get().floatValue() * 5.0f) + : 0.0f; + if (amplifiedIntensity > shakeIntensity || shakeTicks == 0 || durationTicks > shakeTicks) { + shakeIntensity = amplifiedIntensity; + shakeTicks = durationTicks; + ClientEffects.pushIntensity = amplifiedPush; + } } public static void triggerDelayedCameraShake(float intensity, int durationTicks, float pushIntensity, int delayTicks) { + if (delayTicks <= 0) { + triggerLocalCameraShake(intensity, durationTicks, pushIntensity); + return; + } synchronized (DELAYED_SHAKES) { - DELAYED_SHAKES.add(new DelayedShake(intensity, durationTicks, pushIntensity, Math.max(0, delayTicks))); + DELAYED_SHAKES.add(new DelayedShake(intensity, durationTicks, pushIntensity, delayTicks)); } } public static void triggerRealisticExplosion(Vec3 position, float power) { - if (!((Boolean)Config.CLIENT.enableExplosionParticles.get()).booleanValue()) { + if (!Config.CLIENT.enableExplosionParticles.get()) { return; } synchronized (EXPLOSION_EFFECTS) { @@ -70,68 +109,27 @@ public class ClientEffects { } public static void addFlashEffect(Vec3 explosionPos, float power) { - if (!((Boolean)Config.CLIENT.enableFlashEffect.get()).booleanValue()) { + if (!Config.CLIENT.enableFlashEffect.get()) { return; } - flashTicks = Math.max(flashTicks, Mth.clamp(9 + Mth.floor(power * 0.85f), 10, 42)); - flashPower = Math.max(flashPower, power); + synchronized (FLASH_EFFECTS) { + FLASH_EFFECTS.add(new FlashEffect(explosionPos, power)); + } } public static void onClientTick() { Minecraft mc = Minecraft.getInstance(); - synchronized (TRACKED_SOUNDS) { - TRACKED_SOUNDS.removeIf(sound -> sound.tick(mc)); - } - synchronized (DELAYED_SHAKES) { - DELAYED_SHAKES.removeIf(DelayedShake::tick); - } - synchronized (EXPLOSION_EFFECTS) { - Iterator iterator = EXPLOSION_EFFECTS.iterator(); - while (iterator.hasNext()) { - PhysicsBasedExplosionEffect effect = iterator.next(); - effect.tick(); - if (effect.isFinished()) { - iterator.remove(); - } - } - } - synchronized (SHOCKWAVE_EFFECTS) { - Iterator iterator = SHOCKWAVE_EFFECTS.iterator(); - while (iterator.hasNext()) { - ShockwaveEffect effect = iterator.next(); - effect.tick(); - if (effect.isFinished()) { - iterator.remove(); - } - } - } - synchronized (LINGERING_FOGS) { - Iterator iterator = LINGERING_FOGS.iterator(); - while (iterator.hasNext()) { - LingeringFog fog = iterator.next(); - if (fog.tick(mc)) { - iterator.remove(); - } - } - } - if (shakeTicks > 0 && mc.player != null) { - float fade = shakeTicks / Math.max(1.0f, shakeTicks + 8.0f); - float yaw = (mc.player.getRandom().nextFloat() - 0.5f) * shakeIntensity * 0.8f * fade; - float pitch = (mc.player.getRandom().nextFloat() - 0.5f) * shakeIntensity * 0.45f * fade; - mc.player.setYRot(mc.player.getYRot() + yaw); - mc.player.setXRot(Mth.clamp(mc.player.getXRot() + pitch, -89.0f, 89.0f)); - if (pushIntensity > 0.02f) { - mc.player.setDeltaMovement(mc.player.getDeltaMovement().add(0.0, Math.min(0.03, pushIntensity * 0.004), 0.0)); - } - --shakeTicks; - if (shakeTicks <= 0) { - shakeIntensity = 0.0f; - pushIntensity = 0.0f; - } - } - if (flashTicks > 0) { - --flashTicks; + ExplosionWindController.tick(); + tickSounds(mc); + tickDelayedShakes(); + tickList(EXPLOSION_EFFECTS); + tickList(SHOCKWAVE_EFFECTS); + tickList(DUST_EFFECTS); + tickList(MIST_EFFECTS); + synchronized (FLASH_EFFECTS) { + FLASH_EFFECTS.removeIf(FlashEffect::isFinished); } + handleCameraShakeTick(mc); } public static void renderFlash(RenderGuiEvent.Post event) { @@ -139,19 +137,36 @@ public class ClientEffects { } public static void renderFlash(GuiGraphics graphics) { - if (flashTicks <= 0) { + if (!Config.CLIENT.enableFlashEffect.get()) { return; } Minecraft mc = Minecraft.getInstance(); - float alpha = Mth.clamp(flashTicks / 18.0f, 0.0f, 1.0f) * ((Double)Config.CLIENT.flashMaxOpacity.get()).floatValue(); - alpha = Mth.clamp(alpha * (0.7f + flashPower * 0.026f), 0.0f, 0.92f); - int color = ((int)(alpha * 255.0f) << 24) | 0xFFFFD7; - graphics.fill(0, 0, mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight(), color); + if (mc.player == null) { + return; + } + float maxOpacity = 0.0f; + synchronized (FLASH_EFFECTS) { + for (FlashEffect effect : FLASH_EFFECTS) { + maxOpacity = Math.max(maxOpacity, effect.getCurrentOpacity(mc.player)); + } + } + if (maxOpacity <= 0.0f) { + return; + } + + ResourceLocation flashTexture = ResourceLocation.fromNamespaceAndPath("explosionoverhaul", "textures/effects/flash.png"); + int width = mc.getWindow().getGuiScaledWidth(); + int height = mc.getWindow().getGuiScaledHeight(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, maxOpacity); + graphics.blit(flashTexture, 0, 0, 0.0f, 0.0f, width, height, width, height); + RenderSystem.disableBlend(); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); } public static void triggerShockwave(Vec3 position, float power) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null || !((Boolean)Config.CLIENT.enableShockwaveEffect.get()).booleanValue()) { + if (!Config.CLIENT.enableShockwaveEffect.get()) { return; } synchronized (SHOCKWAVE_EFFECTS) { @@ -160,109 +175,152 @@ public class ClientEffects { } public static void triggerDustCloud(Vec3 position, float power) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null || !((Boolean)Config.CLIENT.enableGroundDustEffect.get()).booleanValue()) { + if (!Config.CLIENT.enableGroundDustEffect.get()) { return; } - float quality = ((Double)Config.CLIENT.groundDustQuality.get()).floatValue(); - int count = Math.min(260, Math.max(36, Math.round(power * 9.5f * quality))); - float coreScale = Mth.clamp(Math.max(1.6f, power * 0.42f), 1.6f, 10.5f); - mc.level.addParticle(new SmokeParticleOptions(coreScale, 110, 0.2f, 0.17f, 0.14f, 0.64f, true, 0.0f, 0.2f, null), position.x, position.y + 0.2, position.z, 0.0, 0.045, 0.0); - for (int i = 0; i < count; ++i) { - Vec3 velocity = randomHorizontal(mc.level.random.nextLong()).scale(0.04 + mc.level.random.nextDouble() * 0.16); - mc.level.addParticle(new SmokeParticleOptions(1.0f + mc.level.random.nextFloat() * 2.1f, 58 + mc.level.random.nextInt(62), 0.18f, 0.16f, 0.13f, 0.52f, true, 0.0f, 0.2f, null), position.x, position.y + 0.1, position.z, velocity.x, 0.025 + mc.level.random.nextDouble() * 0.05, velocity.z); + synchronized (DUST_EFFECTS) { + DUST_EFFECTS.add(new GroundDustEffect(position, power)); } } public static void triggerMistCloud(Vec3 position, float power) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null || !((Boolean)Config.CLIENT.enableGroundMistEffect.get()).booleanValue()) { + if (!Config.CLIENT.enableGroundMistEffect.get()) { return; } - int count = Math.min(160, Math.max(24, Math.round(power * 6.0f * ((Double)Config.CLIENT.groundMistQuality.get()).floatValue()))); - for (int i = 0; i < count; ++i) { - Vec3 velocity = randomHorizontal(mc.level.random.nextLong()).scale(0.025 + mc.level.random.nextDouble() * 0.11); - mc.level.addParticle(new SmokeParticleOptions(1.2f + mc.level.random.nextFloat() * 2.2f, 65 + mc.level.random.nextInt(60), 0.32f, 0.31f, 0.29f, 0.34f, false, 0.0f, 0.1f, null), position.x, position.y + 0.08, position.z, velocity.x, 0.01, velocity.z); - } - synchronized (LINGERING_FOGS) { - LINGERING_FOGS.add(new LingeringFog(position, power)); + synchronized (MIST_EFFECTS) { + MIST_EFFECTS.add(new GroundMistEffect(position, power)); } } public static void triggerLineSparks(Vec3 position, float power) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null || !((Boolean)Config.CLIENT.enableLineSparks.get()).booleanValue()) { + if (!Config.CLIENT.enableLineSparks.get()) { return; } - int count = Math.min(160, Math.max(14, Math.round(power * 6.0f * ((Double)Config.CLIENT.lineSparkAmountMultiplier.get()).floatValue()))); - for (int i = 0; i < count; ++i) { - Vec3 velocity = randomHorizontal(mc.level.random.nextLong()).scale(0.25 + mc.level.random.nextDouble() * Math.min(1.2, power * 0.04)); - mc.level.addParticle(ModParticles.LINE_SPARK.get(), position.x, position.y + 0.2, position.z, velocity.x, mc.level.random.nextDouble() * 0.35, velocity.z); + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + if (level == null) { + return; + } + double amountMultiplier = Config.CLIENT.lineSparkAmountMultiplier.get(); + if (amountMultiplier <= 0.0) { + return; + } + int sparkCount = Mth.clamp((int)(power * 15.0f * amountMultiplier), 15, 500); + for (int i = 0; i < sparkCount; ++i) { + Vec3 motion = new Vec3(RANDOM.nextDouble() - 0.5, RANDOM.nextDouble() - 0.5, RANDOM.nextDouble() - 0.5).normalize(); + float powerFraction = power / 100.0f; + float force = RANDOM.nextFloat() < 0.25f + ? Mth.lerp(powerFraction, 0.5f, 8.0f) * (0.9f + RANDOM.nextFloat() * 0.4f) + : Mth.lerp(powerFraction, 0.3f, 4.5f) * (0.7f + RANDOM.nextFloat() * 0.4f); + motion = motion.scale(force); + level.addParticle((SimpleParticleType)ModParticles.LINE_SPARK.get(), position.x, position.y, position.z, motion.x, motion.y, motion.z); } } public static void triggerAmbientCaveDust(float power) { Minecraft mc = Minecraft.getInstance(); - if (mc.level == null || mc.player == null) { + LocalPlayer player = mc.player; + ClientLevel level = mc.level; + if (player == null || level == null) { return; } - Vec3 center = mc.player.position(); - int count = Math.min(180, Math.max(28, Math.round(power * 5.0f))); - for (int i = 0; i < count; ++i) { - double x = center.x + (mc.level.random.nextDouble() - 0.5) * 16.0; - double z = center.z + (mc.level.random.nextDouble() - 0.5) * 16.0; - double y = center.y + 3.0 + mc.level.random.nextDouble() * 9.0; - mc.level.addParticle(new SmokeParticleOptions(0.6f + mc.level.random.nextFloat() * 1.1f, 55 + mc.level.random.nextInt(45), 0.2f, 0.19f, 0.17f, 0.45f, true, 0.0f, 0.4f, null), x, y, z, 0.0, -0.015 - mc.level.random.nextDouble() * 0.04, 0.0); + int particleCount = Mth.clamp(15 + (int)((power / 10.0f) * 3.5f), 10, 70); + BlockPos playerPos = player.blockPosition(); + BlockState ceilingState = Blocks.STONE.defaultBlockState(); + for (int i = 3; i < 10; ++i) { + BlockState state = level.getBlockState(playerPos.above(i)); + if (!state.isAir()) { + ceilingState = state; + break; + } + } + for (int i = 0; i < particleCount; ++i) { + double x = player.getX() + (RANDOM.nextDouble() - 0.5) * 16.0; + double z = player.getZ() + (RANDOM.nextDouble() - 0.5) * 16.0; + double y = player.getY() + 4.0 + RANDOM.nextDouble() * 6.0; + level.addParticle((ParticleOptions)new BlockParticleOption(ParticleTypes.BLOCK, ceilingState), x, y, z, 0.0, -0.15, 0.0); } } - private static Vec3 randomHorizontal(long seed) { - double angle = ((seed ^ (seed >>> 32)) & 0xFFFF) / 65535.0 * Math.PI * 2.0; - return new Vec3(Math.cos(angle), 0.0, Math.sin(angle)); + private static void tickList(List effects) { + synchronized (effects) { + Iterator iterator = effects.iterator(); + while (iterator.hasNext()) { + T effect = iterator.next(); + if (effect instanceof PhysicsBasedExplosionEffect explosion) { + explosion.tick(); + if (explosion.isFinished()) { + iterator.remove(); + } + } else if (effect instanceof ShockwaveEffect shockwave) { + shockwave.tick(); + if (shockwave.isFinished()) { + iterator.remove(); + } + } else if (effect instanceof GroundDustEffect dust) { + dust.tick(); + if (dust.isFinished()) { + iterator.remove(); + } + } else if (effect instanceof GroundMistEffect mist) { + mist.tick(); + if (mist.isFinished()) { + iterator.remove(); + } + } + } + } } - private static final class LingeringFog { - private final Vec3 position; - private final float power; - private final int lifetime; - private final float radius; - private int age; + private static void tickSounds(Minecraft mc) { + synchronized (TRACKED_SOUNDS) { + TRACKED_SOUNDS.removeIf(sound -> sound.tick(mc)); + } + } - private LingeringFog(Vec3 position, float power) { - this.position = position; - this.power = power; - this.lifetime = Mth.clamp(80 + Math.round(power * 4.0f), 90, 260); - this.radius = Mth.clamp(5.0f + power * 1.8f, 7.0f, 72.0f); + private static void tickDelayedShakes() { + synchronized (DELAYED_SHAKES) { + DELAYED_SHAKES.removeIf(DelayedShake::tick); + } + } + + private static void handleCameraShakeTick(Minecraft mc) { + LocalPlayer player = mc.player; + if (player == null) { + lastYawOffset = 0.0f; + lastPitchOffset = 0.0f; + shakeTicks = 0; + shakeIntensity = 0.0f; + pushIntensity = 0.0f; + return; } - private boolean tick(Minecraft mc) { - if (mc.level == null) { - return true; - } - if (++this.age >= this.lifetime) { - return true; - } - if (!((Boolean)Config.CLIENT.enableGroundMistEffect.get()).booleanValue() && !((Boolean)Config.CLIENT.enableGroundDustEffect.get()).booleanValue()) { - return false; - } - if (this.age % 2 != 0) { - return false; - } - float progress = this.age / (float)this.lifetime; - float fade = Mth.clamp(1.0f - progress, 0.0f, 1.0f); - int count = Mth.clamp(Math.round(this.power * 0.26f * fade), 1, 10); - for (int i = 0; i < count; ++i) { - double angle = mc.level.random.nextDouble() * Math.PI * 2.0; - double distance = this.radius * (0.18 + mc.level.random.nextDouble() * (0.82 + progress * 0.35)); - double x = this.position.x + Math.cos(angle) * distance; - double z = this.position.z + Math.sin(angle) * distance; - double y = this.position.y + 0.04 + mc.level.random.nextDouble() * 0.55; - float scale = Mth.clamp(1.1f + this.power * 0.075f + mc.level.random.nextFloat() * 1.6f, 1.0f, 4.8f); - float alpha = Mth.clamp(0.30f * fade, 0.03f, 0.30f); - mc.level.addParticle(new SmokeParticleOptions(scale, 80 + mc.level.random.nextInt(70), 0.29f, 0.28f, 0.26f, alpha, false, 0.0f, 0.04f, null), x, y, z, Math.cos(angle) * 0.018, 0.004, Math.sin(angle) * 0.018); - } - return false; + if (lastYawOffset != 0.0f || lastPitchOffset != 0.0f) { + player.setYRot(player.getYRot() - lastYawOffset); + player.setXRot(Mth.clamp(player.getXRot() - lastPitchOffset, -89.0f, 89.0f)); + lastYawOffset = 0.0f; + lastPitchOffset = 0.0f; } + + if (shakeTicks <= 0) { + shakeIntensity = 0.0f; + pushIntensity = 0.0f; + return; + } + + float progress = shakeTicks / 30.0f; + float cameraIntensity = shakeIntensity * Mth.sin(progress * (float)Math.PI); + float yawChange = (RANDOM.nextFloat() - 0.5f) * 2.0f * cameraIntensity; + float pitchChange = (RANDOM.nextFloat() - 0.5f) * cameraIntensity; + player.setYRot(player.getYRot() + yawChange); + player.setXRot(Mth.clamp(player.getXRot() + pitchChange, -89.0f, 89.0f)); + lastYawOffset = yawChange; + lastPitchOffset = pitchChange; + if (pushIntensity > 0.0f) { + float pushFactor = pushIntensity * Mth.sin(progress * (float)Math.PI) * 0.1f; + player.setDeltaMovement(player.getDeltaMovement().add((RANDOM.nextDouble() - 0.5) * pushFactor, 0.0, (RANDOM.nextDouble() - 0.5) * pushFactor)); + } + --shakeTicks; } private static final class DelayedSound { @@ -311,4 +369,104 @@ public class ClientEffects { return true; } } + + private static final class FlashEffect { + private final Vec3 explosionPos; + private final float power; + private final long startTime; + private final float maxDurationTicks; + private final float maxOpacity; + private final float maxDistance; + + private FlashEffect(Vec3 explosionPos, float power) { + this.explosionPos = explosionPos; + this.power = power; + this.startTime = System.currentTimeMillis(); + this.maxDurationTicks = calculateDuration(power) * 20.0f; + this.maxOpacity = calculateMaxOpacity(power); + this.maxDistance = calculateMaxDistance(power); + } + + private float getCurrentOpacity(Player player) { + float elapsedTicks = (System.currentTimeMillis() - this.startTime) / 50.0f; + if (elapsedTicks > this.maxDurationTicks) { + return 0.0f; + } + float progress = elapsedTicks / this.maxDurationTicks; + float fadeOpacity = this.maxOpacity * (1.0f - progress * progress); + double distance = player.position().distanceTo(this.explosionPos); + float distanceFactor = Math.max(0.0f, 1.0f - (float)distance / this.maxDistance); + Vec3 viewVec = player.getLookAngle(); + Vec3 dirToExplosion = this.explosionPos.subtract(player.position()).normalize(); + float angleFactor = Math.max(0.0f, (float)viewVec.dot(dirToExplosion)); + if (!isVisible(player, this.explosionPos)) { + return 0.0f; + } + return fadeOpacity * distanceFactor * angleFactor; + } + + private boolean isVisible(Player player, Vec3 explosionPos) { + Vec3 start = player.position(); + BlockHitResult hit = player.level().clip(new ClipContext(start, explosionPos, ClipContext.Block.VISUAL, ClipContext.Fluid.NONE, (Entity)player)); + if (hit.getType() == HitResult.Type.MISS) { + return true; + } + if (this.power > 10.0f) { + int checks = Math.max(1, (int)(this.power / 10.0f)); + for (int i = 1; i <= checks; ++i) { + Vec3 elevatedStart = start.add(0.0, 5.0 * i, 0.0); + Vec3 elevatedEnd = explosionPos.add(0.0, 5.0 * i, 0.0); + BlockHitResult elevatedHit = player.level().clip(new ClipContext(elevatedStart, elevatedEnd, ClipContext.Block.VISUAL, ClipContext.Fluid.NONE, (Entity)player)); + if (elevatedHit.getType() == HitResult.Type.MISS) { + return true; + } + } + } + return false; + } + + private boolean isFinished() { + return (System.currentTimeMillis() - this.startTime) / 50.0f > this.maxDurationTicks; + } + + private static float calculateDuration(float power) { + if (power <= 1.0f) { + return 0.5f; + } + if (power <= 5.0f) { + return 1.0f; + } + if (power <= 10.0f) { + return 2.5f; + } + if (power <= 25.0f) { + return 3.5f; + } + return 5.5f; + } + + private static float calculateMaxDistance(float power) { + if (power <= 1.0f) { + return 30.0f; + } + if (power <= 5.0f) { + return 30.0f + (power - 1.0f) * 70.0f / 4.0f; + } + if (power <= 10.0f) { + return 100.0f + (power - 5.0f) * 100.0f / 5.0f; + } + if (power <= 25.0f) { + return 200.0f + (power - 10.0f) * 200.0f / 15.0f; + } + if (power <= 40.0f) { + return 400.0f + (power - 25.0f) * 300.0f / 15.0f; + } + return 700.0f + (power - 40.0f) * 50.0f; + } + + private static float calculateMaxOpacity(float power) { + float baseOpacity = power <= 1.0f ? 0.5f : power <= 5.0f ? 0.7f : power <= 10.0f ? 0.9f : 1.0f; + return baseOpacity * Config.CLIENT.flashMaxOpacity.get().floatValue(); + } + } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java b/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java index e8690c4..d2c4c2d 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java @@ -1,82 +1,331 @@ package com.vinlanx.explosionoverhaul.client; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; import com.vinlanx.explosionoverhaul.Config; import com.vinlanx.explosionoverhaul.CustomGlowParticleOptions; +import net.minecraft.client.Camera; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.particle.ParticleRenderType; import net.minecraft.client.particle.SpriteSet; import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; public class CustomGlowParticle extends TextureSheetParticle { - private final SpriteSet sprites; - private final float baseSize; + private static final int GLOW_FRAME_COUNT = 239; + private static final int SGLOW_FRAME_COUNT = 224; + private static final int FRAMES_PER_SHEET = 64; + private static final int FRAMES_PER_ROW = 8; + + private final float power; + private final float initialQuadSize; + private final int animationDuration; + private final int animationType; + private final float maxRadius; + private float currentHeightPercent; + private final int peakFireEndTick; + private final int fireTransitionEndTick; + private final int smokeStartTick; + private int baseSheetIndex; + private int baseFrameOnSheet; + private int emissiveSheetIndex; + private int emissiveFrameOnSheet; public CustomGlowParticle(ClientLevel level, double x, double y, double z, double xSpeed, double ySpeed, double zSpeed, CustomGlowParticleOptions options, SpriteSet sprites) { super(level, x, y, z, xSpeed, ySpeed, zSpeed); - this.sprites = sprites; - Config.Client.ParticleRenderMode mode = Config.CLIENT.particleRenderMode.get(); - float configScale = ((Double)Config.CLIENT.particleSizeScale.get()).floatValue(); - float powerScale = Mth.clamp(options.getPower() / 4.0f, 0.8f, 4.0f); - float requestedSize = options.getScale() * configScale * powerScale * (0.85f + this.random.nextFloat() * 0.35f); - float maxSize = switch (mode) { - case REALISTIC -> 5.0f; - case REALISTIC_2 -> 5.6f; - case VANILA -> 3.8f; - }; - this.baseSize = Mth.clamp(requestedSize, 0.2f, maxSize); - this.lifetime = switch (mode) { - case REALISTIC -> 34 + this.random.nextInt(15); - case REALISTIC_2 -> 44 + this.random.nextInt(22); - case VANILA -> 22 + this.random.nextInt(12); - }; - this.quadSize = this.baseSize; - this.friction = 0.88f; - this.gravity = mode == Config.Client.ParticleRenderMode.VANILA ? -0.01f : -0.018f; - this.hasPhysics = false; - this.xd = xSpeed * 0.85; - this.yd = ySpeed * 0.85 + 0.01; - this.zd = zSpeed * 0.85; - this.alpha = 0.92f; - this.pickSprite(sprites); - applyModeColor(mode, options.getZone()); - } + int fadeOutDurationTicks; + this.friction = 0.98f; + this.gravity = 0.0f; + this.xd = xSpeed; + this.yd = ySpeed; + this.zd = zSpeed; + this.power = options.getPower(); + this.initialQuadSize = options.getScale(); + this.animationType = options.getAnimationType(); + this.quadSize = this.initialQuadSize; + this.maxRadius = options.getMaxRadius(); + this.currentHeightPercent = options.getHeightPercent(); - @Override - public ParticleRenderType getRenderType() { - return ParticleRenderType.PARTICLE_SHEET_TRANSLUCENT; + if (this.power <= 1.0f) { + this.animationDuration = 40; + fadeOutDurationTicks = 40; + } else if (this.power <= 10.0f) { + this.animationDuration = interpolate(this.power, 1.0f, 40.0f, 10.0f, 100.0f); + fadeOutDurationTicks = interpolate(this.power, 1.0f, 40.0f, 10.0f, 60.0f); + } else if (this.power <= 60.0f) { + this.animationDuration = interpolate(this.power, 10.0f, 100.0f, 60.0f, 140.0f); + fadeOutDurationTicks = interpolate(this.power, 10.0f, 60.0f, 60.0f, 80.0f); + } else if (this.power <= 100.0f) { + this.animationDuration = interpolate(this.power, 60.0f, 140.0f, 100.0f, 200.0f); + fadeOutDurationTicks = interpolate(this.power, 60.0f, 80.0f, 100.0f, 120.0f); + } else { + this.animationDuration = 200; + fadeOutDurationTicks = 120; + } + this.lifetime = this.animationDuration + fadeOutDurationTicks; + + float powerFraction = Mth.clamp(this.power / 100.0f, 0.0f, 1.0f); + float totalFirePercent = Mth.lerp(powerFraction, 0.02f, 0.2f); + float cooldownToSmokePercent = Mth.lerp(powerFraction, 0.04f, 0.08f); + int totalFireDuration = (int)(this.lifetime * totalFirePercent); + this.peakFireEndTick = (int)(totalFireDuration * 0.9f); + this.fireTransitionEndTick = totalFireDuration; + this.smokeStartTick = this.fireTransitionEndTick + (int)(this.lifetime * cooldownToSmokePercent); + + if (Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.VANILA) { + this.alpha = 1.0f; + this.rCol = 1.0f; + this.gCol = 0.85f; + this.bCol = 0.5f; + } } @Override public void tick() { - super.tick(); - if (this.age >= this.lifetime) { + double targetSpeed; + this.xo = this.x; + this.yo = this.y; + this.zo = this.z; + if (this.age++ >= this.lifetime) { + this.remove(); return; } - float progress = this.age / (float)this.lifetime; - this.alpha = Mth.clamp(1.0f - progress, 0.0f, 1.0f) * 0.92f; - this.quadSize = this.baseSize * (0.85f + progress * 1.15f); + + if (Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.VANILA) { + this.tickVanila(); + } else if (Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.REALISTIC_2) { + this.tickRealistic2(); + } else { + this.tickRealistic(); + } + + this.move(this.xd, this.yd, this.zd); + this.xd *= this.friction; + this.yd *= this.friction; + this.zd *= this.friction; + + float heightPercent = this.updateHeightPercent(); + Vec3 direction = ExplosionWindController.getWindDirection(); + if (direction.lengthSqr() > 1.0E-4 && (targetSpeed = ExplosionWindController.computeGlowSpeed(heightPercent)) > 0.0) { + double targetX = direction.x * targetSpeed; + double targetZ = direction.z * targetSpeed; + this.xd += (targetX - this.xd) * 0.1; + this.zd += (targetZ - this.zd) * 0.1; + } } - public boolean shouldCull() { - return false; + private void tickVanila() { + this.baseSheetIndex = ExplosionTextureManager.INDEX_SOFT_GLOW; + this.quadSize = this.initialQuadSize; + if (this.age < this.peakFireEndTick) { + this.alpha = 1.0f; + this.rCol = 1.0f; + this.gCol = 0.85f; + this.bCol = 0.5f; + } else if (this.age < this.fireTransitionEndTick) { + float progress = (this.age - this.peakFireEndTick) / (float)Math.max(1, this.fireTransitionEndTick - this.peakFireEndTick); + this.alpha = 1.0f; + this.rCol = 1.0f; + this.gCol = Mth.lerp(progress, 0.85f, 0.5f); + this.bCol = Mth.lerp(progress, 0.5f, 0.1f); + } else if (this.age < this.smokeStartTick) { + float progress = (this.age - this.fireTransitionEndTick) / (float)Math.max(1, this.smokeStartTick - this.fireTransitionEndTick); + float smokeCol = 0.15f; + this.rCol = Mth.lerp(progress, 1.0f, smokeCol + 0.1f); + this.gCol = Mth.lerp(progress, 0.5f, smokeCol); + this.bCol = Mth.lerp(progress, 0.1f, smokeCol); + this.alpha = Mth.lerp(progress, 1.0f, 0.9f); + } else { + float progress = (this.age - this.smokeStartTick) / (float)Math.max(1, this.lifetime - this.smokeStartTick); + float finalSmokeCol = 0.05f; + this.gCol = this.bCol = Mth.lerp(progress, 0.15f, finalSmokeCol); + this.rCol = this.bCol; + this.alpha = 0.9f * (1.0f - progress * progress * progress); + } } - private void applyModeColor(Config.Client.ParticleRenderMode mode, int zone) { - if (mode == Config.Client.ParticleRenderMode.VANILA) { - this.rCol = zone == 0 ? 1.0f : 0.95f; - this.gCol = zone == 0 ? 0.55f : 0.72f; - this.bCol = zone == 0 ? 0.15f : 0.3f; + private void tickRealistic2() { + this.quadSize = this.initialQuadSize * 2.0f; + this.setAnimatedFrame(); + } + + private void tickRealistic() { + this.setAnimatedFrame(); + } + + private void setAnimatedFrame() { + int frameCount; + int baseSheetStart; + int emissiveSheetStart; + if (this.animationType == 2) { + frameCount = SGLOW_FRAME_COUNT; + baseSheetStart = ExplosionTextureManager.SGLOW_BASE_START; + emissiveSheetStart = ExplosionTextureManager.SGLOW_EMISSIVE_START; + } else if (this.animationType == 1) { + frameCount = GLOW_FRAME_COUNT; + baseSheetStart = ExplosionTextureManager.GLOW2_BASE_START; + emissiveSheetStart = ExplosionTextureManager.GLOW2_EMISSIVE_START; + } else { + frameCount = GLOW_FRAME_COUNT; + baseSheetStart = ExplosionTextureManager.GLOW_BASE_START; + emissiveSheetStart = ExplosionTextureManager.GLOW_EMISSIVE_START; + } + + int frameIndex; + if (this.age < this.animationDuration) { + float animProgress = this.age / (float)this.animationDuration; + float easedProgress = 1.0f - (float)Math.pow(1.0f - animProgress, 3.0); + frameIndex = (int)(easedProgress * (frameCount - 1)); + this.alpha = 1.0f; + } else { + frameIndex = frameCount - 1; + int fadeDuration = Math.max(1, this.lifetime - this.animationDuration); + int ageInFade = this.age - this.animationDuration; + this.alpha = 1.0f - ageInFade / (float)fadeDuration; + } + + frameIndex = Mth.clamp(frameIndex, 0, frameCount - 1); + this.baseSheetIndex = baseSheetStart + frameIndex / FRAMES_PER_SHEET; + this.baseFrameOnSheet = frameIndex % FRAMES_PER_SHEET; + this.emissiveSheetIndex = emissiveSheetStart + frameIndex / FRAMES_PER_SHEET; + this.emissiveFrameOnSheet = frameIndex % FRAMES_PER_SHEET; + } + + @Override + public void render(VertexConsumer buffer, Camera renderInfo, float partialTicks) { + Vec3 cameraPos = renderInfo.getPosition(); + float x = (float)(Mth.lerp(partialTicks, this.xo, this.x) - cameraPos.x()); + float y = (float)(Mth.lerp(partialTicks, this.yo, this.y) - cameraPos.y()); + float z = (float)(Mth.lerp(partialTicks, this.zo, this.z) - cameraPos.z()); + Quaternionf quaternion = new Quaternionf(renderInfo.rotation()); + if (this.roll != 0.0f) { + quaternion.rotateZ(Mth.lerp(partialTicks, this.oRoll, this.roll)); + } + + Vector3f[] vertices; + if (this.animationType == 2) { + float aspectRatio = 0.5629139f; + vertices = new Vector3f[]{ + new Vector3f(-aspectRatio, -1.0f, 0.0f), + new Vector3f(-aspectRatio, 1.0f, 0.0f), + new Vector3f(aspectRatio, 1.0f, 0.0f), + new Vector3f(aspectRatio, -1.0f, 0.0f)}; + } else { + vertices = new Vector3f[]{ + new Vector3f(-1.0f, -1.0f, 0.0f), + new Vector3f(-1.0f, 1.0f, 0.0f), + new Vector3f(1.0f, 1.0f, 0.0f), + new Vector3f(1.0f, -1.0f, 0.0f)}; + } + + float quad = this.getQuadSize(partialTicks); + for (Vector3f vertex : vertices) { + vertex.rotate(quaternion); + vertex.mul(quad); + vertex.add(x, y, z); + } + + int light = this.getLightColor(partialTicks); + int frame = Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.VANILA ? 0 : this.baseFrameOnSheet; + ResourceLocation baseTexture = ExplosionTextureManager.getInstance().getTexture(this.baseSheetIndex); + this.renderFrame(vertices, baseTexture, frame, this.rCol, this.gCol, this.bCol, this.alpha, light); + + if (Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.REALISTIC + || Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.REALISTIC_2) { + ResourceLocation emissiveTexture = ExplosionTextureManager.getInstance().getTexture(this.emissiveSheetIndex); + this.renderFrame(vertices, emissiveTexture, this.emissiveFrameOnSheet, this.rCol, this.gCol, this.bCol, this.alpha, 240); + } else if (this.age < this.smokeStartTick) { + ResourceLocation emissiveTexture = ExplosionTextureManager.getInstance().getTexture(ExplosionTextureManager.INDEX_SOFT_GLOW_E); + this.renderFrame(vertices, emissiveTexture, 0, this.rCol, this.gCol, this.bCol, this.alpha * 0.8f, 240); + } + } + + private void renderFrame(Vector3f[] vertices, ResourceLocation texture, int frame, float red, float green, float blue, float particleAlpha, int light) { + if (texture == null || particleAlpha <= 0.0f) { return; } - if (mode == Config.Client.ParticleRenderMode.REALISTIC_2) { - this.rCol = zone == 0 ? 1.0f : 0.82f; - this.gCol = zone == 0 ? 0.76f : 0.92f; - this.bCol = zone == 0 ? 0.42f : 1.0f; - return; + + float u0; + float v0; + float u1; + float v1; + if (frame == 0 && Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.VANILA) { + u0 = 0.0f; + v0 = 0.0f; + u1 = 1.0f; + v1 = 1.0f; + } else { + float frameUWidth = 1.0f / FRAMES_PER_ROW; + float frameVHeight = 1.0f / FRAMES_PER_ROW; + int col = frame % FRAMES_PER_ROW; + int row = frame / FRAMES_PER_ROW; + u0 = col * frameUWidth; + v0 = row * frameVHeight; + u1 = u0 + frameUWidth; + v1 = v0 + frameVHeight; } - this.rCol = zone == 0 ? 1.0f : 1.0f; - this.gCol = zone == 0 ? 0.48f : 0.72f; - this.bCol = zone == 0 ? 0.08f : 0.2f; + + RenderSystem.setShader(GameRenderer::getParticleShader); + RenderSystem.setShaderTexture(0, texture); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.depthMask(true); + BufferBuilder builder = com.mojang.blaze3d.vertex.Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE); + builder.addVertex(vertices[0].x(), vertices[0].y(), vertices[0].z()).setUv(u0, v1).setColor(red, green, blue, particleAlpha).setLight(light); + builder.addVertex(vertices[1].x(), vertices[1].y(), vertices[1].z()).setUv(u0, v0).setColor(red, green, blue, particleAlpha).setLight(light); + builder.addVertex(vertices[2].x(), vertices[2].y(), vertices[2].z()).setUv(u1, v0).setColor(red, green, blue, particleAlpha).setLight(light); + builder.addVertex(vertices[3].x(), vertices[3].y(), vertices[3].z()).setUv(u1, v1).setColor(red, green, blue, particleAlpha).setLight(light); + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + @Override + public ParticleRenderType getRenderType() { + return ParticleRenderType.CUSTOM; + } + + @Override + public int getLightColor(float partialTick) { + if (Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.VANILA) { + float currentAge = this.age + partialTick; + if (currentAge < this.fireTransitionEndTick) { + return 240; + } + if (currentAge > this.smokeStartTick) { + return super.getLightColor(partialTick); + } + float progress = (currentAge - this.fireTransitionEndTick) / Math.max(1.0f, this.smokeStartTick - this.fireTransitionEndTick); + int packedAmbient = super.getLightColor(partialTick); + int skyLightAmbient = packedAmbient >> 20 & 0xF; + int blockLightAmbient = packedAmbient >> 4 & 0xF; + int skyLightCurrent = (int)Mth.lerp(progress, 15.0f, skyLightAmbient); + int blockLightCurrent = (int)Mth.lerp(progress, 15.0f, blockLightAmbient); + return skyLightCurrent << 20 | blockLightCurrent << 4; + } + return super.getLightColor(partialTick); + } + + private static int interpolate(float power, float p1, float v1, float p2, float v2) { + if (p2 == p1) { + return (int)v1; + } + float fraction = (power - p1) / (p2 - p1); + return (int)(v1 + fraction * (v2 - v1)); + } + + private float updateHeightPercent() { + if (this.maxRadius <= 0.0f) { + return 0.5f; + } + return Math.max(0.25f, this.currentHeightPercent); } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionTextureManager.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionTextureManager.java index 73a1207..9fc842b 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionTextureManager.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionTextureManager.java @@ -1,9 +1,20 @@ package com.vinlanx.explosionoverhaul.client; +import com.vinlanx.explosionoverhaul.Config; import net.minecraft.resources.ResourceLocation; public class ExplosionTextureManager { private static final ExplosionTextureManager INSTANCE = new ExplosionTextureManager(); + private static final ResourceLocation FALLBACK = ResourceLocation.fromNamespaceAndPath("minecraft", "textures/particle/generic_0.png"); + + public static final int INDEX_SOFT_GLOW = 0; + public static final int INDEX_SOFT_GLOW_E = 1; + public static final int GLOW_BASE_START = 2; + public static final int GLOW_EMISSIVE_START = 6; + public static final int GLOW2_BASE_START = 10; + public static final int GLOW2_EMISSIVE_START = 14; + public static final int SGLOW_BASE_START = 18; + public static final int SGLOW_EMISSIVE_START = 22; public static ExplosionTextureManager getInstance() { return INSTANCE; @@ -13,9 +24,48 @@ public class ExplosionTextureManager { } public ResourceLocation getTexture(int index) { - return ResourceLocation.fromNamespaceAndPath("minecraft", "textures/particle/generic_0.png"); + if (index == INDEX_SOFT_GLOW) { + return texture("soft_glow.png"); + } + if (index == INDEX_SOFT_GLOW_E) { + return texture("soft_glow_e.png"); + } + + ResourceLocation sheet = sheetTexture(index, Config.CLIENT.glowTextureQuality.get() == Config.Client.GlowTextureQuality.QUALITY_64); + return sheet == null ? FALLBACK : sheet; } public void clear() { } + + private static ResourceLocation sheetTexture(int index, boolean use64) { + if (index >= GLOW_BASE_START && index < GLOW_BASE_START + 4) { + return sheet("glow", "glow_sheet_", index - GLOW_BASE_START + 1, use64); + } + if (index >= GLOW_EMISSIVE_START && index < GLOW_EMISSIVE_START + 4) { + return sheet("glow", "glow_e_sheet_", index - GLOW_EMISSIVE_START + 1, use64); + } + if (index >= GLOW2_BASE_START && index < GLOW2_BASE_START + 4) { + return sheet("glow_2", "glow_2_sheet_", index - GLOW2_BASE_START + 1, use64); + } + if (index >= GLOW2_EMISSIVE_START && index < GLOW2_EMISSIVE_START + 4) { + return sheet("glow_2", "glow_2_e_sheet_", index - GLOW2_EMISSIVE_START + 1, use64); + } + if (index >= SGLOW_BASE_START && index < SGLOW_BASE_START + 4) { + return sheet("sglow", "sglow_sheet_", index - SGLOW_BASE_START + 1, use64); + } + if (index >= SGLOW_EMISSIVE_START && index < SGLOW_EMISSIVE_START + 4) { + return sheet("sglow", "sglow_e_sheet_", index - SGLOW_EMISSIVE_START + 1, use64); + } + return null; + } + + private static ResourceLocation sheet(String folder, String prefix, int number, boolean use64) { + String qualityPart = use64 ? "/64/" : "/"; + return texture(folder + qualityPart + prefix + number + ".png"); + } + + private static ResourceLocation texture(String path) { + return ResourceLocation.fromNamespaceAndPath("explosionoverhaul", "explosions/" + path); + } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionWindController.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionWindController.java index b4d2b36..66e307f 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionWindController.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ExplosionWindController.java @@ -1,53 +1,102 @@ package com.vinlanx.explosionoverhaul.client; +import com.vinlanx.explosionoverhaul.Config; +import java.util.Random; +import net.minecraft.client.Minecraft; import net.minecraft.world.phys.Vec3; public class ExplosionWindController { + private static final double BASE_SPEED = 0.05; + private static final double MAX_SPEED = 0.06; + private static final double LERP = 0.03; + private static final Random RANDOM = new Random(); + private static final double[] DUST_SPEED_PROFILE = new double[]{0.01, 0.03, 0.045, 0.06}; + private static final double[] GLOW_SPEED_PROFILE = new double[]{0.003, 0.01, 0.018, 0.025}; + public static Vec3 currentWind = Vec3.ZERO; + private static Vec3 targetDirection = Vec3.ZERO; + private static int ticksUntilDirectionShift; private ExplosionWindController() { } public static void tick() { + if (!Config.CLIENT.enableWindEffect.get()) { + currentWind = Vec3.ZERO; + targetDirection = Vec3.ZERO; + return; + } + if (--ticksUntilDirectionShift <= 0 || targetDirection.lengthSqr() < 1.0E-4) { + targetDirection = randomHorizontalDirection(); + ticksUntilDirectionShift = 80 + RANDOM.nextInt(120); + } + Vec3 targetWind = targetDirection.scale(BASE_SPEED * getWeatherMultiplier() * Config.CLIENT.windSpeedMultiplier.get()); + currentWind = currentWind.add(targetWind.subtract(currentWind).scale(LERP)); + if (currentWind.length() > MAX_SPEED) { + currentWind = currentWind.normalize().scale(MAX_SPEED); + } } public static void reset() { currentWind = Vec3.ZERO; + targetDirection = Vec3.ZERO; + ticksUntilDirectionShift = 0; } public static Vec3 getWind() { - return Vec3.ZERO; + return Config.CLIENT.enableWindEffect.get() ? currentWind : Vec3.ZERO; } public static Vec3 getScaledWind(double scale) { - return Vec3.ZERO; + return getWind().scale(scale); } public static Vec3 getWindDirection() { - return Vec3.ZERO; + Vec3 wind = getWind(); + if (wind.lengthSqr() < 1.0E-4) { + return Vec3.ZERO; + } + return new Vec3(wind.x, 0.0, wind.z).normalize(); } public static double computeDustSpeed(double heightPercent) { - return 0.0; + return sampleProfile(heightPercent, DUST_SPEED_PROFILE) * Config.CLIENT.windSpeedMultiplier.get() * getWeatherMultiplier(); } public static double computeGlowSpeed(double heightPercent) { - return 0.0; + return sampleProfile(heightPercent, GLOW_SPEED_PROFILE) * Config.CLIENT.windSpeedMultiplier.get() * getWeatherMultiplier(); } public static Vec3 getDustWindVector(double heightPercent) { - return Vec3.ZERO; + return getWindDirection().scale(computeDustSpeed(heightPercent)); } public static Vec3 getGlowWindVector(double heightPercent) { - return Vec3.ZERO; + return getWindDirection().scale(computeGlowSpeed(heightPercent)); } public static double sampleProfile(double heightPercent, double[] speeds) { - return speeds.length == 0 ? 0.0 : speeds[0]; + if (speeds.length == 0) { + return 0.0; + } + double clamped = Math.max(0.0, Math.min(1.0, heightPercent)); + double scaled = clamped * (speeds.length - 1); + int lower = (int)Math.floor(scaled); + int upper = Math.min(speeds.length - 1, lower + 1); + double fraction = scaled - lower; + return speeds[lower] + (speeds[upper] - speeds[lower]) * fraction; } public static double getWeatherMultiplier() { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level != null && (minecraft.level.isRaining() || minecraft.level.isThundering())) { + return 1.5; + } return 1.0; } + + private static Vec3 randomHorizontalDirection() { + double angle = RANDOM.nextDouble() * Math.PI * 2.0; + return new Vec3(Math.cos(angle), 0.0, Math.sin(angle)).normalize(); + } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/GroundDustEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/GroundDustEffect.java index 24bc0d0..6525d35 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/GroundDustEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/GroundDustEffect.java @@ -1,15 +1,189 @@ package com.vinlanx.explosionoverhaul.client; +import com.vinlanx.explosionoverhaul.Config; +import com.vinlanx.explosionoverhaul.SmokeParticleOptions; +import java.awt.Color; +import java.util.Random; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.MapColor; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; public class GroundDustEffect { + private final ClientLevel level; + private final Vec3 center; + private final float maxRadius; + private final int particlesPerTick; + private final int maxAge; + private final float particleBaseSize; + private final Random random = new Random(); + private final int raycastFrequency; + private Color currentDustColor = new Color(128, 128, 128); + private int age; + private boolean finished; + public GroundDustEffect(Vec3 position, float power) { + this.level = Minecraft.getInstance().level; + this.center = position; + float quality = Config.CLIENT.groundDustQuality.get().floatValue(); + this.raycastFrequency = Math.max(1, Config.CLIENT.groundDustRaycastFrequency.get()); + float powerFraction = Mth.clamp(power / 100.0f, 0.0f, 1.0f); + this.particlesPerTick = Math.max(1, (int)(Mth.lerp(powerFraction, 40.0f, 200.0f) * quality)); + this.maxAge = Math.max(1, (int)(Mth.lerp(powerFraction, 30.0f, 70.0f) * quality)); + this.maxRadius = power * this.calculateRadiusMultiplier(power); + this.particleBaseSize = Mth.lerp(powerFraction, 1.0f, 12.0f); + if (this.level == null || quality <= 0.0f) { + this.finished = true; + } } public void tick() { + if (this.finished || this.level == null) { + return; + } + if (!Config.CLIENT.enableGroundDustEffect.get()) { + this.finished = true; + return; + } + if (++this.age > this.maxAge) { + this.finished = true; + return; + } + + float progress = this.age / (float)this.maxAge; + float easedProgress = 1.0f - (float)Math.pow(1.0f - progress, 3.0); + float currentRadius = this.maxRadius * easedProgress; + if (this.age % 10 == 0) { + double checkAngle = this.random.nextDouble() * Math.PI * 2.0; + double checkX = this.center.x + Math.cos(checkAngle) * currentRadius; + double checkZ = this.center.z + Math.sin(checkAngle) * currentRadius; + BlockHitResult hit = this.level.clip(new ClipContext( + new Vec3(checkX, this.center.y + 15.0, checkZ), + new Vec3(checkX, this.center.y - 15.0, checkZ), + ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, + null)); + if (hit.getType() == HitResult.Type.BLOCK) { + Color color = getDustColorForState(this.level.getBlockState(hit.getBlockPos())); + if (color != null) { + this.currentDustColor = color; + } + } + } + + double cachedY = this.center.y; + for (int i = 0; i < this.particlesPerTick; ++i) { + double angle = i / (double)this.particlesPerTick * Math.PI * 2.0 + (this.random.nextDouble() - 0.5) * 0.1; + double radiusFrac = this.random.nextDouble(); + double spawnRadius = currentRadius * (0.6 + radiusFrac * 0.4); + double x = this.center.x + Math.cos(angle) * spawnRadius; + double z = this.center.z + Math.sin(angle) * spawnRadius; + + if (i % this.raycastFrequency == 0) { + BlockHitResult hit = this.level.clip(new ClipContext( + new Vec3(x, this.center.y + 15.0, z), + new Vec3(x, this.center.y - 15.0, z), + ClipContext.Block.COLLIDER, + ClipContext.Fluid.NONE, + null)); + if (hit.getType() != HitResult.Type.BLOCK) { + continue; + } + cachedY = hit.getLocation().y; + } + + double spawnY = cachedY; + Vec3 direction = new Vec3(x - this.center.x, 0.0, z - this.center.z).normalize(); + double motionStrength = Mth.lerp(1.0f - progress, 0.05f, 0.45f); + double motionX = direction.x * motionStrength + (this.random.nextDouble() - 0.5) * 0.05; + double motionZ = direction.z * motionStrength + (this.random.nextDouble() - 0.5) * 0.05; + double motionY = (this.random.nextDouble() - 0.5) * 0.02; + Vec3 windDirection = ExplosionWindController.getWindDirection(); + double windSpeed = ExplosionWindController.computeDustSpeed(0.0); + if (windDirection.lengthSqr() > 1.0E-4 && windSpeed > 0.0) { + motionX += windDirection.x * windSpeed * 0.65; + motionZ += windDirection.z * windSpeed * 0.65; + } + + float red; + float green; + float blue; + float alpha; + float scale; + if (radiusFrac <= 0.2) { + red = 0.2f; + green = 0.2f; + blue = 0.2f; + alpha = 0.9f; + scale = this.particleBaseSize * 0.5f; + } else if (radiusFrac <= 0.65) { + red = 0.4f; + green = 0.4f; + blue = 0.4f; + alpha = 0.6f; + scale = this.particleBaseSize * 0.75f; + } else { + red = this.currentDustColor.getRed() / 255.0f; + green = this.currentDustColor.getGreen() / 255.0f; + blue = this.currentDustColor.getBlue() / 255.0f; + alpha = 0.35f; + scale = this.particleBaseSize; + } + + SmokeParticleOptions options = new SmokeParticleOptions( + scale * (0.8f + this.random.nextFloat() * 0.4f), + 100 + this.random.nextInt(60), + red, + green, + blue, + alpha, + false, + (float)windSpeed, + 0.0f, + null); + this.level.addParticle(options, true, x, spawnY + 0.2, z, motionX, motionY, motionZ); + } } public boolean isFinished() { - return true; + return this.finished; + } + + private Color getDustColorForState(BlockState state) { + if (state.is(BlockTags.WOOL) || state.getMapColor((BlockGetter)this.level, BlockPos.ZERO) == MapColor.NONE) { + return null; + } + if (state.is(Blocks.GRASS_BLOCK)) { + return new Color(Blocks.DIRT.defaultBlockState().getMapColor((BlockGetter)this.level, BlockPos.ZERO).col); + } + if (state.is(Blocks.STONE) || state.is(Blocks.COBBLESTONE)) { + return new Color(Blocks.COBBLESTONE.defaultBlockState().getMapColor((BlockGetter)this.level, BlockPos.ZERO).col); + } + return new Color(state.getMapColor((BlockGetter)this.level, BlockPos.ZERO).col); + } + + private float calculateRadiusMultiplier(float power) { + if (power <= 5.0f) { + return 2.0f; + } + if (power <= 40.0f) { + return Mth.lerp((power - 5.0f) / 35.0f, 2.0f, 4.0f); + } + if (power <= 80.0f) { + return Mth.lerp((power - 40.0f) / 40.0f, 4.0f, 5.0f); + } + if (power <= 100.0f) { + return Mth.lerp((power - 80.0f) / 20.0f, 5.0f, 7.0f); + } + return 7.0f; } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/GroundMistEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/GroundMistEffect.java index e1d4841..9748f97 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/GroundMistEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/GroundMistEffect.java @@ -1,15 +1,141 @@ package com.vinlanx.explosionoverhaul.client; +import com.vinlanx.explosionoverhaul.Config; +import com.vinlanx.explosionoverhaul.SmokeParticleOptions; +import java.awt.Color; +import java.util.Random; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; public class GroundMistEffect { + private final ClientLevel level; + private final Vec3 center; + private final float maxRadius; + private final int particlesPerTick; + private final int maxAge; + private final float particleBaseSize; + private final Random random = new Random(); + private final int raycastFrequency; + private final Color currentMistColor = new Color(255, 255, 255); + private int age; + private boolean finished; + public GroundMistEffect(Vec3 position, float power) { + this.level = Minecraft.getInstance().level; + this.center = position; + float quality = Config.CLIENT.groundMistQuality.get().floatValue(); + this.raycastFrequency = Math.max(1, Config.CLIENT.groundMistRaycastFrequency.get()); + float powerFraction = Mth.clamp(power / 100.0f, 0.0f, 1.0f); + this.particlesPerTick = Math.max(1, (int)(Mth.lerp(powerFraction, 40.0f, 200.0f) * quality * 1.5f)); + this.maxAge = Math.max(1, (int)(Mth.lerp(powerFraction, 30.0f, 70.0f) * quality / 3.0f)); + this.maxRadius = power * this.calculateRadiusMultiplier(power) * 3.0f; + this.particleBaseSize = Mth.lerp(powerFraction, 1.0f, 12.0f); + if (this.level == null || quality <= 0.0f) { + this.finished = true; + } } public void tick() { + if (this.finished || this.level == null) { + return; + } + if (!Config.CLIENT.enableGroundMistEffect.get()) { + this.finished = true; + return; + } + if (++this.age > this.maxAge) { + this.finished = true; + return; + } + + float progress = this.age / (float)this.maxAge; + float easedProgress = 1.0f - (float)Math.pow(1.0f - progress, 3.0); + float currentRadius = this.maxRadius * easedProgress; + double cachedY = this.center.y; + for (int i = 0; i < this.particlesPerTick; ++i) { + double angle = i / (double)this.particlesPerTick * Math.PI * 2.0 + (this.random.nextDouble() - 0.5) * 0.1; + double radiusFrac = this.random.nextDouble(); + double spawnRadius = currentRadius * (0.6 + radiusFrac * 0.4); + double x = this.center.x + Math.cos(angle) * spawnRadius; + double z = this.center.z + Math.sin(angle) * spawnRadius; + + if (i % this.raycastFrequency == 0) { + Vec3 from = new Vec3(x, this.center.y + 15.0, z); + Vec3 to = new Vec3(x, this.center.y - 15.0, z); + BlockHitResult hit = this.level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, null)); + if (hit.getType() != HitResult.Type.BLOCK) { + continue; + } + cachedY = hit.getLocation().y; + } + + double spawnY = cachedY; + Vec3 direction = new Vec3(x - this.center.x, 0.0, z - this.center.z).normalize(); + double motionStrength = Mth.lerp(1.0f - progress, 0.02f, 0.15f); + double motionX = direction.x * motionStrength + (this.random.nextDouble() - 0.5) * 0.02; + double motionZ = direction.z * motionStrength + (this.random.nextDouble() - 0.5) * 0.02; + double motionY = -0.01 - this.random.nextDouble() * 0.02; + float red; + float green; + float blue; + float alpha; + float scale; + if (radiusFrac <= 0.2) { + red = 0.8f; + green = 0.8f; + blue = 0.8f; + alpha = 0.3f; + scale = this.particleBaseSize * 0.1667f; + } else if (radiusFrac <= 0.65) { + red = 0.9f; + green = 0.9f; + blue = 0.9f; + alpha = 0.2f; + scale = this.particleBaseSize * 0.25f; + } else { + red = this.currentMistColor.getRed() / 255.0f; + green = this.currentMistColor.getGreen() / 255.0f; + blue = this.currentMistColor.getBlue() / 255.0f; + alpha = 0.15f; + scale = this.particleBaseSize * 0.333f; + } + SmokeParticleOptions options = new SmokeParticleOptions( + scale * (0.8f + this.random.nextFloat() * 0.4f), + 100 + this.random.nextInt(60), + red, + green, + blue, + alpha, + false, + 0.0f, + 0.0f, + null); + this.level.addParticle(options, true, x, spawnY + 0.05, z, motionX, motionY, motionZ); + } } public boolean isFinished() { - return true; + return this.finished; + } + + private float calculateRadiusMultiplier(float power) { + if (power <= 5.0f) { + return 2.0f; + } + if (power <= 40.0f) { + return Mth.lerp((power - 5.0f) / 35.0f, 2.0f, 4.0f); + } + if (power <= 80.0f) { + return Mth.lerp((power - 40.0f) / 40.0f, 4.0f, 5.0f); + } + if (power <= 100.0f) { + return Mth.lerp((power - 80.0f) / 20.0f, 5.0f, 7.0f); + } + return 7.0f; } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java index 26671aa..ee3e8fa 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java @@ -2,164 +2,179 @@ package com.vinlanx.explosionoverhaul.client; import com.vinlanx.explosionoverhaul.Config; import com.vinlanx.explosionoverhaul.CustomGlowParticleOptions; -import com.vinlanx.explosionoverhaul.ModParticles; import com.vinlanx.explosionoverhaul.PlasmaParticleOptions; -import com.vinlanx.explosionoverhaul.SmokeParticleOptions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; public class PhysicsBasedExplosionEffect { + private static final int MAX_AGE = 8; + + private final ClientLevel level; + private final Random random; private final Vec3 position; private final float power; - private final int lifetime; - private final float stemHeight; - private final float capRadius; - private final float stemRadius; + private final float maxRadius; + private final int particleCount; + private final float particleScaleBase; + private final int totalSparks; + private final List sparkDirections; private int age; + private boolean finished; + private float sparkSpawnAccumulator; + private int sparkIndex; public PhysicsBasedExplosionEffect(Vec3 position, float power) { this.position = position; this.power = power; - this.lifetime = Mth.clamp(64 + Math.round(power * 4.2f), 78, 210); - this.stemHeight = Mth.clamp(3.6f + power * 0.42f, 4.5f, 28.0f); - this.capRadius = Mth.clamp(3.0f + power * 0.46f, 4.0f, 26.0f); - this.stemRadius = Mth.clamp(0.75f + power * 0.055f, 0.9f, 4.0f); - this.spawnInitialBurst(); + this.random = new Random(); + this.level = Minecraft.getInstance().level; + if (this.level == null) { + this.finished = true; + this.maxRadius = 0.0f; + this.particleCount = 0; + this.particleScaleBase = 0.0f; + this.totalSparks = 0; + this.sparkDirections = List.of(); + return; + } + + float calcPower = Mth.clamp(power, 1.0f, 100.0f); + float powerFraction = calcPower / 100.0f; + this.maxRadius = Mth.lerp(powerFraction, 3.0f, 40.0f); + this.particleCount = (int)Mth.lerp(powerFraction, 40.0f, 400.0f); + this.particleScaleBase = Mth.lerp(powerFraction, 5.0f, 37.5f); + this.totalSparks = (int)Mth.lerp(powerFraction, 3.0f, 150.0f); + this.sparkDirections = new ArrayList<>(); + + if (this.totalSparks > 0) { + double goldenRatio = (1.0 + Math.sqrt(5.0)) / 2.0; + double angleIncrement = Math.PI * 2.0 * goldenRatio; + for (int i = 0; i < this.totalSparks; ++i) { + double denominator = Math.max(1, this.totalSparks - 1); + double y = 1.0 - (double)i / denominator * 2.0; + double radius = Math.sqrt(Math.max(0.0, 1.0 - y * y)); + double theta = angleIncrement * i; + double x = Math.cos(theta) * radius; + double z = Math.sin(theta) * radius; + this.sparkDirections.add(new Vec3(x, z, y).normalize()); + } + Collections.shuffle(this.sparkDirections, this.random); + } } public void tick() { - ++this.age; - ClientLevel level = Minecraft.getInstance().level; - if (level == null) { + if (this.finished) { return; } - float progress = this.age / (float)this.lifetime; - if (progress < 0.62f) { - this.spawnRisingStem(level, progress); + if (!((Boolean)Config.CLIENT.enableExplosionParticles.get()).booleanValue()) { + this.finished = true; + return; } - if (progress > 0.06f && progress < 0.86f) { - this.spawnMushroomCap(level, progress); + ++this.age; + if (this.age > MAX_AGE) { + this.finished = true; + return; } - if (progress < 0.58f && ((Boolean)Config.CLIENT.enablePlasmaParticles.get()).booleanValue()) { - this.spawnPlasmaCore(level, progress); + if (Config.CLIENT.particleRenderMode.get() == Config.Client.ParticleRenderMode.REALISTIC_2) { + this.tickRealistic2(); + } else { + this.tickRegular(); } - if (progress > 0.34f && progress < 0.94f) { - this.spawnTrailingSmoke(level, progress); + } + + private void tickRealistic2() { + if (this.age == 1) { + float progress = this.age / (float)MAX_AGE; + float currentRadius = this.maxRadius * (float)Math.pow(progress, 0.4); + Vec3 particlePos = this.position.add((this.random.nextDouble() - 0.5) * currentRadius, (this.random.nextDouble() - 0.5) * currentRadius, (this.random.nextDouble() - 0.5) * currentRadius); + Vec3 motion = new Vec3((this.random.nextDouble() - 0.5) * 0.05, (this.random.nextDouble() - 0.5) * 0.05, (this.random.nextDouble() - 0.5) * 0.05); + double distance3D = particlePos.distanceTo(this.position); + double heightPercent = this.maxRadius <= 0.0f ? 0.5 : Mth.clamp(distance3D / this.maxRadius, 0.25, 1.0); + Vec3 windDirection = ExplosionWindController.getWindDirection(); + double glowSpeed = ExplosionWindController.computeGlowSpeed(heightPercent); + if (windDirection.lengthSqr() > 1.0E-4 && glowSpeed > 0.0) { + motion = motion.add(windDirection.scale(glowSpeed * 0.6)); + } + int zone = particlePos.distanceTo(this.position) / this.maxRadius < 0.4 ? 1 : 0; + float particleScale = this.particleScaleBase * ((Double)Config.CLIENT.particleSizeScale.get()).floatValue(); + int animationType = this.power <= 5.0f ? 2 : this.random.nextInt(2); + CustomGlowParticleOptions options = new CustomGlowParticleOptions(zone, this.power, particleScale, animationType, (float)this.position.y, this.maxRadius, (float)heightPercent); + this.level.addParticle(options, particlePos.x, particlePos.y, particlePos.z, motion.x, motion.y, motion.z); + } + this.spawnPlasmaSparks(this.maxRadius * (float)Math.pow(this.age / (float)MAX_AGE, 0.4)); + } + + private void tickRegular() { + int particlesThisTick = this.particleCount / MAX_AGE; + if (this.age == MAX_AGE) { + particlesThisTick = this.particleCount - particlesThisTick * (MAX_AGE - 1); + } + float progress = this.age / (float)MAX_AGE; + float currentRadius = this.maxRadius * (float)Math.pow(progress, 0.4); + for (int i = 0; i < particlesThisTick; ++i) { + double u = this.random.nextDouble(); + double v = this.random.nextDouble(); + double theta = Math.PI * 2.0 * u; + double phi = Math.acos(2.0 * v - 1.0); + double r = currentRadius; + Vec3 particlePos = this.position.add(r * Math.sin(phi) * Math.cos(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(phi)); + Vec3 motion = new Vec3((this.random.nextDouble() - 0.5) * 0.05, (this.random.nextDouble() - 0.5) * 0.05, (this.random.nextDouble() - 0.5) * 0.05); + double distance3D = particlePos.distanceTo(this.position); + double heightPercent = this.maxRadius <= 0.0f ? 0.5 : Mth.clamp(distance3D / this.maxRadius, 0.25, 1.0); + Vec3 windDirection = ExplosionWindController.getWindDirection(); + double glowSpeed = ExplosionWindController.computeGlowSpeed(heightPercent); + if (windDirection.lengthSqr() > 1.0E-4 && glowSpeed > 0.0) { + motion = motion.add(windDirection.scale(glowSpeed * 0.6)); + } + int zone = particlePos.distanceTo(this.position) / this.maxRadius < 0.4 ? 1 : 0; + float particleScale = this.particleScaleBase * (0.8f + this.random.nextFloat() * 0.4f) * ((Double)Config.CLIENT.particleSizeScale.get()).floatValue(); + int animationType = this.power <= 5.0f ? 2 : this.random.nextInt(2); + CustomGlowParticleOptions options = new CustomGlowParticleOptions(zone, this.power, particleScale, animationType, (float)this.position.y, this.maxRadius, (float)heightPercent); + this.level.addParticle(options, particlePos.x, particlePos.y, particlePos.z, motion.x, motion.y, motion.z); + } + this.spawnPlasmaSparks(currentRadius); + } + + private void spawnPlasmaSparks(float currentRadius) { + if (this.totalSparks <= 0 || this.sparkIndex >= this.totalSparks || !((Boolean)Config.CLIENT.enablePlasmaParticles.get()).booleanValue()) { + return; + } + this.sparkSpawnAccumulator += this.totalSparks / (float)MAX_AGE; + int sparksToSpawn = (int)this.sparkSpawnAccumulator; + if (sparksToSpawn <= 0) { + return; + } + this.sparkSpawnAccumulator -= sparksToSpawn; + for (int j = 0; j < sparksToSpawn && this.sparkIndex < this.totalSparks; ++j) { + Vec3 plasmaMotion = this.sparkDirections.get(this.sparkIndex++); + Vec3 plasmaSpawnPos = this.position.add(plasmaMotion.scale(currentRadius * 0.8)); + float strongSparkChance = 0.1f; + if (this.power > 10.0f) { + float powerFractionForChance = inverseLerp(this.power, 10.0f, 100.0f); + strongSparkChance = Mth.lerp(powerFractionForChance, 0.1f, 0.35f); + } + float force = this.random.nextFloat() < strongSparkChance + ? Mth.lerp(this.power / 100.0f, 2.0f, 14.0f) * (0.9f + this.random.nextFloat() * 0.4f) + : Mth.lerp(this.power / 100.0f, 1.3f, 6.5f) * (0.6f + this.random.nextFloat() * 0.4f); + plasmaMotion = plasmaMotion.scale(force); + this.level.addParticle(new PlasmaParticleOptions(this.power), true, plasmaSpawnPos.x, plasmaSpawnPos.y, plasmaSpawnPos.z, plasmaMotion.x, plasmaMotion.y, plasmaMotion.z); } } public boolean isFinished() { - return this.age >= this.lifetime; + return this.finished; } - private void spawnInitialBurst() { - ClientLevel level = Minecraft.getInstance().level; - if (level == null) { - return; + private static float inverseLerp(float value, float min, float max) { + if (max == min) { + return 0.0f; } - float modeScale = modeScale(); - int glowCount = Mth.clamp(Math.round(this.power * 9.0f * modeScale), 28, 220); - for (int i = 0; i < glowCount; ++i) { - Vec3 velocity = randomDirection(level).scale(0.08 + level.random.nextDouble() * 0.42); - level.addParticle(new CustomGlowParticleOptions(i % 2, this.power, 0.8f + level.random.nextFloat() * 0.9f, i % 3, (float)this.position.y, this.capRadius, 0.0f), this.position.x, this.position.y + 0.2, this.position.z, velocity.x, Math.abs(velocity.y) * 0.45 + 0.04, velocity.z); - } - int smokeCount = Mth.clamp(Math.round(this.power * 5.5f * modeScale), 26, 150); - for (int i = 0; i < smokeCount; ++i) { - Vec3 velocity = randomDirection(level).scale(0.03 + level.random.nextDouble() * 0.18); - float scale = 1.0f + level.random.nextFloat() * Math.max(1.8f, this.power * 0.22f); - level.addParticle(new SmokeParticleOptions(scale, 70 + level.random.nextInt(65), 0.22f, 0.19f, 0.15f, 0.64f, true, 0.0f, 0.12f, null), this.position.x, this.position.y + 0.15, this.position.z, velocity.x, Math.abs(velocity.y) * 0.22 + 0.03, velocity.z); - } - int sparks = Mth.clamp(Math.round(this.power * 4.2f * modeScale), 12, 120); - for (int i = 0; i < sparks; ++i) { - Vec3 velocity = randomDirection(level).scale(0.45 + level.random.nextDouble() * 1.35); - level.addParticle(ModParticles.LINE_SPARK.get(), this.position.x, this.position.y + 0.25, this.position.z, velocity.x, Math.abs(velocity.y) * 0.52, velocity.z); - } - } - - private void spawnRisingStem(ClientLevel level, float progress) { - float modeScale = modeScale(); - int count = Mth.clamp(Math.round(this.power * 0.28f * modeScale), 2, 10); - float lift = this.stemHeight * Mth.clamp(progress / 0.62f, 0.0f, 1.0f); - for (int i = 0; i < count; ++i) { - double angle = level.random.nextDouble() * Math.PI * 2.0; - double radius = this.stemRadius * Math.sqrt(level.random.nextDouble()); - double x = this.position.x + Math.cos(angle) * radius; - double z = this.position.z + Math.sin(angle) * radius; - double y = this.position.y + 0.4 + level.random.nextDouble() * Math.max(1.4, lift); - float fade = 1.0f - progress * 0.45f; - float scale = Mth.clamp(1.3f + this.power * 0.16f + level.random.nextFloat() * 1.4f, 1.2f, 7.2f); - level.addParticle(new SmokeParticleOptions(scale, 92 + level.random.nextInt(76), 0.17f, 0.15f, 0.13f, 0.58f * fade, true, 0.0f, progress, null), x, y, z, Math.cos(angle) * 0.025, 0.075 + level.random.nextDouble() * 0.065, Math.sin(angle) * 0.025); - } - } - - private void spawnMushroomCap(ClientLevel level, float progress) { - float capProgress = Mth.clamp((progress - 0.06f) / 0.58f, 0.0f, 1.0f); - float spread = this.capRadius * easeOutCubic(capProgress); - float capY = (float)this.position.y + this.stemHeight * (0.72f + capProgress * 0.28f); - int count = Mth.clamp(Math.round(this.power * 0.34f * modeScale()), 2, 14); - for (int i = 0; i < count; ++i) { - double angle = level.random.nextDouble() * Math.PI * 2.0; - double radius = spread * (0.35 + level.random.nextDouble() * 0.78); - double x = this.position.x + Math.cos(angle) * radius; - double z = this.position.z + Math.sin(angle) * radius; - double y = capY + (level.random.nextDouble() - 0.35) * Math.max(1.0, this.stemHeight * 0.18); - float alpha = Mth.clamp(0.64f * (1.0f - progress * 0.55f), 0.16f, 0.64f); - float scale = Mth.clamp(1.6f + this.power * 0.18f + level.random.nextFloat() * 2.0f, 1.4f, 8.4f); - Vec3 outward = new Vec3(x - this.position.x, 0.0, z - this.position.z); - if (outward.lengthSqr() < 0.0001) { - outward = new Vec3(Math.cos(angle), 0.0, Math.sin(angle)); - } - Vec3 velocity = outward.normalize().scale(0.035 + capProgress * 0.055).add(0.0, 0.02 + level.random.nextDouble() * 0.035, 0.0); - level.addParticle(new SmokeParticleOptions(scale, 100 + level.random.nextInt(92), 0.18f, 0.16f, 0.14f, alpha, true, 0.0f, capProgress, null), x, y, z, velocity.x, velocity.y, velocity.z); - } - } - - private void spawnPlasmaCore(ClientLevel level, float progress) { - int count = Mth.clamp(Math.round(this.power * 0.16f * modeScale()), 1, 8); - for (int i = 0; i < count; ++i) { - Vec3 velocity = randomDirection(level).scale(0.18 + level.random.nextDouble() * 0.78); - double y = this.position.y + 0.25 + level.random.nextDouble() * this.stemHeight * Mth.clamp(progress * 1.6f, 0.12f, 1.0f); - level.addParticle(new PlasmaParticleOptions(this.power), this.position.x, y, this.position.z, velocity.x, Math.abs(velocity.y) * 0.45, velocity.z); - } - } - - private void spawnTrailingSmoke(ClientLevel level, float progress) { - if (!((Boolean)Config.CLIENT.enableGroundMistEffect.get()).booleanValue()) { - return; - } - float fade = 1.0f - progress; - int count = Mth.clamp(Math.round(this.power * 0.14f * modeScale()), 1, 6); - for (int i = 0; i < count; ++i) { - double angle = level.random.nextDouble() * Math.PI * 2.0; - double radius = this.capRadius * (0.15 + level.random.nextDouble() * 0.95); - double x = this.position.x + Math.cos(angle) * radius; - double z = this.position.z + Math.sin(angle) * radius; - double y = this.position.y + 0.1 + level.random.nextDouble() * 1.2; - float scale = Mth.clamp(1.0f + this.power * 0.1f + level.random.nextFloat() * 1.7f, 1.0f, 5.8f); - level.addParticle(new SmokeParticleOptions(scale, 84 + level.random.nextInt(70), 0.26f, 0.25f, 0.23f, 0.28f * fade, false, 0.0f, 0.05f, null), x, y, z, Math.cos(angle) * 0.025, 0.006, Math.sin(angle) * 0.025); - } - } - - private static Vec3 randomDirection(ClientLevel level) { - double yaw = level.random.nextDouble() * Math.PI * 2.0; - double y = level.random.nextDouble() * 1.15 - 0.25; - double horizontal = Math.sqrt(Math.max(0.0, 1.0 - y * y)); - return new Vec3(Math.cos(yaw) * horizontal, y, Math.sin(yaw) * horizontal); - } - - private static float modeScale() { - return switch (Config.CLIENT.particleRenderMode.get()) { - case REALISTIC -> 1.0f; - case REALISTIC_2 -> 1.12f; - case VANILA -> 0.68f; - }; - } - - private static float easeOutCubic(float t) { - float clamped = Mth.clamp(t, 0.0f, 1.0f); - float inv = 1.0f - clamped; - return 1.0f - inv * inv * inv; + return Mth.clamp((value - min) / (max - min), 0.0f, 1.0f); } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ShockwaveEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ShockwaveEffect.java index 57af489..2150e7c 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/ShockwaveEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ShockwaveEffect.java @@ -1,60 +1,74 @@ package com.vinlanx.explosionoverhaul.client; import com.vinlanx.explosionoverhaul.Config; -import com.vinlanx.explosionoverhaul.ModParticles; -import com.vinlanx.explosionoverhaul.SmokeParticleOptions; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.particles.ParticleTypes; import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; public class ShockwaveEffect { private final Vec3 position; private final float power; - private final int lifetime; + private final int maxAge; private final float maxRadius; private int age; + private boolean finished; public ShockwaveEffect(Vec3 position, float power) { this.position = position; this.power = power; - this.lifetime = Mth.clamp(18 + Math.round(power * 0.9f), 24, 72); - this.maxRadius = Mth.clamp(8.0f + power * 2.9f, 10.0f, 96.0f); + float clampedPower = Mth.clamp(power, 5.0f, 100.0f); + float powerFraction = inverseLerp(clampedPower, 5.0f, 100.0f); + this.maxAge = (int)Mth.lerp(powerFraction, 4.0f, 7.0f); + float fireballRadius = Mth.lerp(clampedPower / 100.0f, 3.0f, 40.0f); + float normalizedPowerSqrt = inverseLerp((float)Math.sqrt(clampedPower), (float)Math.sqrt(5.0), (float)Math.sqrt(100.0)); + float shockwaveMultiplier = Mth.lerp(normalizedPowerSqrt, 2.0f, 8.0f); + this.maxRadius = fireballRadius * shockwaveMultiplier * 3.0f; } public void tick() { - ++this.age; - ClientLevel level = Minecraft.getInstance().level; - if (level == null || !((Boolean)Config.CLIENT.enableShockwaveEffect.get()).booleanValue()) { + if (this.finished) { return; } - float progress = this.age / (float)this.lifetime; - float radius = this.maxRadius * easeOutCubic(progress); - float fade = Mth.clamp(1.0f - progress, 0.0f, 1.0f); - int points = Mth.clamp(Math.round(8.0f + this.power * 1.15f), 12, 44); - for (int i = 0; i < points; ++i) { - double angle = (Math.PI * 2.0 * i / points) + level.random.nextDouble() * 0.22; - double x = this.position.x + Math.cos(angle) * radius; - double z = this.position.z + Math.sin(angle) * radius; - double y = this.position.y + 0.08 + level.random.nextDouble() * 0.35; - double outwardSpeed = 0.18 + Math.min(0.9, this.power * 0.025); - if (progress < 0.72f) { - level.addParticle(ModParticles.LINE_SPARK.get(), x, y + 0.16, z, Math.cos(angle) * outwardSpeed, 0.015 + level.random.nextDouble() * 0.04, Math.sin(angle) * outwardSpeed); - } - if (((Boolean)Config.CLIENT.enableGroundDustEffect.get()).booleanValue()) { - float scale = Mth.clamp(0.8f + this.power * 0.055f + level.random.nextFloat() * 0.8f, 0.8f, 4.2f); - level.addParticle(new SmokeParticleOptions(scale, 42 + level.random.nextInt(36), 0.28f, 0.25f, 0.21f, 0.34f * fade, true, 0.0f, 0.05f, null), x, y, z, Math.cos(angle) * 0.055, 0.012, Math.sin(angle) * 0.055); - } + if (!Config.CLIENT.enableShockwaveEffect.get()) { + this.finished = true; + return; + } + + ClientLevel level = Minecraft.getInstance().level; + if (level == null || ++this.age > this.maxAge) { + this.finished = true; + return; + } + + float powerFraction = inverseLerp(Mth.clamp(this.power, 5.0f, 100.0f), 5.0f, 100.0f); + int particlesThisTick = (int)Mth.lerp(powerFraction, 70.0f, 400.0f); + float progress = this.age / (float)this.maxAge; + float currentRadius = this.maxRadius * (float)Math.sin(progress * Math.PI / 2.0); + float shellThickness = Mth.lerp(powerFraction, 0.5f, 4.0f); + + for (int i = 0; i < particlesThisTick; ++i) { + double u = level.random.nextDouble(); + double v = level.random.nextDouble(); + double theta = Math.PI * 2.0 * u; + double phi = Math.acos(2.0 * v - 1.0); + double radius = currentRadius + (level.random.nextDouble() - 0.5) * shellThickness; + double x = this.position.x + radius * Math.sin(phi) * Math.cos(theta); + double y = this.position.y + radius * Math.sin(phi) * Math.sin(theta); + double z = this.position.z + radius * Math.cos(phi); + level.addParticle(ParticleTypes.CLOUD, x, y, z, 0.0, 0.0, 0.0); } } public boolean isFinished() { - return this.age >= this.lifetime; + return this.finished; } - private static float easeOutCubic(float t) { - float clamped = Mth.clamp(t, 0.0f, 1.0f); - float inv = 1.0f - clamped; - return 1.0f - inv * inv * inv; + private static float inverseLerp(float value, float min, float max) { + if (max == min) { + return 0.0f; + } + return Mth.clamp((value - min) / (max - min), 0.0f, 1.0f); } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java b/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java index 46c8be8..1283aeb 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java @@ -1,38 +1,44 @@ package com.vinlanx.explosionoverhaul.client; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.VertexConsumer; import com.vinlanx.explosionoverhaul.SmokeParticleOptions; +import net.minecraft.client.Camera; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.ParticleProvider; import net.minecraft.client.particle.ParticleRenderType; import net.minecraft.client.particle.SpriteSet; import net.minecraft.client.particle.TextureSheetParticle; -import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; public class SmokeParticle extends TextureSheetParticle { private final SpriteSet sprites; - private final float startSize; - private final float startAlpha; + private final float initialAlpha; + private final boolean heavy; + private final float windSpeed; + private final float heightPercent; protected SmokeParticle(ClientLevel level, double x, double y, double z, double xSpeed, double ySpeed, double zSpeed, SmokeParticleOptions options, SpriteSet sprites) { super(level, x, y, z, xSpeed, ySpeed, zSpeed); this.sprites = sprites; - this.lifetime = Math.max(6, options.getLifetime()); - float requestedSize = options.getScale() * (options.isHeavy() ? 1.25f : 1.0f); - this.startSize = Mth.clamp(requestedSize, 0.12f, options.isHeavy() ? 8.0f : 5.5f); - this.startAlpha = options.getAlpha(); - this.quadSize = this.startSize; + this.lifetime = Math.max(1, options.getLifetime()); + this.quadSize = options.getScale(); this.rCol = options.getRed(); this.gCol = options.getGreen(); this.bCol = options.getBlue(); - this.alpha = this.startAlpha; - this.friction = options.isHeavy() ? 0.94f : 0.9f; - this.gravity = options.isHeavy() ? -0.004f : -0.012f; + this.alpha = options.getAlpha(); + this.initialAlpha = this.alpha; + this.heavy = options.isHeavy(); + this.windSpeed = options.getWindSpeed(); + this.heightPercent = options.getHeightPercent(); + this.friction = 0.96f; + this.gravity = 0.0f; this.hasPhysics = false; - this.xd = xSpeed + options.getWindSpeed() * 0.02; - this.yd = ySpeed + 0.008 + options.getHeightPercent() * 0.01; + this.xd = xSpeed; + this.yd = ySpeed; this.zd = zSpeed; - this.pickSprite(sprites); + this.setSpriteFromAge(sprites); } @Override @@ -40,20 +46,50 @@ public class SmokeParticle extends TextureSheetParticle { return ParticleRenderType.PARTICLE_SHEET_TRANSLUCENT; } - public boolean shouldCull() { - return false; + @Override + public void render(VertexConsumer buffer, Camera renderInfo, float partialTicks) { + RenderSystem.depthMask(this.alpha > 0.3f); + super.render(buffer, renderInfo, partialTicks); + RenderSystem.depthMask(true); } @Override public void tick() { - super.tick(); - if (this.age >= this.lifetime) { + this.xo = this.x; + this.yo = this.y; + this.zo = this.z; + if (this.age++ >= this.lifetime) { + this.remove(); return; } - float progress = this.age / (float)this.lifetime; + + if (this.heavy) { + this.yd = -0.04; + } + + this.move(this.xd, this.yd, this.zd); + this.xd *= this.friction; + this.yd *= this.friction; + this.zd *= this.friction; + + Vec3 direction = ExplosionWindController.getWindDirection(); + if (this.windSpeed > 0.0f && direction.lengthSqr() > 1.0E-4) { + Vec3 target = direction.scale(this.windSpeed); + this.xd += (target.x - this.xd) * 0.12; + this.zd += (target.z - this.zd) * 0.12; + } else if (this.heightPercent > 0.0f) { + Vec3 wind = ExplosionWindController.getDustWindVector(this.heightPercent); + this.xd += (wind.x - this.xd) * 0.08; + this.zd += (wind.z - this.zd) * 0.08; + } + + float lifeProgress = this.age / (float)this.lifetime; + this.alpha = this.initialAlpha * (1.0f - lifeProgress); this.setSpriteFromAge(this.sprites); - this.quadSize = Math.min(this.startSize + 5.5f, this.startSize * (1.0f + progress * 1.25f)); - this.alpha = this.startAlpha * Mth.clamp(1.0f - progress, 0.0f, 1.0f); + } + + public boolean shouldCull() { + return false; } public static class Provider implements ParticleProvider {