From 66bdfab2d6abc8a25e4e34fb8b764dd7768227b7 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Sat, 9 May 2026 14:18:13 +0200 Subject: [PATCH] Restore explosion parity systems --- gradle.properties | 2 +- .../AmbientExplosionManager.java | 134 +++++++++- .../explosionoverhaul/AsyncCraterManager.java | 70 +++++ .../explosionoverhaul/BlockIndexManager.java | 154 ++++++++++- .../explosionoverhaul/ClientSoundHandler.java | 7 +- .../explosionoverhaul/CommonEvents.java | 49 ++++ .../explosionoverhaul/CraterDeformer.java | 76 +++++- .../explosionoverhaul/ExplosionOverhaul.java | 104 ++++++++ .../explosionoverhaul/PacketHandler.java | 7 +- .../RedstoneLampEffects.java | 94 +++++-- .../explosionoverhaul/ScanControlPacket.java | 2 +- .../ScanLoadControlPacket.java | 2 +- .../ServerExplosionHandler.java | 251 +++++++++++++++--- .../client/CameraShakeConcussionEffect.java | 26 ++ .../client/ClientEffectEvents.java | 29 ++ .../client/ClientEffects.java | 197 +++++++++++++- .../client/ConcussionAudioEffect.java | 89 ++++++- .../client/CustomGlowParticle.java | 1 - .../client/DeafnessConcussionEffect.java | 28 +- .../client/FirstTimeScreen.java | 12 +- .../client/GuideSlidesScreen.java | 12 +- .../client/LowPassConcussionEffect.java | 42 ++- .../client/PhysicsBasedExplosionEffect.java | 74 +++++- .../compat/network/NetworkEvent.java | 12 +- .../compat/network/simple/SimpleChannel.java | 101 +++++++ .../blockstates/vinlanx_the_light.json | 5 +- .../particles/custom_glow.json | 50 +--- 27 files changed, 1466 insertions(+), 164 deletions(-) create mode 100644 src/main/java/com/vinlanx/explosionoverhaul/CommonEvents.java create mode 100644 src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffectEvents.java diff --git a/gradle.properties b/gradle.properties index 0e83cd2..448e264 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ minecraft_version=1.21.1 # as they do not follow standard versioning conventions. minecraft_version_range=[1.21.1] # The Neo version must agree with the Minecraft version to get a valid artifact -neo_version=21.1.225 +neo_version=21.1.228 # The loader version range can only use the major version of FML as bounds loader_version_range=[1,) diff --git a/src/main/java/com/vinlanx/explosionoverhaul/AmbientExplosionManager.java b/src/main/java/com/vinlanx/explosionoverhaul/AmbientExplosionManager.java index 8cfa233..2d1e3d9 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/AmbientExplosionManager.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/AmbientExplosionManager.java @@ -1,20 +1,152 @@ package com.vinlanx.explosionoverhaul; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; public class AmbientExplosionManager { + private static final Map PLAYER_TIMERS = new HashMap<>(); + private static final Map PENDING_SHOTS = new HashMap<>(); + public static void onServerTick(MinecraftServer server) { + if (!((Boolean)Config.COMMON.ambient.enableAmbientExplosions.get()).booleanValue()) { + PLAYER_TIMERS.clear(); + PENDING_SHOTS.clear(); + return; + } + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + PLAYER_TIMERS.computeIfAbsent(player, AmbientExplosionManager::nextDelay); + } + Iterator> timers = PLAYER_TIMERS.entrySet().iterator(); + while (timers.hasNext()) { + Map.Entry entry = timers.next(); + ServerPlayer player = entry.getKey(); + if (player.isRemoved()) { + timers.remove(); + continue; + } + int ticks = entry.getValue() - 1; + if (ticks > 0) { + entry.setValue(ticks); + continue; + } + playAmbient(player); + entry.setValue(nextDelay(player)); + } + Iterator> shots = PENDING_SHOTS.entrySet().iterator(); + while (shots.hasNext()) { + Map.Entry entry = shots.next(); + if (entry.getValue().tick(entry.getKey())) { + shots.remove(); + } + } } public static void onPlayerLoggedIn(ServerPlayer player) { + PLAYER_TIMERS.put(player, nextDelay(player)); } public static void onPlayerLoggedOut(ServerPlayer player) { + PLAYER_TIMERS.remove(player); + PENDING_SHOTS.remove(player); } public static ServerExplosionHandler.CameraShakeProfile determineCameraShakeProfile(float power, double distance, boolean playerInCave, ServerLevel level) { - return new ServerExplosionHandler.CameraShakeProfile(0.0f, 0, 0.0f); + return ServerExplosionHandler.determineCameraShakeProfile(power, distance, playerInCave, level); + } + + private static void playAmbient(ServerPlayer player) { + ServerLevel level = player.serverLevel(); + int minDistance = (Integer)Config.COMMON.ambient.minExplosionDistance.get(); + int maxDistance = (Integer)Config.COMMON.ambient.maxExplosionDistance.get(); + int distance = Mth.randomBetweenInclusive(level.random, minDistance, maxDistance); + double angle = level.random.nextDouble() * Math.PI * 2.0; + Vec3 pos = player.position().add(Math.cos(angle) * distance, level.random.nextInt(80) - 20, Math.sin(angle) * distance); + float power = weightedPower(level); + boolean cave = !level.canSeeSky(player.blockPosition()) && ((Boolean)Config.COMMON.ambient.soundTypes.enableCaveSounds.get()).booleanValue(); + ResourceLocation sound = ServerExplosionHandler.soundFor(power, distance, cave, cave, false); + PacketHandler.sendToPlayer(player, new PlayTrackedSoundPacket(pos, sound, 0.9f, 0.82f + level.random.nextFloat() * 0.18f, 0L, false)); + if (cave && ((Boolean)Config.COMMON.ambient.soundTypes.enableAmbientCaveDust.get()).booleanValue()) { + PacketHandler.sendToPlayer(player, new SpawnAmbientCaveDustPacket(power)); + } + int chainWeight = (Integer)Config.COMMON.ambient.scenarios.chainReactionWeight.get(); + int shellWeight = (Integer)Config.COMMON.ambient.scenarios.shellingWeight.get(); + int roll = level.random.nextInt(Math.max(1, chainWeight + shellWeight + (Integer)Config.COMMON.ambient.scenarios.singleExplosionWeight.get())); + if (roll < chainWeight) { + int shots = Mth.randomBetweenInclusive(level.random, (Integer)Config.COMMON.ambient.scenarios.minChainReactionShots.get(), (Integer)Config.COMMON.ambient.scenarios.maxChainReactionShots.get()); + PENDING_SHOTS.put(player, new AmbientShot(pos, power, shots, 12, true)); + } else if (roll < chainWeight + shellWeight) { + int delay = Mth.randomBetweenInclusive(level.random, (Integer)Config.COMMON.ambient.scenarios.minShellingDelay.get(), (Integer)Config.COMMON.ambient.scenarios.maxShellingDelay.get()); + PENDING_SHOTS.put(player, new AmbientShot(pos, power * 1.25f, 1, delay, false)); + } + } + + private static int nextDelay(ServerPlayer player) { + int min = (Integer)Config.COMMON.ambient.minTimeBetweenExplosions.get(); + int max = (Integer)Config.COMMON.ambient.maxTimeBetweenExplosions.get(); + return Mth.randomBetweenInclusive(player.serverLevel().random, Math.min(min, max), Math.max(min, max)); + } + + private static float weightedPower(ServerLevel level) { + int[] weights = { + (Integer)Config.COMMON.ambient.powerTiers.tier1_weight.get(), + (Integer)Config.COMMON.ambient.powerTiers.tier2_weight.get(), + (Integer)Config.COMMON.ambient.powerTiers.tier3_weight.get(), + (Integer)Config.COMMON.ambient.powerTiers.tier4_weight.get(), + (Integer)Config.COMMON.ambient.powerTiers.tier5_weight.get() + }; + int total = 0; + for (int weight : weights) { + total += Math.max(0, weight); + } + int roll = level.random.nextInt(Math.max(1, total)); + int tier = 0; + for (; tier < weights.length - 1; ++tier) { + roll -= Math.max(0, weights[tier]); + if (roll < 0) { + break; + } + } + return switch (tier) { + case 0 -> Mth.randomBetween(level.random, 1.0f, 4.0f); + case 1 -> Mth.randomBetween(level.random, 5.0f, 15.0f); + case 2 -> Mth.randomBetween(level.random, 16.0f, 40.0f); + case 3 -> Mth.randomBetween(level.random, 41.0f, 80.0f); + default -> Mth.randomBetween(level.random, 81.0f, ((Double)Config.COMMON.ambient.maxAmbientExplosionPower.get()).floatValue()); + }; + } + + private static final class AmbientShot { + private final Vec3 pos; + private float power; + private int remaining; + private int delay; + private final boolean chain; + + private AmbientShot(Vec3 pos, float power, int remaining, int delay, boolean chain) { + this.pos = pos; + this.power = power; + this.remaining = remaining; + this.delay = delay; + this.chain = chain; + } + + private boolean tick(ServerPlayer player) { + if (this.delay-- > 0) { + return false; + } + double distance = player.position().distanceTo(this.pos); + PacketHandler.sendToPlayer(player, new PlayTrackedSoundPacket(this.pos, ServerExplosionHandler.soundFor(this.power, distance, false, false, false), 0.8f, 0.85f, 0L, false)); + this.power *= this.chain ? 1.12f : 1.0f; + --this.remaining; + this.delay = Mth.randomBetweenInclusive(player.serverLevel().random, (Integer)Config.COMMON.ambient.scenarios.minTimeBetweenChainShots.get(), (Integer)Config.COMMON.ambient.scenarios.maxTimeBetweenChainShots.get()); + return this.remaining <= 0; + } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java b/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java index 5f04f88..1a1474f 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/AsyncCraterManager.java @@ -1,19 +1,89 @@ package com.vinlanx.explosionoverhaul; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +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<>(); + public static void submit(ServerLevel level, Vec3 pos, float power) { + List blocks = new ArrayList<>(CraterDeformer.getCraterBlocks(level, pos, power)); + synchronized (JOBS) { + JOBS.add(new CraterJob(level, pos, power, blocks)); + } } public static void onServerTick(MinecraftServer server) { + int budget = Math.max(1, (Integer)Config.COMMON.craterApplyBlocksPerTick.get()); + 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 List blocks; + private int index; + private int debrisSpawned; + + private CraterJob(ServerLevel level, Vec3 origin, float power, List blocks) { + this.level = level; + this.origin = origin; + this.power = power; + this.blocks = blocks; + } + + private int apply(int budget) { + int debrisBudget = Math.max(0, (Integer)Config.COMMON.craterMaxFallingBlocksPerTick.get()); + while (this.index < this.blocks.size() && budget > 0) { + BlockPos pos = this.blocks.get(this.index++); + BlockState state = this.level.getBlockState(pos); + if (!state.isAir() && state.getDestroySpeed(this.level, pos) >= 0.0f && !ExplosionOverhaul.isBlockStateBlacklisted(state)) { + if (this.debrisSpawned < debrisBudget && this.level.random.nextFloat() < 0.035f) { + CraterDeformer.spawnDebris(this.level, this.origin, this.power, 1); + ++this.debrisSpawned; + } + this.level.levelEvent(2001, pos, Block.getId(state)); + this.level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + } + --budget; + } + return budget; + } + + private boolean done() { + return this.index >= this.blocks.size(); + } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/BlockIndexManager.java b/src/main/java/com/vinlanx/explosionoverhaul/BlockIndexManager.java index 438d34d..0ff37fc 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/BlockIndexManager.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/BlockIndexManager.java @@ -1,70 +1,114 @@ package com.vinlanx.explosionoverhaul; +import java.util.ArrayList; import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RedstoneLampBlock; import net.minecraft.world.level.block.state.BlockState; public class BlockIndexManager { public static volatile boolean ENABLED = false; + private static final Map, EnumMap>> INDEX = new ConcurrentHashMap<>(); + private static volatile int totalChunksToScan; + private static volatile int chunksScanned; + private static volatile boolean scanning; + private static volatile boolean rescanMode; public static int getTotalChunksToScan() { - return 0; + return totalChunksToScan; } public static int getChunksScanned() { - return 0; + return chunksScanned; } public static float getScanProgress() { - return 0.0f; + return totalChunksToScan <= 0 ? (scanning ? 0.0f : 1.0f) : Math.min(1.0f, chunksScanned / (float)totalChunksToScan); } public static boolean isScanningComplete() { - return true; + return !scanning; } public static int getLampsFound() { - return 0; + return count(BlockType.LAMP); } public static int getDripstonesFound() { - return 0; + return count(BlockType.DRIPSTONE); } public static int getGlassBlocksFound() { - return 0; + return count(BlockType.GLASS); } public static void startManualScan() { + rescanMode = true; + scanning = true; } public static void loadExistingData() { + scanning = false; } public static void startNewScan() { + resetScanState(); + scanning = true; } public static void cancelManualScan() { + scanning = false; } public static void resetScanState() { + INDEX.clear(); + totalChunksToScan = 0; + chunksScanned = 0; + scanning = false; + rescanMode = false; } public static void forceServerScan(MinecraftServer server) { + if (server == null) { + return; + } + startNewScan(); + for (ServerLevel level : server.getAllLevels()) { + scanLoadedLevel(level); + } + scanning = false; } public static void forceSingleplayerScan(ServerLevel level) { + startNewScan(); + scanLoadedLevel(level); + scanning = false; } public static void showRescanPrompt(ServerPlayer player) { + ENABLED = ((Boolean)Config.COMMON.scan.enableBlockIndexing.get()).booleanValue(); + if (ENABLED && player.hasPermissions(2) && hasSaveFile(player.getServer())) { + PacketHandler.sendToPlayer(player, new ScanLoadPromptPacket(true)); + } } public static boolean isReinforcedGlass(BlockState state) { - return false; + ResourceLocation id = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + return id != null && getReinforcedGlassBlacklist().contains(id.toString()); } public static List getReinforcedGlassBlacklist() { @@ -90,21 +134,109 @@ public class BlockIndexManager { } public static boolean isRescanMode() { - return false; + return rescanMode; } public static void register(ServerLevel level, BlockPos pos, BlockType type) { + bucket(level, type).add(pos.immutable()); } public static void unregister(ServerLevel level, BlockPos pos, BlockType type) { + bucket(level, type).remove(pos); } public static List getNearby(ServerLevel level, BlockPos center, int radius, BlockType type) { - return Collections.emptyList(); + ENABLED = ((Boolean)Config.COMMON.scan.enableBlockIndexing.get()).booleanValue(); + if (!ENABLED) { + return scanNearby(level, center, radius, type); + } + List result = new ArrayList<>(); + int radiusSq = radius * radius; + for (BlockPos pos : bucket(level, type)) { + if (pos.distSqr(center) <= radiusSq && matches(level.getBlockState(pos), type)) { + result.add(pos); + } + } + if (result.isEmpty()) { + result.addAll(scanNearby(level, center, radius, type)); + } + return result; } public static boolean isPlayerAuthorized(ServerPlayer player) { - return true; + return player != null && player.hasPermissions(2); + } + + private static void scanLoadedLevel(ServerLevel level) { + ENABLED = ((Boolean)Config.COMMON.scan.enableBlockIndexing.get()).booleanValue(); + EnumMap> map = new EnumMap<>(BlockType.class); + for (BlockType type : BlockType.values()) { + map.put(type, ConcurrentHashMap.newKeySet()); + } + INDEX.put(level.dimension(), map); + List players = level.players(); + int horizontalRadius = players.isEmpty() ? 192 : 256; + Set centers = new HashSet<>(); + for (ServerPlayer player : players) { + centers.add(player.blockPosition()); + } + if (centers.isEmpty()) { + centers.add(level.getSharedSpawnPos()); + } + totalChunksToScan = centers.size(); + for (BlockPos center : centers) { + BlockPos min = center.offset(-horizontalRadius, level.getMinBuildHeight() - center.getY(), -horizontalRadius); + BlockPos max = center.offset(horizontalRadius, level.getMaxBuildHeight() - center.getY() - 1, horizontalRadius); + for (BlockPos cursor : BlockPos.betweenClosed(min, max)) { + BlockState state = level.getBlockState(cursor); + for (BlockType type : BlockType.values()) { + if (matches(state, type)) { + register(level, cursor.immutable(), type); + } + } + } + ++chunksScanned; + for (ServerPlayer player : players) { + PacketHandler.sendToPlayer(player, new ScanProgressPacket(totalChunksToScan, chunksScanned, !scanning, getLampsFound(), getDripstonesFound(), getGlassBlocksFound())); + } + } + } + + private static List scanNearby(ServerLevel level, BlockPos center, int radius, BlockType type) { + List result = new ArrayList<>(); + for (BlockPos cursor : BlockPos.betweenClosed(center.offset(-radius, -radius, -radius), center.offset(radius, radius, radius))) { + if (cursor.distSqr(center) <= radius * radius && matches(level.getBlockState(cursor), type)) { + result.add(cursor.immutable()); + } + } + return result; + } + + private static boolean matches(BlockState state, BlockType type) { + return switch (type) { + case LAMP -> state.is(Blocks.REDSTONE_LAMP) && state.hasProperty(RedstoneLampBlock.LIT); + case DRIPSTONE -> state.is(Blocks.POINTED_DRIPSTONE); + case GLASS -> GlassBreakingEffects.isGlass(state) && !isReinforcedGlass(state); + }; + } + + private static Set bucket(ServerLevel level, BlockType type) { + EnumMap> map = INDEX.computeIfAbsent(level.dimension(), ignored -> { + EnumMap> created = new EnumMap<>(BlockType.class); + for (BlockType blockType : BlockType.values()) { + created.put(blockType, ConcurrentHashMap.newKeySet()); + } + return created; + }); + return map.computeIfAbsent(type, ignored -> ConcurrentHashMap.newKeySet()); + } + + private static int count(BlockType type) { + int count = 0; + for (EnumMap> map : INDEX.values()) { + count += map.getOrDefault(type, Collections.emptySet()).size(); + } + return count; } public enum BlockType { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ClientSoundHandler.java b/src/main/java/com/vinlanx/explosionoverhaul/ClientSoundHandler.java index deba2d7..a4d7aea 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ClientSoundHandler.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ClientSoundHandler.java @@ -3,11 +3,14 @@ */ package com.vinlanx.explosionoverhaul; +import net.neoforged.api.distmarker.Dist; import net.neoforged.neoforge.client.event.sound.PlaySoundEvent; import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.bus.api.EventPriority; import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +@EventBusSubscriber(modid = ExplosionOverhaul.MODID, value = Dist.CLIENT) public class ClientSoundHandler { private static boolean suppressVanillaExplosionSound = false; private static int suppressionTicksRemaining = 0; @@ -18,7 +21,7 @@ public class ClientSoundHandler { } @SubscribeEvent - public void onClientTick(ClientTickEvent.Post event) { + public static void onClientTick(ClientTickEvent.Post event) { if (suppressionTicksRemaining > 0) { --suppressionTicksRemaining; } else if (suppressionTicksRemaining == 0 && suppressVanillaExplosionSound) { @@ -27,7 +30,7 @@ public class ClientSoundHandler { } @SubscribeEvent(priority=EventPriority.HIGH) - public void onExplosionSound(PlaySoundEvent event) { + public static void onExplosionSound(PlaySoundEvent event) { if (event.getSound() != null && event.getSound().getLocation().getPath().equals("entity.generic.explode") && suppressVanillaExplosionSound) { event.setSound(null); } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/CommonEvents.java b/src/main/java/com/vinlanx/explosionoverhaul/CommonEvents.java new file mode 100644 index 0000000..e64b0de --- /dev/null +++ b/src/main/java/com/vinlanx/explosionoverhaul/CommonEvents.java @@ -0,0 +1,49 @@ +package com.vinlanx.explosionoverhaul; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.level.LevelEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +@EventBusSubscriber(modid = ExplosionOverhaul.MODID) +public final class CommonEvents { + private CommonEvents() { + } + + @SubscribeEvent + public static void onServerTick(ServerTickEvent.Post event) { + ExplosionOverhaul.onServerTick(event.getServer()); + AmbientExplosionManager.onServerTick(event.getServer()); + AsyncCraterManager.onServerTick(event.getServer()); + RedstoneLampEffects.onServerTick(); + GlassBreakingEffects.onServerTick(); + for (ServerLevel level : event.getServer().getAllLevels()) { + DripstoneEffects.onServerTick(level); + } + } + + @SubscribeEvent + public static void onLevelUnload(LevelEvent.Unload event) { + if (event.getLevel() instanceof ServerLevel level) { + AsyncCraterManager.onLevelUnload(level); + } + } + + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + AmbientExplosionManager.onPlayerLoggedIn(player); + BlockIndexManager.showRescanPrompt(player); + } + } + + @SubscribeEvent + public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + AmbientExplosionManager.onPlayerLoggedOut(player); + } + } +} diff --git a/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java b/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java index 1856d96..071907b 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/CraterDeformer.java @@ -1,27 +1,95 @@ package com.vinlanx.explosionoverhaul; -import java.util.Collections; +import java.util.LinkedHashSet; 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.state.BlockState; import net.minecraft.world.phys.Vec3; public class CraterDeformer { public static Set getCraterBlocks(ServerLevel level, Vec3 explosionPos, float power) { - return Collections.emptySet(); + Set blocks = new LinkedHashSet<>(); + float radius = calculateRadius(power); + float coreRatio = ((Double)Config.COMMON.craterCoreRatio.get()).floatValue(); + float coreRadius = radius * Mth.clamp(coreRatio, 0.1f, 0.95f); + float maxResistance = calculateMaxResistance(power); + BlockPos origin = BlockPos.containing(explosionPos); + int r = Mth.ceil(radius + 2.0f); + for (BlockPos cursor : BlockPos.betweenClosed(origin.offset(-r, -r, -r), origin.offset(r, r, r))) { + BlockPos pos = cursor.immutable(); + BlockState state = level.getBlockState(pos); + if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f || ExplosionOverhaul.isBlockStateBlacklisted(state)) { + continue; + } + float resistance = state.getBlock().getExplosionResistance(); + if (resistance > maxResistance && pos.distToCenterSqr(explosionPos) > coreRadius * coreRadius) { + continue; + } + double dx = (pos.getX() + 0.5 - explosionPos.x()) / radius; + double dy = (pos.getY() + 0.5 - explosionPos.y()) / (radius * 0.78); + double dz = (pos.getZ() + 0.5 - explosionPos.z()) / radius; + double normalized = Math.sqrt(dx * dx + dy * dy + dz * dz); + double noise = roughness(pos, explosionPos, power); + double shell = 0.82 + noise * 0.22; + double undercut = pos.getY() + 0.5 < explosionPos.y() ? 0.06 : -0.03; + if (normalized <= shell + undercut || Math.sqrt(pos.distToCenterSqr(explosionPos)) <= coreRadius * (0.82 + noise * 0.12)) { + blocks.add(pos); + } + } + return blocks; } public static float calculateRadius(float power) { - return Math.max(1.0f, power); + float multiplier = ((Double)Config.COMMON.craterSizeMultiplier.get()).floatValue(); + return Math.max(2.5f, Math.min(70.0f, power * 2.15f * multiplier)); } public static float calculateMaxResistance(float power) { - return Float.MAX_VALUE; + return Math.max(8.0f, power * 6.0f); } public static void applyLargeExplosionLogic(ServerLevel level, Vec3 explosionPos, float power) { + spawnDebris(level, explosionPos, power, Math.min(180, Math.round(power * 8.0f))); } public static void applySmallExplosionLogic(ServerLevel level, Vec3 explosionPos, float power) { + spawnDebris(level, explosionPos, power, Math.min(48, Math.round(power * 5.0f))); + } + + public static void spawnDebris(ServerLevel level, Vec3 explosionPos, float power, int maxDebris) { + if (!((Boolean)Config.COMMON.enableFallingBlocks.get()).booleanValue() || maxDebris <= 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) { + 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)) { + 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; + ++spawned; + } + } + + 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; + seed *= 0xff51afd7ed558ccdL; + seed ^= seed >>> 33; + seed *= 0xc4ceb9fe1a85ec53L; + seed ^= seed >>> 33; + return (seed & 0xFFFF) / 65535.0; } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ExplosionOverhaul.java b/src/main/java/com/vinlanx/explosionoverhaul/ExplosionOverhaul.java index 67d8a89..ec1aea4 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ExplosionOverhaul.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ExplosionOverhaul.java @@ -16,12 +16,16 @@ import java.util.List; import java.util.Map; import net.minecraft.core.particles.BlockParticleOption; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.server.MinecraftServer; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; 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; import net.neoforged.bus.api.IEventBus; @@ -67,6 +71,8 @@ public class ExplosionOverhaul { private static final Path CONFIG_DIR = Paths.get("config", "explosionoverhaul"); private static final Path BLACKLIST_JSON = CONFIG_DIR.resolve("DestroyingBlacklist.json"); private static final Path SOURCE_MODES_JSON = CONFIG_DIR.resolve("ExplosionSourceBlacklist.json"); + private static final List DELAYED_SOUNDS = new ArrayList<>(); + private static final List DELAYED_PARTICLES = new ArrayList<>(); public ExplosionOverhaul(IEventBus modEventBus, ModContainer modContainer) { modContainer.registerConfig(ModConfig.Type.COMMON, Config.COMMON_SPEC, "explosionoverhaul/explosionoverhaul-common.toml"); @@ -77,6 +83,7 @@ public class ExplosionOverhaul { ModItems.register(modEventBus); ModBlockEntities.register(modEventBus); ModCreativeTabs.register(modEventBus); + modEventBus.addListener(PacketHandler::registerPayloads); modEventBus.addListener(this::commonSetup); } @@ -195,9 +202,30 @@ public class ExplosionOverhaul { } public static void addDelayedSound(ServerPlayer player, SoundEvent sound, SoundSource source, float x, float y, float z, float volume, float pitch, long seed, long delayTicks) { + if (player == null || sound == null) { + return; + } + synchronized (DELAYED_SOUNDS) { + DELAYED_SOUNDS.add(new DelayedSound(player, sound, source, x, y, z, volume, pitch, seed, Math.max(0L, delayTicks))); + } } public static void addDelayedParticle(ServerLevel level, BlockParticleOption particleOption, double x, double y, double z, int count, double dx, double dy, double dz, double speed, long delayTicks, long durationTicks) { + if (level == null || particleOption == null) { + return; + } + synchronized (DELAYED_PARTICLES) { + DELAYED_PARTICLES.add(new DelayedParticle(level, particleOption, x, y, z, count, dx, dy, dz, speed, Math.max(0L, delayTicks), Math.max(1L, durationTicks))); + } + } + + public static void onServerTick(MinecraftServer server) { + synchronized (DELAYED_SOUNDS) { + DELAYED_SOUNDS.removeIf(sound -> sound.tick()); + } + synchronized (DELAYED_PARTICLES) { + DELAYED_PARTICLES.removeIf(particle -> particle.tick()); + } } public enum ExplosionSourceMode { @@ -209,6 +237,82 @@ public class ExplosionOverhaul { public static class CaveEffects { public static void spawnFallingBlocksAndDust(ServerLevel level, Vec3 explosionPos, ServerPlayer player, float power, long initialDelayTicks) { + int count = Math.max(8, Math.min(80, Math.round(power * 5.0f))); + BlockParticleOption option = new BlockParticleOption(net.minecraft.core.particles.ParticleTypes.BLOCK, Blocks.STONE.defaultBlockState()); + addDelayedParticle(level, option, explosionPos.x(), explosionPos.y() + 1.0, explosionPos.z(), count, power * 0.3, power * 0.18, power * 0.3, 0.18, initialDelayTicks, 26); + } + } + + private static final class DelayedSound { + private final ServerPlayer player; + private final SoundEvent sound; + private final SoundSource source; + private final double x; + private final double y; + private final double z; + private final float volume; + private final float pitch; + private final long seed; + private long delayTicks; + + private DelayedSound(ServerPlayer player, SoundEvent sound, SoundSource source, double x, double y, double z, float volume, float pitch, long seed, long delayTicks) { + this.player = player; + this.sound = sound; + this.source = source; + this.x = x; + this.y = y; + this.z = z; + this.volume = volume; + this.pitch = pitch; + this.seed = seed; + this.delayTicks = delayTicks; + } + + private boolean tick() { + if (this.player.isRemoved() || this.player.connection == null) { + return true; + } + if (this.delayTicks-- > 0L) { + return false; + } + this.player.connection.send(new ClientboundSoundPacket(Holder.direct(this.sound), this.source, this.x, this.y, this.z, this.volume, this.pitch, this.seed)); + return true; + } + } + + private static final class DelayedParticle { + private final ServerLevel level; + private final BlockParticleOption option; + private final double x; + private final double y; + private final double z; + private final int count; + private final double dx; + private final double dy; + private final double dz; + private final double speed; + private long delayTicks; + + private DelayedParticle(ServerLevel level, BlockParticleOption option, double x, double y, double z, int count, double dx, double dy, double dz, double speed, long delayTicks, long durationTicks) { + this.level = level; + this.option = option; + this.x = x; + this.y = y; + this.z = z; + this.count = count; + this.dx = dx; + this.dy = dy; + this.dz = dz; + this.speed = speed; + this.delayTicks = delayTicks; + } + + private boolean tick() { + if (this.delayTicks-- > 0L) { + return false; + } + this.level.sendParticles(this.option, this.x, this.y, this.z, this.count, this.dx, this.dy, this.dz, this.speed); + return true; } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/PacketHandler.java b/src/main/java/com/vinlanx/explosionoverhaul/PacketHandler.java index f7fbf32..56aaa84 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/PacketHandler.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/PacketHandler.java @@ -29,6 +29,7 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import com.vinlanx.explosionoverhaul.compat.network.NetworkRegistry; import com.vinlanx.explosionoverhaul.compat.network.simple.SimpleChannel; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; public class PacketHandler { private static final String PROTOCOL_VERSION = "1"; @@ -60,8 +61,12 @@ public class PacketHandler { INSTANCE.registerMessage(messageId++, ScanLoadControlPacket.class, ScanLoadControlPacket::encode, ScanLoadControlPacket::decode, ScanLoadControlPacket::handle); } + public static void registerPayloads(RegisterPayloadHandlersEvent event) { + INSTANCE.registerPayloads(event); + } + public static void sendToPlayer(ServerPlayer player, Object message) { - INSTANCE.send(null, message); + INSTANCE.send(player, message); } public static void sendToAll(Object message) { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/RedstoneLampEffects.java b/src/main/java/com/vinlanx/explosionoverhaul/RedstoneLampEffects.java index 878d089..cdc2b9a 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/RedstoneLampEffects.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/RedstoneLampEffects.java @@ -1,5 +1,8 @@ package com.vinlanx.explosionoverhaul; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -10,36 +13,95 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; public class RedstoneLampEffects { + private static final List TASKS = new ArrayList<>(); + public static void triggerLampFlicker(ServerLevel level, ServerPlayer player, float power, long delayTicks, double distance) { - triggerNearbyLamps(level, player.position(), power); + triggerNearbyLamps(level, player.position(), power, delayTicks); } public static void triggerNearbyLamps(ServerLevel level, Vec3 explosionPos, float power) { + triggerNearbyLamps(level, explosionPos, power, 0L); + } + + private static void triggerNearbyLamps(ServerLevel level, Vec3 explosionPos, float power, long delayTicks) { if (!((Boolean)Config.COMMON.enableLampFlicker.get()).booleanValue()) { return; } int radius = Math.min((Integer)Config.COMMON.lampFlickerSearchRadius.get(), Math.max(8, Math.round(power * 6.0f))); BlockPos origin = BlockPos.containing(explosionPos); - int changed = 0; - for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-radius, -radius, -radius), origin.offset(radius, radius, radius))) { - if (changed >= 96) { - break; + List candidates = new ArrayList<>(BlockIndexManager.getNearby(level, origin, radius, BlockIndexManager.BlockType.LAMP)); + if (candidates.isEmpty()) { + for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-radius, -radius, -radius), origin.offset(radius, radius, radius))) { + candidates.add(pos.immutable()); } - BlockState state = level.getBlockState(pos); - if (!state.is(Blocks.REDSTONE_LAMP) || !state.hasProperty(RedstoneLampBlock.LIT)) { - continue; + } + int scheduled = 0; + synchronized (TASKS) { + for (BlockPos pos : candidates) { + if (scheduled >= 96) { + break; + } + BlockState state = level.getBlockState(pos); + if (!state.is(Blocks.REDSTONE_LAMP) || !state.hasProperty(RedstoneLampBlock.LIT) || !state.getValue(RedstoneLampBlock.LIT)) { + continue; + } + long localDelay = delayTicks + Math.round(Math.sqrt(pos.distToCenterSqr(explosionPos)) / 343.0 * 20.0); + TASKS.add(new FlickerTask(level, pos.immutable(), localDelay, 18 + level.random.nextInt(24), 2 + level.random.nextInt(4))); + ++scheduled; } - BlockPos immutable = pos.immutable(); - boolean lit = state.getValue(RedstoneLampBlock.LIT); - level.setBlock(immutable, state.setValue(RedstoneLampBlock.LIT, !lit), 3); - level.playSound(null, immutable, ModSounds.LAMP_FLICKER_SPARK_1.get(), SoundSource.BLOCKS, 0.25f, 0.8f + level.random.nextFloat() * 0.4f); - if (level.hasNeighborSignal(immutable)) { - level.setBlock(immutable, state.setValue(RedstoneLampBlock.LIT, true), 3); - } - ++changed; } } public static void onServerTick() { + synchronized (TASKS) { + Iterator iterator = TASKS.iterator(); + while (iterator.hasNext()) { + if (iterator.next().tick()) { + iterator.remove(); + } + } + } + } + + private static final class FlickerTask { + private final ServerLevel level; + private final BlockPos pos; + private final int interval; + private long delayTicks; + private int durationTicks; + private int intervalTicks; + private boolean currentLit = true; + + private FlickerTask(ServerLevel level, BlockPos pos, long delayTicks, int durationTicks, int interval) { + this.level = level; + this.pos = pos; + this.delayTicks = delayTicks; + this.durationTicks = durationTicks; + this.interval = interval; + this.intervalTicks = interval; + } + + private boolean tick() { + if (this.delayTicks-- > 0L) { + return false; + } + BlockState state = this.level.getBlockState(this.pos); + if (!state.is(Blocks.REDSTONE_LAMP) || !state.hasProperty(RedstoneLampBlock.LIT)) { + return true; + } + if (this.durationTicks-- <= 0) { + this.level.setBlock(this.pos, state.setValue(RedstoneLampBlock.LIT, this.level.hasNeighborSignal(this.pos)), 3); + return true; + } + if (this.intervalTicks-- <= 0) { + this.currentLit = !this.currentLit; + this.level.setBlock(this.pos, state.setValue(RedstoneLampBlock.LIT, this.currentLit), 3); + if (this.level.random.nextFloat() < 0.35f) { + this.level.playSound(null, this.pos, ModSounds.LAMP_FLICKER_SOUNDS.get(this.level.random.nextInt(ModSounds.LAMP_FLICKER_SOUNDS.size())).get(), SoundSource.BLOCKS, 0.24f, 0.75f + this.level.random.nextFloat() * 0.5f); + } + this.intervalTicks = this.interval; + } + return false; + } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ScanControlPacket.java b/src/main/java/com/vinlanx/explosionoverhaul/ScanControlPacket.java index 40ae0c9..da16e4a 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ScanControlPacket.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ScanControlPacket.java @@ -29,7 +29,7 @@ public class ScanControlPacket { ServerPlayer player = ((NetworkEvent.Context)ctx.get()).getSender(); if (player != null && BlockIndexManager.isPlayerAuthorized(player)) { if (msg.startScan) { - BlockIndexManager.startManualScan(); + BlockIndexManager.forceServerScan(player.getServer()); } else { BlockIndexManager.cancelManualScan(); } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ScanLoadControlPacket.java b/src/main/java/com/vinlanx/explosionoverhaul/ScanLoadControlPacket.java index 56c058f..457b36f 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ScanLoadControlPacket.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ScanLoadControlPacket.java @@ -31,7 +31,7 @@ public class ScanLoadControlPacket { if (msg.loadExisting) { BlockIndexManager.loadExistingData(); } else { - BlockIndexManager.startNewScan(); + BlockIndexManager.forceServerScan(player.getServer()); } } }); diff --git a/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java b/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java index 18ae79d..8da4d30 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/ServerExplosionHandler.java @@ -6,70 +6,237 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import net.minecraft.core.BlockPos; -import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Explosion; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; public class ServerExplosionHandler { + private static final double SOUND_SPEED_BLOCKS_PER_TICK = 343.0 / 20.0; + public static void handleExplosion(ServerLevel level, Explosion explosion, List affectedBlocks) { - if (!((Boolean)Config.COMMON.enableCraterDestruction.get()).booleanValue()) { - affectedBlocks.clear(); - return; - } float power = 4.0f; if (explosion instanceof IExplosionPower explosionPower) { power = Math.max(1.0f, explosionPower.getPower()); } Vec3 center = explosion instanceof ExplosionAccessor accessor ? accessor.explosionoverhaul$getCenter() : Vec3.ZERO; + Entity source = explosion instanceof ExplosionAccessor accessor ? accessor.explosionoverhaul$getSource() : null; + ExplosionOverhaul.ExplosionSourceMode sourceMode = sourceMode(source); + if (sourceMode == ExplosionOverhaul.ExplosionSourceMode.VANILLA) { + return; + } + power = ExplosionClusterHandler.calculateClusteredPower(level, center, power); - int radius = Math.max(5, Math.min(18, Math.round(power * 2.0f * ((Double)Config.COMMON.craterSizeMultiplier.get()).floatValue()))); - ExplosionOverhaul.LOGGER.info("Explosion Overhaul handling explosion at {} with power {} and radius {}", center, power, radius); + ExplosionOverhaul.LOGGER.info("Explosion Overhaul handling explosion at {} with power {}", center, power); + + dispatchPlayerEffects(level, center, power); GlassBreakingEffects.trigger(level, center, power); DripstoneEffects.handleDripstoneFall(level, BlockPos.containing(center), Math.round(power), level.random); RedstoneLampEffects.triggerNearbyLamps(level, center, power); - Set existing = new HashSet<>(affectedBlocks); - BlockPos origin = BlockPos.containing(center); - double radiusSq = radius * radius; - int directBlocksChanged = 0; - for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-radius, -radius, -radius), origin.offset(radius, radius, radius))) { - double distanceSq = pos.distToCenterSqr(center); - if (distanceSq > radiusSq) { - continue; - } - BlockState state = level.getBlockState(pos); - if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f) { - continue; - } - BlockPos immutable = pos.immutable(); - if (existing.add(immutable)) { - affectedBlocks.add(immutable); - } - if (directBlocksChanged < 2500) { - level.setBlock(immutable, Blocks.AIR.defaultBlockState(), 3); - ++directBlocksChanged; + ExplosionOverhaul.CaveEffects.spawnFallingBlocksAndDust(level, center, nearestPlayer(level, center), power, 8L); + + boolean noDestruction = sourceMode == ExplosionOverhaul.ExplosionSourceMode.NO_DESTRUCTION + || sourceMode == ExplosionOverhaul.ExplosionSourceMode.NO_DESTRUCTION_GLASSWORKS + || !((Boolean)Config.COMMON.enableCraterDestruction.get()).booleanValue(); + if (noDestruction) { + affectedBlocks.clear(); + } else if (((Boolean)Config.COMMON.enableAsyncCrater.get()).booleanValue()) { + affectedBlocks.clear(); + AsyncCraterManager.submit(level, center, power); + } else { + Set craterBlocks = new HashSet<>(CraterDeformer.getCraterBlocks(level, center, power)); + affectedBlocks.clear(); + affectedBlocks.addAll(craterBlocks); + if (power >= 12.0f) { + CraterDeformer.applyLargeExplosionLogic(level, center, power); + } else { + CraterDeformer.applySmallExplosionLogic(level, center, power); } } - level.sendParticles(ParticleTypes.EXPLOSION_EMITTER, center.x(), center.y(), center.z(), Math.max(2, radius / 3), 0.5, 0.5, 0.5, 0.0); - level.sendParticles(ParticleTypes.LARGE_SMOKE, center.x(), center.y(), center.z(), radius * 8, radius * 0.35, radius * 0.25, radius * 0.35, 0.08); - level.sendParticles(ParticleTypes.FLAME, center.x(), center.y(), center.z(), radius * 4, radius * 0.25, radius * 0.2, radius * 0.25, 0.05); - spawnCustomExplosionParticles(level, center, power, radius); + + spawnServerParticles(level, center, power); } - private static void spawnCustomExplosionParticles(ServerLevel level, Vec3 center, float power, int radius) { - int glowCount = Math.min(220, Math.max(24, radius * 9)); - int smokeCount = Math.min(260, Math.max(32, radius * 11)); - int plasmaCount = Math.min(120, Math.max(12, radius * 5)); - int sparkCount = Math.min(80, Math.max(10, radius * 3)); + private static void dispatchPlayerEffects(ServerLevel level, Vec3 center, float power) { + for (ServerPlayer player : level.players()) { + double distance = player.position().distanceTo(center); + if (distance > Math.max(96.0, power * 90.0)) { + continue; + } + boolean playerInCave = isInCave(level, player.blockPosition()); + boolean explosionInCave = isInCave(level, BlockPos.containing(center)); + boolean playerInHouse = isInHouse(level, player.blockPosition()); + boolean los = hasLineOfSight(level, player.getEyePosition(), center); + int delayTicks = soundDelay(distance); + PacketHandler.sendToPlayer(player, new SuppressExplosionSoundPacket(power)); + PacketHandler.sendToPlayer(player, new PlayTrackedSoundPacket(center, soundFor(power, distance, explosionInCave, playerInCave, playerInHouse), soundVolume(power, distance), soundPitch(power), delayTicks, playerInHouse)); + PacketHandler.sendToPlayer(player, new ExplosionVisualsPacket(center, power)); + PacketHandler.sendToPlayer(player, new FlashEffectPacket(center, power)); + CameraShakeProfile profile = determineCameraShakeProfile(power, distance, playerInCave, level); + if (profile.durationTicks() > 0) { + PacketHandler.sendToPlayer(player, new CameraShakePacket(profile.intensity(), profile.durationTicks(), profile.pushIntensity(), delayTicks)); + } + PacketHandler.sendToPlayer(player, new StartConcussionPacket(power, distance, los, explosionInCave)); + PacketHandler.sendToPlayer(player, new SpawnShockwavePacket(center, power)); + PacketHandler.sendToPlayer(player, new SpawnDustCloudPacket(center, power)); + PacketHandler.sendToPlayer(player, new SpawnMistCloudPacket(center, power)); + PacketHandler.sendToPlayer(player, new SpawnLineSparksPacket(center, power)); + if (playerInCave || explosionInCave) { + PacketHandler.sendToPlayer(player, new SpawnAmbientCaveDustPacket(power)); + } + if (distance < Math.max(12.0, power * 4.0)) { + pushPlayer(player, center, power, distance); + } + RedstoneLampEffects.triggerLampFlicker(level, player, power, delayTicks, distance); + } + } + + private static void spawnServerParticles(ServerLevel level, Vec3 center, float power) { + int radius = Math.round(CraterDeformer.calculateRadius(power)); + int glowCount = Math.min(320, Math.max(32, radius * 9)); + int smokeCount = Math.min(360, Math.max(48, radius * 12)); + int plasmaCount = Math.min(180, Math.max(14, radius * 5)); + int sparkCount = Math.min(140, Math.max(12, radius * 4)); float maxRadius = Math.max(4.0f, radius); for (int i = 0; i < 3; ++i) { - level.sendParticles(new CustomGlowParticleOptions(i % 2, power, 0.95f + i * 0.2f, i, (float)center.y(), maxRadius, 0.5f), center.x(), center.y(), center.z(), glowCount / 3, radius * 0.18, radius * 0.12, radius * 0.18, 0.08); + level.sendParticles(new CustomGlowParticleOptions(i % 2, power, 0.95f + i * 0.22f, i, (float)center.y(), maxRadius, 0.5f), center.x(), center.y(), center.z(), glowCount / 3, radius * 0.2, radius * 0.14, radius * 0.2, 0.1); } - level.sendParticles(new SmokeParticleOptions(Math.max(1.2f, radius * 0.28f), 58 + radius * 3, 0.18f, 0.16f, 0.14f, 0.7f, true, 0.0f, 0.35f, null), center.x(), center.y() + 0.2, center.z(), smokeCount, radius * 0.35, radius * 0.22, radius * 0.35, 0.05); - level.sendParticles(new PlasmaParticleOptions(power), center.x(), center.y(), center.z(), plasmaCount, radius * 0.16, radius * 0.1, radius * 0.16, 0.45); - level.sendParticles(ModParticles.LINE_SPARK.get(), center.x(), center.y(), center.z(), sparkCount, radius * 0.22, radius * 0.16, radius * 0.22, 0.55); + level.sendParticles(new SmokeParticleOptions(Math.max(1.2f, radius * 0.3f), 70 + radius * 3, 0.18f, 0.16f, 0.14f, 0.72f, true, 0.0f, 0.35f, null), center.x(), center.y() + 0.25, center.z(), smokeCount, radius * 0.4, radius * 0.26, radius * 0.4, 0.06); + level.sendParticles(new PlasmaParticleOptions(power), center.x(), center.y(), center.z(), plasmaCount, radius * 0.18, radius * 0.12, radius * 0.18, 0.48); + level.sendParticles(ModParticles.LINE_SPARK.get(), center.x(), center.y(), center.z(), sparkCount, radius * 0.24, radius * 0.18, radius * 0.24, 0.6); + } + + public static ResourceLocation soundFor(float power, double distance, boolean explosionInCave, boolean playerInCave, boolean playerInHouse) { + int tier = powerTier(power); + int variant = 1 + Math.floorMod((int)(power * 31.0f + distance), 3); + String band = distance < 90.0 ? "close" : distance < 700.0 ? "medium" : distance < 2100.0 ? "far" : "superfar"; + String environment = ""; + if (!band.equals("close")) { + if (explosionInCave && playerInHouse) { + environment = "_cave_to_house"; + } else if (playerInHouse) { + environment = "_to_house"; + } else if (explosionInCave || playerInCave) { + environment = band.equals("medium") && playerInCave && !explosionInCave ? "_underground" : "_cave"; + } + } + String name = "explode_" + band + environment + "_power_" + tier + "_" + variant; + ResourceLocation id = ResourceLocation.fromNamespaceAndPath(ExplosionOverhaul.MODID, name); + if (!BuiltInRegistries.SOUND_EVENT.containsKey(id)) { + id = ResourceLocation.fromNamespaceAndPath(ExplosionOverhaul.MODID, "explode_" + band + "_power_" + tier + "_" + variant); + } + if (!BuiltInRegistries.SOUND_EVENT.containsKey(id)) { + id = ResourceLocation.fromNamespaceAndPath(ExplosionOverhaul.MODID, "explode_close_power_" + tier + "_" + variant); + } + return id; + } + + private static ExplosionOverhaul.ExplosionSourceMode sourceMode(Entity source) { + if (source == null || source.getType() == null) { + return ExplosionOverhaul.ExplosionSourceMode.DEFAULT; + } + ResourceLocation id = BuiltInRegistries.ENTITY_TYPE.getKey(source.getType()); + return id == null ? ExplosionOverhaul.ExplosionSourceMode.DEFAULT : ExplosionOverhaul.getSourceMode(id.toString()); + } + + private static int powerTier(float power) { + if (power <= 4.0f) { + return 1; + } + if (power <= 8.0f) { + return 2; + } + if (power <= 14.0f) { + return 3; + } + if (power <= 24.0f) { + return 4; + } + if (power <= 40.0f) { + return 5; + } + if (power <= 70.0f) { + return 6; + } + return 7; + } + + private static int soundDelay(double distance) { + if (!((Boolean)Config.COMMON.enableAdvancedSoundSpeed.get()).booleanValue()) { + return Math.max(0, (int)Math.round(distance / SOUND_SPEED_BLOCKS_PER_TICK)); + } + return Math.max(0, (int)Math.round(distance / SOUND_SPEED_BLOCKS_PER_TICK)); + } + + private static float soundVolume(float power, double distance) { + return Mth.clamp((float)(1.2 + power * 0.08 - distance / 3500.0), 0.22f, 5.0f); + } + + private static float soundPitch(float power) { + return Mth.clamp(1.08f - power * 0.006f, 0.65f, 1.08f); + } + + public static CameraShakeProfile determineCameraShakeProfile(float power, double distance, boolean playerInCave, ServerLevel level) { + if (!((Boolean)Config.CLIENT.enableCameraShake.get()).booleanValue() && !((Boolean)Config.COMMON.enablePlayerShake.get()).booleanValue()) { + return new CameraShakeProfile(0.0f, 0, 0.0f); + } + double maxDistance = Math.max(18.0, power * (playerInCave ? 8.0 : 5.0)); + double percent = Math.max(0.0, 1.0 - distance / maxDistance); + float amplifier = ((Double)Config.COMMON.playerShakeAmplifier.get()).floatValue(); + float intensity = (float)Mth.clamp(percent * (0.45 + power * 0.035) * amplifier, 0.0, 4.0); + int duration = intensity <= 0.02f ? 0 : Mth.clamp((int)Math.round(10 + power * 1.4 + percent * 45.0), 6, 140); + float push = (float)Mth.clamp(percent * power * 0.07, 0.0, 2.0); + return new CameraShakeProfile(intensity, duration, push); + } + + private static void pushPlayer(ServerPlayer player, Vec3 center, float power, double distance) { + Vec3 direction = player.position().subtract(center); + if (direction.lengthSqr() < 0.0001) { + direction = new Vec3(0.0, 1.0, 0.0); + } + double strength = Mth.clamp((1.0 - distance / Math.max(12.0, power * 4.0)) * power * 0.06, 0.0, 1.4); + player.setDeltaMovement(player.getDeltaMovement().add(direction.normalize().scale(strength).add(0.0, Math.min(0.45, strength * 0.35), 0.0))); + } + + private static boolean hasLineOfSight(ServerLevel level, Vec3 from, Vec3 to) { + return level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, null)).getType() == HitResult.Type.MISS; + } + + private static boolean isInCave(ServerLevel level, BlockPos pos) { + return !level.canSeeSky(pos) && solidBlocksAbove(level, pos, 16) >= 5; + } + + private static boolean isInHouse(ServerLevel level, BlockPos pos) { + return !level.canSeeSky(pos) && solidBlocksAbove(level, pos, 8) >= 2; + } + + private static int solidBlocksAbove(ServerLevel level, BlockPos pos, int height) { + int count = 0; + for (int y = 1; y <= height; ++y) { + if (!level.getBlockState(pos.above(y)).isAir()) { + ++count; + } + } + return count; + } + + private static ServerPlayer nearestPlayer(ServerLevel level, Vec3 pos) { + ServerPlayer nearest = null; + double nearestDistance = Double.MAX_VALUE; + for (ServerPlayer player : level.players()) { + double distance = player.position().distanceToSqr(pos); + if (distance < nearestDistance) { + nearestDistance = distance; + nearest = player; + } + } + return nearest; } public static void register() { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/CameraShakeConcussionEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/CameraShakeConcussionEffect.java index 8eda4e9..248a24b 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/CameraShakeConcussionEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/CameraShakeConcussionEffect.java @@ -1,18 +1,44 @@ package com.vinlanx.explosionoverhaul.client; +import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Mth; public class CameraShakeConcussionEffect { + private static int ticksRemaining; + private static int totalTicks; + private static float intensity; + public static void start(int seconds, float intensity) { + CameraShakeConcussionEffect.totalTicks = Math.max(1, seconds * 20); + CameraShakeConcussionEffect.ticksRemaining = Math.max(CameraShakeConcussionEffect.ticksRemaining, CameraShakeConcussionEffect.totalTicks); + CameraShakeConcussionEffect.intensity = Math.max(CameraShakeConcussionEffect.intensity, intensity); } public static void stop() { + ticksRemaining = 0; + totalTicks = 0; + intensity = 0.0f; } public static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + if (ticksRemaining <= 0 || mc.player == null) { + intensity = 0.0f; + return; + } + applySway(mc.player, totalTicks); + --ticksRemaining; } public static void applySway(LocalPlayer player, float totalTicks) { + float progress = 1.0f - ticksRemaining / Math.max(1.0f, totalTicks); + float eased = easeInOutCubic(Math.min(1.0f, progress)); + float fade = 1.0f - eased; + float yaw = (float)Math.sin(player.tickCount * 0.31f) * intensity * 0.7f * fade; + float pitch = (float)Math.cos(player.tickCount * 0.23f) * intensity * 0.35f * fade; + player.setYRot(player.getYRot() + yaw); + player.setXRot(Mth.clamp(player.getXRot() + pitch, -89.0f, 89.0f)); } public static float easeInOutCubic(float t) { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffectEvents.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffectEvents.java new file mode 100644 index 0000000..a6cd61d --- /dev/null +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffectEvents.java @@ -0,0 +1,29 @@ +package com.vinlanx.explosionoverhaul.client; + +import com.vinlanx.explosionoverhaul.ExplosionOverhaul; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.RenderGuiEvent; + +@EventBusSubscriber(modid = ExplosionOverhaul.MODID, value = Dist.CLIENT) +public final class ClientEffectEvents { + private ClientEffectEvents() { + } + + @SubscribeEvent + public static void onClientTick(ClientTickEvent.Post event) { + ClientEffects.onClientTick(); + ConcussionAudioEffect.onClientTick(); + CameraShakeConcussionEffect.onClientTick(); + LowPassConcussionEffect.onClientTick(); + DeafnessConcussionEffect.onClientTick(); + } + + @SubscribeEvent + public static void onRenderGui(RenderGuiEvent.Post event) { + ClientEffects.renderFlash(event); + ConcussionAudioEffect.renderHeartbeatHUD(event.getGuiGraphics()); + } +} diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java index 2a582d8..56732bf 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ClientEffects.java @@ -1,52 +1,247 @@ package com.vinlanx.explosionoverhaul.client; +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 net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; import net.neoforged.neoforge.client.event.RenderGuiEvent; public class ClientEffects { + 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 int shakeTicks; + private static float shakeIntensity; + private static float pushIntensity; + private static int flashTicks; + private static float flashPower; + public static void addTrackedSound(PlayTrackedSoundPacket msg) { + synchronized (TRACKED_SOUNDS) { + TRACKED_SOUNDS.add(new DelayedSound(msg, Math.max(0L, msg.getDelayTicks()))); + } } public static Vec3 calculateSoundPosition(Player player, Vec3 explosionPos, boolean isPlayerInHouse) { - return explosionPos; + if (player == null || !isPlayerInHouse) { + return explosionPos; + } + Vec3 direction = explosionPos.subtract(player.position()); + if (direction.lengthSqr() < 0.0001) { + return explosionPos; + } + return player.position().add(direction.normalize().scale(Math.min(16.0, direction.length()))); } 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); } public static void triggerDelayedCameraShake(float intensity, int durationTicks, float pushIntensity, int delayTicks) { + synchronized (DELAYED_SHAKES) { + DELAYED_SHAKES.add(new DelayedShake(intensity, durationTicks, pushIntensity, Math.max(0, delayTicks))); + } } public static void triggerRealisticExplosion(Vec3 position, float power) { + if (!((Boolean)Config.CLIENT.enableExplosionParticles.get()).booleanValue()) { + return; + } + synchronized (EXPLOSION_EFFECTS) { + EXPLOSION_EFFECTS.add(new PhysicsBasedExplosionEffect(position, power)); + } } public static void addFlashEffect(Vec3 explosionPos, float power) { + if (!((Boolean)Config.CLIENT.enableFlashEffect.get()).booleanValue()) { + return; + } + flashTicks = Math.max(flashTicks, 8 + Mth.floor(power * 0.5f)); + flashPower = Math.max(flashPower, 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(); + } + } + } + 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; + } } public static void renderFlash(RenderGuiEvent.Post event) { + renderFlash(event.getGuiGraphics()); } public static void renderFlash(GuiGraphics graphics) { + if (flashTicks <= 0) { + return; + } + Minecraft mc = Minecraft.getInstance(); + float alpha = Mth.clamp(flashTicks / 12.0f, 0.0f, 1.0f) * ((Double)Config.CLIENT.flashMaxOpacity.get()).floatValue(); + alpha = Mth.clamp(alpha * (0.6f + flashPower * 0.018f), 0.0f, 0.92f); + int color = ((int)(alpha * 255.0f) << 24) | 0xFFFFD7; + graphics.fill(0, 0, mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight(), color); } public static void triggerShockwave(Vec3 position, float power) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || !((Boolean)Config.CLIENT.enableShockwaveEffect.get()).booleanValue()) { + return; + } + int count = Math.min(120, Math.max(24, Math.round(power * 5.0f))); + for (int i = 0; i < count; ++i) { + double angle = Math.PI * 2.0 * i / count; + double speed = 0.18 + Math.min(0.75, power * 0.018); + mc.level.addParticle(ModParticles.LINE_SPARK.get(), position.x, position.y + 0.12, position.z, Math.cos(angle) * speed, 0.01, Math.sin(angle) * speed); + } } public static void triggerDustCloud(Vec3 position, float power) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || !((Boolean)Config.CLIENT.enableGroundDustEffect.get()).booleanValue()) { + return; + } + int count = Math.min(180, Math.max(30, Math.round(power * 8.0f * ((Double)Config.CLIENT.groundDustQuality.get()).floatValue()))); + mc.level.addParticle(new SmokeParticleOptions(Math.max(1.4f, power * 0.35f), 80, 0.2f, 0.17f, 0.14f, 0.68f, true, 0.0f, 0.2f, null), position.x, position.y + 0.2, position.z, 0.0, 0.04, 0.0); + for (int i = 0; i < count; ++i) { + Vec3 velocity = randomHorizontal(mc.player == null ? 0 : mc.player.getRandom().nextLong()).scale(0.04 + mc.level.random.nextDouble() * 0.16); + mc.level.addParticle(new SmokeParticleOptions(0.9f + mc.level.random.nextFloat() * 1.7f, 45 + mc.level.random.nextInt(55), 0.18f, 0.16f, 0.13f, 0.58f, true, 0.0f, 0.2f, null), position.x, position.y + 0.1, position.z, velocity.x, 0.03 + mc.level.random.nextDouble() * 0.05, velocity.z); + } } public static void triggerMistCloud(Vec3 position, float power) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || !((Boolean)Config.CLIENT.enableGroundMistEffect.get()).booleanValue()) { + return; + } + int count = Math.min(120, Math.max(20, Math.round(power * 5.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); + } } public static void triggerLineSparks(Vec3 position, float power) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || !((Boolean)Config.CLIENT.enableLineSparks.get()).booleanValue()) { + 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); + } } public static void triggerAmbientCaveDust(float power) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == 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); + } + } + + 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 final class DelayedSound { + private final PlayTrackedSoundPacket packet; + private long delayTicks; + + private DelayedSound(PlayTrackedSoundPacket packet, long delayTicks) { + this.packet = packet; + this.delayTicks = delayTicks; + } + + private boolean tick(Minecraft mc) { + if (this.delayTicks-- > 0L) { + return false; + } + if (mc.level == null || mc.player == null) { + return true; + } + SoundEvent event = BuiltInRegistries.SOUND_EVENT.get(this.packet.getSoundId()); + if (event != null) { + Vec3 pos = calculateSoundPosition(mc.player, this.packet.getExplosionPos(), this.packet.isPlayerInHouse()); + mc.level.playLocalSound(pos.x, pos.y, pos.z, event, SoundSource.AMBIENT, this.packet.getVolume(), this.packet.getPitch(), false); + } + return true; + } + } + + private static final class DelayedShake { + private final float intensity; + private final int durationTicks; + private final float pushIntensity; + private int delayTicks; + + private DelayedShake(float intensity, int durationTicks, float pushIntensity, int delayTicks) { + this.intensity = intensity; + this.durationTicks = durationTicks; + this.pushIntensity = pushIntensity; + this.delayTicks = delayTicks; + } + + private boolean tick() { + if (this.delayTicks-- > 0) { + return false; + } + ClientEffects.triggerLocalCameraShake(this.intensity, this.durationTicks, this.pushIntensity); + return true; + } } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/ConcussionAudioEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/ConcussionAudioEffect.java index 9aa9489..0910e82 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/ConcussionAudioEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/ConcussionAudioEffect.java @@ -1,28 +1,97 @@ package com.vinlanx.explosionoverhaul.client; +import com.vinlanx.explosionoverhaul.Config; +import com.vinlanx.explosionoverhaul.ModSounds; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundSource; public class ConcussionAudioEffect { + private static int ticksRemaining; + private static float currentIntensity; + private static int heartbeatTicks; + private static int tinnitusCooldown; + public static void start(float power, double distance, boolean hasDirectLineOfSight, boolean explosionInCave) { + if (!((Boolean)Config.CLIENT.enableConcussion.get()).booleanValue()) { + return; + } + double rawPercent = computePercent(power, distance, explosionInCave); + double effectivePercent = hasDirectLineOfSight ? rawPercent : applyOcclusion(rawPercent); + if (effectivePercent <= 1.0) { + return; + } + float chance = ((Double)Config.CLIENT.concussionChanceMultiplier.get()).floatValue(); + currentIntensity = Math.max(currentIntensity, (float)(computeBaseIntensityPercent(effectivePercent) / 100.0 * chance)); + ticksRemaining = Math.max(ticksRemaining, (int)Math.round((computeBaseSilentSeconds(effectivePercent, explosionInCave) + power * 0.08) * 20.0 * ((Double)Config.CLIENT.concussionDurationMultiplier.get()).floatValue())); + ticksRemaining = Math.min(ticksRemaining, 20 * 18); + if (((Boolean)Config.CLIENT.enableCameraSway.get()).booleanValue()) { + CameraShakeConcussionEffect.start(Math.max(1, ticksRemaining / 20), currentIntensity * ((Double)Config.CLIENT.cameraSwayIntensity.get()).floatValue()); + } + if (((Boolean)Config.CLIENT.enableLowPass.get()).booleanValue()) { + LowPassConcussionEffect.start(Math.max(1, ticksRemaining / 20), currentIntensity, effectivePercent, hasDirectLineOfSight ? "direct" : "occluded", Math.round(currentIntensity * 100.0f)); + } + if (((Boolean)Config.CLIENT.enableDeafness.get()).booleanValue()) { + DeafnessConcussionEffect.start(currentIntensity, computeBaseSilentSeconds(effectivePercent, explosionInCave), effectivePercent, hasDirectLineOfSight ? "direct" : "occluded", Math.round(currentIntensity * 100.0f)); + tinnitusCooldown = 1; + } } public static void stopAll() { + ticksRemaining = 0; + currentIntensity = 0.0f; + heartbeatTicks = 0; + tinnitusCooldown = 0; + } + + public static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + if (ticksRemaining <= 0 || mc.player == null || mc.level == null) { + currentIntensity = 0.0f; + return; + } + --ticksRemaining; + currentIntensity *= 0.992f; + if (((Boolean)Config.CLIENT.enableHeartbeatPulse.get()).booleanValue()) { + updateHeartbeat(mc.player, currentIntensity); + } + if (tinnitusCooldown > 0 && --tinnitusCooldown == 0) { + mc.level.playLocalSound(mc.player.getX(), mc.player.getY(), mc.player.getZ(), ModSounds.LOW_SOUND.get(), SoundSource.AMBIENT, Math.min(0.8f, currentIntensity), 1.25f, false); + } } public static void updateHeartbeat(LocalPlayer player, float currentIntensity) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || heartbeatTicks-- > 0) { + return; + } + heartbeatTicks = Math.max(10, (int)(32 - currentIntensity * 16.0f)); + mc.level.playLocalSound(player.getX(), player.getY(), player.getZ(), ModSounds.HEART_LAB.get(), SoundSource.PLAYERS, 0.28f + currentIntensity * 0.35f, 1.0f, false); + if (heartbeatTicks > 4) { + mc.level.playLocalSound(player.getX(), player.getY(), player.getZ(), ModSounds.HEART_DAB.get(), SoundSource.PLAYERS, 0.22f + currentIntensity * 0.25f, 1.0f, false); + } } public static float getCurrentHeartbeatVisual() { - return 0.0f; + return Math.max(0.0f, Math.min(1.0f, currentIntensity)); } public static void renderHeartbeatHUD(GuiGraphics guiGraphics) { + if (!((Boolean)Config.CLIENT.showHeartbeatHUD.get()).booleanValue() || currentIntensity <= 0.02f) { + return; + } + Minecraft mc = Minecraft.getInstance(); + String text = "Heartbeat " + Math.round(60 + currentIntensity * 80) + " BPM"; + guiGraphics.drawString(mc.font, Component.literal(text), mc.getWindow().getGuiScaledWidth() - mc.font.width(text) - 8, 8, 0xFFFF7777); } public static void sendDebugMessage(Minecraft mc, double effectivePercent, double rawPercent, boolean hasDirectLineOfSight, double distance, double maxDistance) { + if (mc.player != null) { + mc.player.displayClientMessage(Component.literal("Concussion " + Math.round(effectivePercent) + "% / " + Math.round(rawPercent) + "%").withStyle(pickColor(effectivePercent)), true); + } } public static double computePercent(float power, double distance, boolean explosionInCave) { @@ -34,7 +103,7 @@ public class ConcussionAudioEffect { } public static double computeMaxDistance(float power, boolean explosionInCave) { - return explosionInCave ? Math.max(10.0, power * 4.0) : Math.max(5.0, power * 2.5); + return explosionInCave ? Math.max(18.0, power * 8.0) : Math.max(10.0, power * 5.0); } public static double applyOcclusion(double percent) { @@ -42,7 +111,7 @@ public class ConcussionAudioEffect { } public static double computeBaseSilentSeconds(double effectivePercent, boolean explosionInCave) { - return Math.max(0.0, effectivePercent / 25.0); + return Math.max(0.0, effectivePercent / (explosionInCave ? 16.0 : 24.0)); } public static double computeBaseIntensityPercent(double effectivePercent) { @@ -54,31 +123,31 @@ public class ConcussionAudioEffect { } public static double computeLowpassPowerChance(float power, boolean explosionInCave) { - return 0.0; + return Math.min(1.0, (power / (explosionInCave ? 18.0 : 28.0)) * ((Double)Config.CLIENT.lowPassChanceMultiplier.get()).doubleValue()); } public static double computePowerChance(float power, boolean explosionInCave) { - return 0.0; + return Math.min(1.0, (power / (explosionInCave ? 14.0 : 24.0)) * ((Double)Config.CLIENT.deafnessChanceMultiplier.get()).doubleValue()); } public static double computeBaseBlurSilentSeconds(double effectivePercent, boolean explosionInCave) { - return 0.0; + return computeBaseSilentSeconds(effectivePercent, explosionInCave) * 0.65; } public static double computeBlurPowerMultiplier(float power) { - return 1.0; + return Math.max(1.0, power / 16.0); } public static double computeBaseBlurIntensityPercent(double effectivePercent) { - return 0.0; + return Math.min(100.0, effectivePercent * 0.8); } public static double computeBaseSwayIntensityPercent(double effectivePercent) { - return 0.0; + return Math.min(100.0, effectivePercent * 0.75); } public static double computeBaseLowpassIntensityPercent(double effectivePercent) { - return 0.0; + return Math.min(100.0, effectivePercent * 0.9); } public static ChatFormatting pickColor(double percent) { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java b/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java index 862b5b5..4307007 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/CustomGlowParticle.java @@ -48,7 +48,6 @@ public class CustomGlowParticle extends TextureSheetParticle { return; } float progress = this.age / (float)this.lifetime; - this.setSpriteFromAge(this.sprites); this.alpha = Mth.clamp(1.0f - progress, 0.0f, 1.0f) * 0.92f; this.quadSize = this.baseSize * (0.8f + progress * 1.8f); } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/DeafnessConcussionEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/DeafnessConcussionEffect.java index 78675e6..5b6e0c6 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/DeafnessConcussionEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/DeafnessConcussionEffect.java @@ -5,24 +5,48 @@ import net.minecraft.client.Minecraft; public class DeafnessConcussionEffect { public static boolean debugShowChat = false; public static volatile boolean enabled = true; + private static int ticksRemaining; + private static int totalTicks; + private static float intensity; public static boolean start(float intensity, double silentSeconds, double effectivePercent, String visibility, int intensityPercent) { - return false; + if (!enabled) { + return false; + } + DeafnessConcussionEffect.intensity = Math.max(DeafnessConcussionEffect.intensity, intensity); + totalTicks = Math.max(1, (int)Math.round(Math.max(1.0, silentSeconds) * 20.0)); + ticksRemaining = Math.max(ticksRemaining, totalTicks); + LowPassConcussionEffect.setDeafnessGain((float)Math.max(0.18, 1.0 - intensity * 0.75)); + return true; } public static void stop() { + ticksRemaining = 0; + intensity = 0.0f; + resetVolume(); } public static boolean isActive() { - return false; + return enabled && ticksRemaining > 0 && intensity > 0.01f; } public static void resetVolume() { + LowPassConcussionEffect.setDeafnessGain(1.0f); } public static void applyMasterVolume(Minecraft mc, double volume) { } + public static void onClientTick() { + if (ticksRemaining <= 0) { + stop(); + return; + } + --ticksRemaining; + double remaining = ticksRemaining / Math.max(1.0, totalTicks); + LowPassConcussionEffect.setDeafnessGain((float)Math.max(0.18, lerp(1.0, 1.0 - intensity * 0.75, easeOutQuad(remaining)))); + } + public static double lerp(double a, double b, double t) { return a + (b - a) * t; } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/FirstTimeScreen.java b/src/main/java/com/vinlanx/explosionoverhaul/client/FirstTimeScreen.java index 2b14977..0b51c81 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/FirstTimeScreen.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/FirstTimeScreen.java @@ -15,9 +15,9 @@ public class FirstTimeScreen extends Screen { private static final int BORDER = 0xFFFF7A70; private static final int BORDER_HOVER = 0xFFFF9B91; private static final Choice[] CHOICES = new Choice[] { - new Choice("gui_screen_1.png", 10, 18, "REALISTIC", Config.Client.ParticleRenderMode.REALISTIC), - new Choice("gui_screen_2.png", 8, 18, "VANILLA-LIKE", Config.Client.ParticleRenderMode.VANILA), - new Choice("gui_screen_3.png", 28, 35, "REALISTIC 2", Config.Client.ParticleRenderMode.REALISTIC_2) + new Choice("gui_screen_1.png", 0, 18, "REALISTIC", Config.Client.ParticleRenderMode.REALISTIC), + new Choice("gui_screen_2.png", 0, 18, "VANILLA-LIKE", Config.Client.ParticleRenderMode.VANILA), + new Choice("gui_screen_3.png", 0, 35, "REALISTIC 2", Config.Client.ParticleRenderMode.REALISTIC_2) }; private final Rect[] cardRects = new Rect[] { Rect.empty(), Rect.empty(), Rect.empty() }; @@ -38,15 +38,15 @@ public class FirstTimeScreen extends Screen { graphics.drawCenteredString(this.font, Component.literal("You can change this anytime in the config"), this.width / 2, top + 25, 0xFFE5E5E5); graphics.drawCenteredString(this.font, Component.literal("The game may differ from the animations because compression has altered them."), this.width / 2, top + 48, 0xFF9C9C9C); - int availableCardH = Math.max(54, (this.height - top - 150) / 2); - int cardW = Math.min(300, Math.max(120, (this.width - 190) / 2)); + int availableCardH = Math.max(54, (this.height - top - 178) / 2); + int cardW = Math.min(260, Math.max(110, (this.width - 220) / 2)); cardW = Math.min(cardW, availableCardH * FRAME_WIDTH / FRAME_HEIGHT); int cardH = cardW * FRAME_HEIGHT / FRAME_WIDTH; int gap = Math.max(34, this.width / 28); int firstX = this.width / 2 - cardW - gap / 2; int secondX = this.width / 2 + gap / 2; int firstRowY = top + 70; - int secondRowY = firstRowY + cardH + 40; + int secondRowY = firstRowY + cardH + 34; this.cardRects[0] = new Rect(firstX, firstRowY, cardW, cardH + 34); this.cardRects[1] = new Rect(secondX, firstRowY, cardW, cardH + 34); diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/GuideSlidesScreen.java b/src/main/java/com/vinlanx/explosionoverhaul/client/GuideSlidesScreen.java index 91ecd96..9932c4f 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/GuideSlidesScreen.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/GuideSlidesScreen.java @@ -41,8 +41,8 @@ public class GuideSlidesScreen extends Screen { this.drawAmbient(graphics); int imageY = Math.max(28, this.height / 9); - int maxImageH = Math.max(96, this.height - imageY - 135); - int imageW = Math.min(Math.min(this.width - 290, 860), maxImageH * FRAME_WIDTH / FRAME_HEIGHT); + int maxImageH = Math.max(90, this.height - imageY - 185); + int imageW = Math.min(Math.min(this.width - 360, 820), maxImageH * FRAME_WIDTH / FRAME_HEIGHT); imageW = Math.max(180, imageW); int imageH = imageW * FRAME_HEIGHT / FRAME_WIDTH; int imageX = (this.width - imageW) / 2; @@ -61,7 +61,7 @@ public class GuideSlidesScreen extends Screen { int textY = imageY + imageH + 24; int textBottom = this.drawCenteredWrappedText(graphics, slide.text, this.width / 2, textY, textWidth, 0xFFFFFFFF); - int arrowSize = 34; + int arrowSize = 24; int arrowY = imageY + imageH / 2 - arrowSize / 2; this.leftArrow = new Rect(Math.max(18, imageX / 2 - arrowSize / 2), arrowY, arrowSize, arrowSize); this.rightArrow = new Rect(Math.min(this.width - arrowSize - 18, imageX + imageW + (this.width - imageX - imageW) / 2 - arrowSize / 2), arrowY, arrowSize, arrowSize); @@ -72,9 +72,9 @@ public class GuideSlidesScreen extends Screen { this.drawArrow(graphics, this.rightArrow, ">", this.rightArrow.contains(mouseX, mouseY)); } - int skipW = 96; - int skipH = 24; - int skipY = Math.min(Math.max(textBottom + 10, this.height - skipH - 18), this.height - skipH - 8); + int skipW = 78; + int skipH = 20; + int skipY = Math.min(Math.max(textBottom + 14, this.height - skipH - 14), this.height - skipH - 6); this.skipButton = new Rect((this.width - skipW) / 2, skipY, skipW, skipH); this.drawFlatButton(graphics, this.skipButton, "Skip Guide", this.skipButton.contains(mouseX, mouseY)); } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/LowPassConcussionEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/LowPassConcussionEffect.java index 51eba9a..0a797ac 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/LowPassConcussionEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/LowPassConcussionEffect.java @@ -3,21 +3,45 @@ package com.vinlanx.explosionoverhaul.client; public class LowPassConcussionEffect { public static boolean debugShowChat = false; public static volatile boolean enabled = true; + private static int ticksRemaining; + private static int totalTicks; + private static float targetIntensity; + private static float currentHfMultiplier = 1.0f; + private static float currentGainMultiplier = 1.0f; + private static boolean compatibilityMode; public static void start(int durationSeconds, float intensity) { + start(durationSeconds, intensity, intensity * 100.0, "direct", Math.round(intensity * 100.0f)); } public static void start(int durationSeconds, float intensity, double effectivePercent, String visibility, int intensityPercent) { + if (!enabled) { + return; + } + totalTicks = Math.max(1, durationSeconds * 20); + ticksRemaining = Math.max(ticksRemaining, totalTicks); + targetIntensity = Math.max(targetIntensity, Math.max(0.0f, intensity)); + updateMultipliers(); } public static void stop() { + ticksRemaining = 0; + targetIntensity = 0.0f; + currentHfMultiplier = 1.0f; + currentGainMultiplier = 1.0f; } public static boolean isActive() { - return false; + return enabled && ticksRemaining > 0 && targetIntensity > 0.01f; } public static void onClientTick() { + if (ticksRemaining <= 0) { + stop(); + return; + } + --ticksRemaining; + updateMultipliers(); } public static void ensureFilterExists() { @@ -30,20 +54,24 @@ public class LowPassConcussionEffect { } public static void applyFilterParamsOnAudioThread(float hf, float gain) { + currentHfMultiplier = hf; + currentGainMultiplier = gain; } public static void setCompatibilityMode(boolean enabled) { + compatibilityMode = enabled; } public static void setDeafnessGain(float gain) { + currentGainMultiplier = Math.min(currentGainMultiplier, gain); } public static float getCurrentHfMultiplier() { - return 1.0f; + return currentHfMultiplier; } public static float getCurrentGainMultiplier() { - return 1.0f; + return currentGainMultiplier; } public static float lerp(float a, float b, double t) { @@ -57,4 +85,12 @@ public class LowPassConcussionEffect { public static float easeInQuad(double t) { return (float)(t * t); } + + private static void updateMultipliers() { + double remaining = ticksRemaining / Math.max(1.0, totalTicks); + double curve = easeOutQuad(Math.max(0.0, Math.min(1.0, remaining))); + float amount = (float)(targetIntensity * curve); + currentHfMultiplier = lerp(1.0f, compatibilityMode ? 0.45f : 0.18f, amount); + currentGainMultiplier = lerp(1.0f, 0.55f, amount); + } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java b/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java index d957ab4..b1ec6f0 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/client/PhysicsBasedExplosionEffect.java @@ -1,15 +1,87 @@ 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 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 final Vec3 position; + private final float power; + private final int lifetime; + private int age; + public PhysicsBasedExplosionEffect(Vec3 position, float power) { + this.position = position; + this.power = power; + this.lifetime = Mth.clamp(18 + Math.round(power * 1.4f), 22, 90); + this.spawnInitialBurst(); } public void tick() { + ++this.age; + ClientLevel level = Minecraft.getInstance().level; + if (level == null) { + return; + } + float progress = this.age / (float)this.lifetime; + if (progress < 0.45f) { + int count = Math.max(2, Math.round(this.power * 0.7f)); + for (int i = 0; i < count; ++i) { + Vec3 velocity = randomDirection(level).scale(0.05 + level.random.nextDouble() * 0.18); + level.addParticle(new CustomGlowParticleOptions(i % 2, this.power, 0.75f + level.random.nextFloat() * 0.75f, i % 3, (float)this.position.y, this.power * 2.2f, progress), this.position.x, this.position.y, this.position.z, velocity.x, Math.abs(velocity.y) * 0.45, velocity.z); + } + } + if (progress < 0.8f && ((Boolean)Config.CLIENT.enablePlasmaParticles.get()).booleanValue()) { + int count = Math.max(1, Math.round(this.power * 0.28f)); + for (int i = 0; i < count; ++i) { + Vec3 velocity = randomDirection(level).scale(0.18 + level.random.nextDouble() * 0.7); + level.addParticle(new PlasmaParticleOptions(this.power), this.position.x, this.position.y, this.position.z, velocity.x, velocity.y, velocity.z); + } + } } public boolean isFinished() { - return true; + return this.age >= this.lifetime; + } + + private void spawnInitialBurst() { + ClientLevel level = Minecraft.getInstance().level; + if (level == null) { + return; + } + Config.Client.ParticleRenderMode mode = Config.CLIENT.particleRenderMode.get(); + int glowCount = switch (mode) { + case REALISTIC -> Math.round(this.power * 10.0f); + case REALISTIC_2 -> Math.round(this.power * 12.0f); + case VANILA -> Math.round(this.power * 6.0f); + }; + glowCount = Mth.clamp(glowCount, 24, 260); + for (int i = 0; i < glowCount; ++i) { + Vec3 velocity = randomDirection(level).scale(0.04 + level.random.nextDouble() * 0.22); + level.addParticle(new CustomGlowParticleOptions(i % 2, this.power, 0.75f + level.random.nextFloat() * 0.9f, i % 3, (float)this.position.y, this.power * 2.2f, 0.0f), this.position.x, this.position.y, this.position.z, velocity.x, velocity.y * 0.35, velocity.z); + } + int smokeCount = Mth.clamp(Math.round(this.power * 7.0f), 18, 180); + for (int i = 0; i < smokeCount; ++i) { + Vec3 velocity = randomDirection(level).scale(0.02 + level.random.nextDouble() * 0.12); + level.addParticle(new SmokeParticleOptions(0.8f + level.random.nextFloat() * Math.max(1.6f, this.power * 0.28f), 60 + level.random.nextInt(60), 0.18f, 0.16f, 0.13f, 0.62f, true, 0.0f, 0.2f, null), this.position.x, this.position.y + 0.1, this.position.z, velocity.x, Math.abs(velocity.y) * 0.25 + 0.02, velocity.z); + } + int sparks = Mth.clamp(Math.round(this.power * 5.0f), 10, 140); + for (int i = 0; i < sparks; ++i) { + Vec3 velocity = randomDirection(level).scale(0.28 + level.random.nextDouble() * 1.1); + level.addParticle(ModParticles.LINE_SPARK.get(), this.position.x, this.position.y + 0.15, this.position.z, velocity.x, Math.abs(velocity.y) * 0.5, velocity.z); + } + } + + private static Vec3 randomDirection(ClientLevel level) { + double yaw = level.random.nextDouble() * Math.PI * 2.0; + double y = level.random.nextDouble() * 0.9 - 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); } } diff --git a/src/main/java/com/vinlanx/explosionoverhaul/compat/network/NetworkEvent.java b/src/main/java/com/vinlanx/explosionoverhaul/compat/network/NetworkEvent.java index d320cf3..1545b8b 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/compat/network/NetworkEvent.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/compat/network/NetworkEvent.java @@ -7,12 +7,22 @@ public final class NetworkEvent { } public static class Context { + private final ServerPlayer sender; + + public Context() { + this(null); + } + + public Context(ServerPlayer sender) { + this.sender = sender; + } + public void enqueueWork(Runnable runnable) { runnable.run(); } public ServerPlayer getSender() { - return null; + return this.sender; } public void setPacketHandled(boolean handled) { diff --git a/src/main/java/com/vinlanx/explosionoverhaul/compat/network/simple/SimpleChannel.java b/src/main/java/com/vinlanx/explosionoverhaul/compat/network/simple/SimpleChannel.java index e10d483..cec2465 100644 --- a/src/main/java/com/vinlanx/explosionoverhaul/compat/network/simple/SimpleChannel.java +++ b/src/main/java/com/vinlanx/explosionoverhaul/compat/network/simple/SimpleChannel.java @@ -1,18 +1,119 @@ package com.vinlanx.explosionoverhaul.compat.network.simple; +import com.vinlanx.explosionoverhaul.ExplosionOverhaul; import com.vinlanx.explosionoverhaul.compat.network.NetworkEvent; +import io.netty.buffer.Unpooled; +import java.util.HashMap; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; public class SimpleChannel { + private static final int MAX_PAYLOAD_SIZE = 1_048_576; + private final Map> registrationsById = new HashMap<>(); + private final Map, Integer> idsByType = new HashMap<>(); + public void registerMessage(int id, Class messageType, BiConsumer encoder, Function decoder, BiConsumer> handler) { + MessageRegistration registration = new MessageRegistration<>(messageType, encoder, decoder, handler); + this.registrationsById.put(id, registration); + this.idsByType.put(messageType, id); + } + + public void registerPayloads(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1"); + registrar.playBidirectional(LegacyPayload.TYPE, LegacyPayload.STREAM_CODEC, this::handlePayload); } public void send(Object target, Object message) { + LegacyPayload payload = this.encodePayload(message); + if (payload == null) { + return; + } + if (target instanceof ServerPlayer player) { + PacketDistributor.sendToPlayer(player, payload); + return; + } + PacketDistributor.sendToAllPlayers(payload); } public void sendToServer(Object message) { + LegacyPayload payload = this.encodePayload(message); + if (payload != null) { + ClientPacketDistributor.sendToServer(payload); + } + } + + private LegacyPayload encodePayload(Object message) { + Integer id = this.idsByType.get(message.getClass()); + if (id == null) { + ExplosionOverhaul.LOGGER.warn("Tried to send unregistered legacy packet {}", message.getClass().getName()); + return null; + } + MessageRegistration registration = this.registration(id); + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + registration.encoder.accept(message, buffer); + byte[] data = new byte[buffer.readableBytes()]; + buffer.readBytes(data); + return new LegacyPayload(id, data); + } + + private void handlePayload(LegacyPayload payload, IPayloadContext context) { + MessageRegistration registration = this.registration(payload.messageId()); + if (registration == null) { + ExplosionOverhaul.LOGGER.warn("Received unknown legacy packet id {}", payload.messageId()); + return; + } + Player contextPlayer; + try { + contextPlayer = context.player(); + } catch (Exception ignored) { + contextPlayer = null; + } + ServerPlayer sender = contextPlayer instanceof ServerPlayer serverPlayer ? serverPlayer : null; + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.wrappedBuffer(payload.data())); + Object decoded = registration.decoder.apply(buffer); + NetworkEvent.Context legacyContext = new NetworkEvent.Context(sender); + registration.handler.accept(decoded, () -> legacyContext); + } + + @SuppressWarnings("unchecked") + private MessageRegistration registration(int id) { + return (MessageRegistration)this.registrationsById.get(id); + } + + private record MessageRegistration( + Class messageType, + BiConsumer encoder, + Function decoder, + BiConsumer> handler) { + } + + public record LegacyPayload(int messageId, byte[] data) implements CustomPacketPayload { + public static final CustomPacketPayload.Type TYPE = + new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath(ExplosionOverhaul.MODID, "legacy_packet")); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buffer, value) -> { + buffer.writeVarInt(value.messageId); + buffer.writeByteArray(value.data); + }, + buffer -> new LegacyPayload(buffer.readVarInt(), buffer.readByteArray(MAX_PAYLOAD_SIZE))); + + @Override + public Type type() { + return TYPE; + } } } diff --git a/src/main/resources/assets/explosionoverhaul/blockstates/vinlanx_the_light.json b/src/main/resources/assets/explosionoverhaul/blockstates/vinlanx_the_light.json index c355a90..2b2cb7e 100644 --- a/src/main/resources/assets/explosionoverhaul/blockstates/vinlanx_the_light.json +++ b/src/main/resources/assets/explosionoverhaul/blockstates/vinlanx_the_light.json @@ -1,8 +1,5 @@ { "variants": { - "facing=north": { "model": "explosionoverhaul:block/vinlanx_the_light" }, - "facing=south": { "model": "explosionoverhaul:block/vinlanx_the_light", "y": 180 }, - "facing=east": { "model": "explosionoverhaul:block/vinlanx_the_light", "y": 90 }, - "facing=west": { "model": "explosionoverhaul:block/vinlanx_the_light", "y": 270 } + "": { "model": "explosionoverhaul:block/vinlanx_the_light" } } } diff --git a/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json b/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json index 97d0a55..abe91f7 100644 --- a/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json +++ b/src/main/resources/assets/explosionoverhaul/particles/custom_glow.json @@ -1,54 +1,6 @@ { "textures": [ "explosionoverhaul:soft_glow", - "explosionoverhaul:soft_glow_e", - "explosionoverhaul:glow/glow_sheet_1", - "explosionoverhaul:glow/glow_sheet_2", - "explosionoverhaul:glow/glow_sheet_3", - "explosionoverhaul:glow/glow_sheet_4", - "explosionoverhaul:glow_2/glow_2_sheet_1", - "explosionoverhaul:glow_2/glow_2_sheet_2", - "explosionoverhaul:glow_2/glow_2_sheet_3", - "explosionoverhaul:glow_2/glow_2_sheet_4", - "explosionoverhaul:sglow/sglow_sheet_1", - "explosionoverhaul:sglow/sglow_sheet_2", - "explosionoverhaul:sglow/sglow_sheet_3", - "explosionoverhaul:sglow/sglow_sheet_4", - "explosionoverhaul:glow/glow_e_sheet_1", - "explosionoverhaul:glow/glow_e_sheet_2", - "explosionoverhaul:glow/glow_e_sheet_3", - "explosionoverhaul:glow/glow_e_sheet_4", - "explosionoverhaul:glow_2/glow_2_e_sheet_1", - "explosionoverhaul:glow_2/glow_2_e_sheet_2", - "explosionoverhaul:glow_2/glow_2_e_sheet_3", - "explosionoverhaul:glow_2/glow_2_e_sheet_4", - "explosionoverhaul:sglow/sglow_e_sheet_1", - "explosionoverhaul:sglow/sglow_e_sheet_2", - "explosionoverhaul:sglow/sglow_e_sheet_3", - "explosionoverhaul:sglow/sglow_e_sheet_4", - "explosionoverhaul:glow/64/glow_sheet_1", - "explosionoverhaul:glow/64/glow_sheet_2", - "explosionoverhaul:glow/64/glow_sheet_3", - "explosionoverhaul:glow/64/glow_sheet_4", - "explosionoverhaul:glow_2/64/glow_2_sheet_1", - "explosionoverhaul:glow_2/64/glow_2_sheet_2", - "explosionoverhaul:glow_2/64/glow_2_sheet_3", - "explosionoverhaul:glow_2/64/glow_2_sheet_4", - "explosionoverhaul:sglow/64/sglow_sheet_1", - "explosionoverhaul:sglow/64/sglow_sheet_2", - "explosionoverhaul:sglow/64/sglow_sheet_3", - "explosionoverhaul:sglow/64/sglow_sheet_4", - "explosionoverhaul:glow/64/glow_e_sheet_1", - "explosionoverhaul:glow/64/glow_e_sheet_2", - "explosionoverhaul:glow/64/glow_e_sheet_3", - "explosionoverhaul:glow/64/glow_e_sheet_4", - "explosionoverhaul:glow_2/64/glow_2_e_sheet_1", - "explosionoverhaul:glow_2/64/glow_2_e_sheet_2", - "explosionoverhaul:glow_2/64/glow_2_e_sheet_3", - "explosionoverhaul:glow_2/64/glow_2_e_sheet_4", - "explosionoverhaul:sglow/64/sglow_e_sheet_1", - "explosionoverhaul:sglow/64/sglow_e_sheet_2", - "explosionoverhaul:sglow/64/sglow_e_sheet_3", - "explosionoverhaul:sglow/64/sglow_e_sheet_4" + "explosionoverhaul:soft_glow_e" ] }