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 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; } }