From 77a55286b8b5db0d1660b63e8ceeb110488d188b Mon Sep 17 00:00:00 2001 From: MrSphay Date: Sat, 9 May 2026 16:46:14 +0200 Subject: [PATCH] Restore instant crater debris and glow sprites --- .../explosionoverhaul/AsyncCraterManager.java | 130 +----------------- .../explosionoverhaul/CraterDeformer.java | 117 ++++++++++++++-- .../client/CustomGlowParticle.java | 10 +- .../client/SmokeParticle.java | 5 +- .../particles/custom_glow.json | 4 +- .../textures/particle/soft_glow.png | Bin 0 -> 3778 bytes .../textures/particle/soft_glow_e.png | Bin 0 -> 3778 bytes 7 files changed, 123 insertions(+), 143 deletions(-) create mode 100644 src/main/resources/assets/explosionoverhaul/textures/particle/soft_glow.png create mode 100644 src/main/resources/assets/explosionoverhaul/textures/particle/soft_glow_e.png diff --git a/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java b/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java index 12679e9..a5ac49c 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java @@ -1,148 +1,20 @@ package com.vinlanx.explosionoverhaul; -import java.util.ArrayDeque; -import java.util.Iterator; -import java.util.Queue; -import net.minecraft.core.BlockPos; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; public class AsyncCraterManager { - private static final Queue JOBS = new ArrayDeque<>(); - private static final int MAX_SCAN_POSITIONS_PER_TICK = 4096; - private static final int MAX_BREAK_EVENTS_PER_TICK = 96; - private static final int MAX_DEBRIS_PER_TICK = 4; - private static final int MAX_DEBRIS_PER_EXPLOSION = 64; - public static void submit(ServerLevel level, Vec3 pos, float power) { - synchronized (JOBS) { - for (CraterJob job : JOBS) { - if (job.overlaps(level, pos, power)) { - return; - } - } - JOBS.add(new CraterJob(level, pos, power)); - } + CraterDeformer.applyInstantCrater(level, pos, power); } public static void onServerTick(MinecraftServer server) { - int configuredBudget = Math.max(1, (Integer)Config.COMMON.craterApplyBlocksPerTick.get()); - int budget = Math.max(256, Math.min(configuredBudget, MAX_SCAN_POSITIONS_PER_TICK)); - synchronized (JOBS) { - Iterator iterator = JOBS.iterator(); - while (iterator.hasNext() && budget > 0) { - CraterJob job = iterator.next(); - budget = job.apply(budget); - if (job.done()) { - iterator.remove(); - } - } - } } public static void onLevelUnload(ServerLevel level) { - synchronized (JOBS) { - JOBS.removeIf(job -> job.level == level); - } } public static void shutdown() { - synchronized (JOBS) { - JOBS.clear(); - } - } - - private static final class CraterJob { - private final ServerLevel level; - private final Vec3 origin; - private final float power; - private final float radius; - private final int minX; - private final int maxX; - private final int minY; - private final int maxY; - private final int minZ; - private final int maxZ; - private final int maxDebris; - private int x; - private int y; - private int z; - private int debrisSpawned; - - private CraterJob(ServerLevel level, Vec3 origin, float power) { - this.level = level; - this.origin = origin; - this.power = power; - this.radius = CraterDeformer.calculateRadius(power); - int scanRadius = CraterDeformer.calculateScanRadius(power); - BlockPos center = BlockPos.containing(origin); - this.minX = center.getX() - scanRadius; - this.maxX = center.getX() + scanRadius; - this.minY = Math.max(level.getMinBuildHeight(), center.getY() - scanRadius); - this.maxY = Math.min(level.getMaxBuildHeight() - 1, center.getY() + scanRadius); - this.minZ = center.getZ() - scanRadius; - this.maxZ = center.getZ() + scanRadius; - this.x = this.minX; - this.y = this.minY; - this.z = this.minZ; - this.maxDebris = Math.min(MAX_DEBRIS_PER_EXPLOSION, Math.max(4, Math.round(power * 2.5f))); - } - - private int apply(int budget) { - int breakEvents = 0; - int debrisThisTick = 0; - int configuredDebris = Math.max(0, (Integer)Config.COMMON.craterMaxFallingBlocksPerTick.get()); - int debrisBudget = Math.min(configuredDebris, MAX_DEBRIS_PER_TICK); - while (!this.done() && budget > 0) { - BlockPos pos = new BlockPos(this.x, this.y, this.z); - this.advance(); - --budget; - if (!CraterDeformer.shouldDestroyCraterBlock(this.level, pos, this.origin, this.power, this.radius)) { - continue; - } - BlockState state = this.level.getBlockState(pos); - if (!state.isAir() && state.getDestroySpeed(this.level, pos) >= 0.0f && !ExplosionOverhaul.isBlockStateBlacklisted(state)) { - if (this.debrisSpawned < this.maxDebris && debrisThisTick < debrisBudget && this.level.random.nextFloat() < 0.006f) { - CraterDeformer.spawnDebris(this.level, this.origin, this.power, 1); - ++this.debrisSpawned; - ++debrisThisTick; - } - if (breakEvents < MAX_BREAK_EVENTS_PER_TICK && this.level.random.nextFloat() < 0.035f) { - this.level.levelEvent(2001, pos, Block.getId(state)); - ++breakEvents; - } - this.level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); - } - } - return budget; - } - - private boolean overlaps(ServerLevel level, Vec3 pos, float power) { - if (this.level != level || this.done()) { - return false; - } - double threshold = Math.max(3.0, Math.min(this.radius, CraterDeformer.calculateRadius(power)) * 0.45); - return this.origin.distanceToSqr(pos) <= threshold * threshold; - } - - private void advance() { - if (++this.x <= this.maxX) { - return; - } - this.x = this.minX; - if (++this.y <= this.maxY) { - return; - } - this.y = this.minY; - ++this.z; - } - - private boolean done() { - return this.z > this.maxZ; - } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java b/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java index b0ca199..9f825c9 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java @@ -1,15 +1,22 @@ package com.vinlanx.explosionoverhaul; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.Mth; import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; public class CraterDeformer { + private static final int MAX_BREAK_EVENTS_PER_EXPLOSION = 128; + private static final int MAX_DEBRIS_PER_EXPLOSION = 96; + public static Set getCraterBlocks(ServerLevel level, Vec3 explosionPos, float power) { Set blocks = new LinkedHashSet<>(); float radius = calculateRadius(power); @@ -64,6 +71,29 @@ public class CraterDeformer { return Math.max(8.0f, power * 6.0f); } + public static void applyInstantCrater(ServerLevel level, Vec3 explosionPos, float power) { + Set craterBlocks = getCraterBlocks(level, explosionPos, power); + int debrisLimit = resolveDebrisLimit(power, defaultDebrisLimit(power)); + List debris = sampleDebrisCandidates(level, explosionPos, craterBlocks, debrisLimit); + for (DebrisCandidate candidate : debris) { + launchDebris(level, explosionPos, power, candidate); + } + + int breakEvents = 0; + int maxBreakEvents = Math.min(MAX_BREAK_EVENTS_PER_EXPLOSION, Math.max(24, Math.round(power * 5.0f))); + for (BlockPos pos : craterBlocks) { + BlockState state = level.getBlockState(pos); + if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f || ExplosionOverhaul.isBlockStateBlacklisted(state)) { + continue; + } + if (breakEvents < maxBreakEvents && level.random.nextFloat() < 0.035f) { + level.levelEvent(2001, pos, Block.getId(state)); + ++breakEvents; + } + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + } + } + public static void applyLargeExplosionLogic(ServerLevel level, Vec3 explosionPos, float power) { spawnDebris(level, explosionPos, power, Math.min(72, Math.round(power * 3.0f))); } @@ -73,29 +103,97 @@ public class CraterDeformer { } public static void spawnDebris(ServerLevel level, Vec3 explosionPos, float power, int maxDebris) { - if (!((Boolean)Config.COMMON.enableFallingBlocks.get()).booleanValue() || maxDebris <= 0) { + int debrisLimit = resolveDebrisLimit(power, maxDebris); + if (debrisLimit <= 0) { return; } float radius = calculateRadius(power); BlockPos origin = BlockPos.containing(explosionPos); int spawned = 0; - int attempts = maxDebris * 4; - for (int i = 0; i < attempts && spawned < maxDebris; ++i) { + int attempts = debrisLimit * 5; + for (int i = 0; i < attempts && spawned < debrisLimit; ++i) { double angle = level.random.nextDouble() * Math.PI * 2.0; double distance = radius * (0.25 + level.random.nextDouble() * 0.7); BlockPos pos = origin.offset(Mth.floor(Math.cos(angle) * distance), level.random.nextInt(5) - 1, Mth.floor(Math.sin(angle) * distance)); BlockState state = level.getBlockState(pos); - if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f || ExplosionOverhaul.isBlockStateBlacklisted(state)) { + if (!canLaunchDebris(level, pos, state)) { continue; } - FallingBlockEntity falling = FallingBlockEntity.fall(level, pos, state); - Vec3 motion = new Vec3(pos.getX() + 0.5 - explosionPos.x(), 0.3 + level.random.nextDouble() * 0.9, pos.getZ() + 0.5 - explosionPos.z()).normalize().scale(0.18 + Math.min(1.4, power * 0.035)); - falling.setDeltaMovement(motion); - falling.time = 1; + launchDebris(level, explosionPos, power, new DebrisCandidate(pos.immutable(), state)); ++spawned; } } + private static List sampleDebrisCandidates(ServerLevel level, Vec3 explosionPos, Set craterBlocks, int maxDebris) { + List candidates = new ArrayList<>(Math.max(0, maxDebris)); + if (maxDebris <= 0) { + return candidates; + } + int seen = 0; + for (BlockPos pos : craterBlocks) { + BlockState state = level.getBlockState(pos); + if (!canLaunchDebris(level, pos, state)) { + continue; + } + double distance = Math.sqrt(pos.distToCenterSqr(explosionPos)); + if (distance < 1.5 || level.random.nextFloat() < 0.22f) { + continue; + } + DebrisCandidate candidate = new DebrisCandidate(pos.immutable(), state); + ++seen; + if (candidates.size() < maxDebris) { + candidates.add(candidate); + } else { + int replacement = level.random.nextInt(seen); + if (replacement < maxDebris) { + candidates.set(replacement, candidate); + } + } + } + return candidates; + } + + private static boolean canLaunchDebris(ServerLevel level, BlockPos pos, BlockState state) { + return !state.isAir() + && state.getDestroySpeed(level, pos) >= 0.0f + && state.getFluidState().isEmpty() + && !ExplosionOverhaul.isBlockStateBlacklisted(state); + } + + private static void launchDebris(ServerLevel level, Vec3 explosionPos, float power, DebrisCandidate candidate) { + BlockState state = level.getBlockState(candidate.pos()); + if (state.isAir() || !state.is(candidate.state().getBlock())) { + return; + } + FallingBlockEntity falling = FallingBlockEntity.fall(level, candidate.pos(), candidate.state()); + Vec3 offset = Vec3.atCenterOf(candidate.pos()).subtract(explosionPos); + Vec3 horizontal = new Vec3(offset.x, 0.0, offset.z); + if (horizontal.lengthSqr() < 0.0001) { + double angle = level.random.nextDouble() * Math.PI * 2.0; + horizontal = new Vec3(Math.cos(angle), 0.0, Math.sin(angle)); + } + double horizontalSpeed = 0.22 + Math.min(1.45, power * 0.045) + level.random.nextDouble() * 0.45; + double verticalSpeed = 0.32 + Math.min(0.75, power * 0.02) + level.random.nextDouble() * 0.65; + falling.setDeltaMovement(horizontal.normalize().scale(horizontalSpeed).add(0.0, verticalSpeed, 0.0)); + falling.time = 1; + } + + private static int defaultDebrisLimit(float power) { + return power >= 12.0f ? Math.min(72, Math.round(power * 3.0f)) : Math.min(24, Math.round(power * 3.0f)); + } + + private static int resolveDebrisLimit(float power, int requestedLimit) { + if (!((Boolean)Config.COMMON.enableFallingBlocks.get()).booleanValue() || requestedLimit <= 0) { + return 0; + } + int configuredLimit = Math.max(0, (Integer)Config.COMMON.craterMaxFallingBlocksPerTick.get()); + if (configuredLimit <= 0) { + return 0; + } + int visibleLimit = Math.max(requestedLimit, Math.round(power * 4.0f)); + return Math.min(MAX_DEBRIS_PER_EXPLOSION, Math.max(configuredLimit, visibleLimit)); + } + private static double roughness(BlockPos pos, Vec3 origin, float power) { long seed = pos.asLong() ^ Double.doubleToLongBits(origin.x()) ^ (Double.doubleToLongBits(origin.z()) << 1) ^ (long)(power * 1000.0f); seed ^= seed >>> 33; @@ -105,4 +203,7 @@ public class CraterDeformer { seed ^= seed >>> 33; return (seed & 0xFFFF) / 65535.0; } + + private record DebrisCandidate(BlockPos pos, BlockState state) { + } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java b/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java index 4307007..e8690c4 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java @@ -18,7 +18,13 @@ public class CustomGlowParticle extends TextureSheetParticle { 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); - this.baseSize = options.getScale() * configScale * powerScale * (0.85f + this.random.nextFloat() * 0.35f); + 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); @@ -49,7 +55,7 @@ public class CustomGlowParticle extends TextureSheetParticle { } 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.8f + progress * 1.8f); + this.quadSize = this.baseSize * (0.85f + progress * 1.15f); } public boolean shouldCull() { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java b/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java index 23b5784..46c8be8 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/SmokeParticle.java @@ -18,7 +18,8 @@ public class SmokeParticle extends TextureSheetParticle { super(level, x, y, z, xSpeed, ySpeed, zSpeed); this.sprites = sprites; this.lifetime = Math.max(6, options.getLifetime()); - this.startSize = options.getScale() * (options.isHeavy() ? 1.45f : 1.0f); + 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.rCol = options.getRed(); @@ -51,7 +52,7 @@ public class SmokeParticle extends TextureSheetParticle { } float progress = this.age / (float)this.lifetime; this.setSpriteFromAge(this.sprites); - this.quadSize = this.startSize * (1.0f + progress * 1.6f); + 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); } diff --git a/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json b/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json index 4d367e3..abe91f7 100644 --- a/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json +++ b/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json @@ -1,6 +1,6 @@ { "textures": [ - "explosionoverhaul:plasma", - "explosionoverhaul:plasma_e" + "explosionoverhaul:soft_glow", + "explosionoverhaul:soft_glow_e" ] } diff --git a/src/main/resources/assets/explosionoverhaul/textures/particle/soft_glow.png b/src/main/resources/assets/explosionoverhaul/textures/particle/soft_glow.png new file mode 100644 index 0000000000000000000000000000000000000000..590e934d66598e4b36de6dcd5348121c8805b241 GIT binary patch literal 3778 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 s+-z~7IUdo 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 s+-