Restore explosion parity systems
Some checks failed
Build / build (push) Failing after 6m22s

This commit is contained in:
MrSphay
2026-05-09 14:18:13 +02:00
parent 1ce8376c1b
commit 66bdfab2d6
27 changed files with 1466 additions and 164 deletions

View File

@@ -18,7 +18,7 @@ minecraft_version=1.21.1
# as they do not follow standard versioning conventions. # as they do not follow standard versioning conventions.
minecraft_version_range=[1.21.1] minecraft_version_range=[1.21.1]
# The Neo version must agree with the Minecraft version to get a valid artifact # 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 # The loader version range can only use the major version of FML as bounds
loader_version_range=[1,) loader_version_range=[1,)

View File

@@ -1,20 +1,152 @@
package com.vinlanx.explosionoverhaul; 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.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.Vec3;
public class AmbientExplosionManager { public class AmbientExplosionManager {
private static final Map<ServerPlayer, Integer> PLAYER_TIMERS = new HashMap<>();
private static final Map<ServerPlayer, AmbientShot> PENDING_SHOTS = new HashMap<>();
public static void onServerTick(MinecraftServer server) { 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<Map.Entry<ServerPlayer, Integer>> timers = PLAYER_TIMERS.entrySet().iterator();
while (timers.hasNext()) {
Map.Entry<ServerPlayer, Integer> 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<Map.Entry<ServerPlayer, AmbientShot>> shots = PENDING_SHOTS.entrySet().iterator();
while (shots.hasNext()) {
Map.Entry<ServerPlayer, AmbientShot> entry = shots.next();
if (entry.getValue().tick(entry.getKey())) {
shots.remove();
}
}
} }
public static void onPlayerLoggedIn(ServerPlayer player) { public static void onPlayerLoggedIn(ServerPlayer player) {
PLAYER_TIMERS.put(player, nextDelay(player));
} }
public static void onPlayerLoggedOut(ServerPlayer 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) { 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;
}
} }
} }

View File

@@ -1,19 +1,89 @@
package com.vinlanx.explosionoverhaul; 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.MinecraftServer;
import net.minecraft.server.level.ServerLevel; 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; import net.minecraft.world.phys.Vec3;
public class AsyncCraterManager { public class AsyncCraterManager {
private static final Queue<CraterJob> JOBS = new ArrayDeque<>();
public static void submit(ServerLevel level, Vec3 pos, float power) { public static void submit(ServerLevel level, Vec3 pos, float power) {
List<BlockPos> blocks = new ArrayList<>(CraterDeformer.getCraterBlocks(level, pos, power));
synchronized (JOBS) {
JOBS.add(new CraterJob(level, pos, power, blocks));
}
} }
public static void onServerTick(MinecraftServer server) { public static void onServerTick(MinecraftServer server) {
int budget = Math.max(1, (Integer)Config.COMMON.craterApplyBlocksPerTick.get());
synchronized (JOBS) {
Iterator<CraterJob> 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) { public static void onLevelUnload(ServerLevel level) {
synchronized (JOBS) {
JOBS.removeIf(job -> job.level == level);
}
} }
public static void shutdown() { 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<BlockPos> blocks;
private int index;
private int debrisSpawned;
private CraterJob(ServerLevel level, Vec3 origin, float power, List<BlockPos> 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();
}
} }
} }

View File

@@ -1,70 +1,114 @@
package com.vinlanx.explosionoverhaul; package com.vinlanx.explosionoverhaul;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List; 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.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.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; 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; import net.minecraft.world.level.block.state.BlockState;
public class BlockIndexManager { public class BlockIndexManager {
public static volatile boolean ENABLED = false; public static volatile boolean ENABLED = false;
private static final Map<ResourceKey<Level>, EnumMap<BlockType, Set<BlockPos>>> 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() { public static int getTotalChunksToScan() {
return 0; return totalChunksToScan;
} }
public static int getChunksScanned() { public static int getChunksScanned() {
return 0; return chunksScanned;
} }
public static float getScanProgress() { 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() { public static boolean isScanningComplete() {
return true; return !scanning;
} }
public static int getLampsFound() { public static int getLampsFound() {
return 0; return count(BlockType.LAMP);
} }
public static int getDripstonesFound() { public static int getDripstonesFound() {
return 0; return count(BlockType.DRIPSTONE);
} }
public static int getGlassBlocksFound() { public static int getGlassBlocksFound() {
return 0; return count(BlockType.GLASS);
} }
public static void startManualScan() { public static void startManualScan() {
rescanMode = true;
scanning = true;
} }
public static void loadExistingData() { public static void loadExistingData() {
scanning = false;
} }
public static void startNewScan() { public static void startNewScan() {
resetScanState();
scanning = true;
} }
public static void cancelManualScan() { public static void cancelManualScan() {
scanning = false;
} }
public static void resetScanState() { public static void resetScanState() {
INDEX.clear();
totalChunksToScan = 0;
chunksScanned = 0;
scanning = false;
rescanMode = false;
} }
public static void forceServerScan(MinecraftServer server) { 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) { public static void forceSingleplayerScan(ServerLevel level) {
startNewScan();
scanLoadedLevel(level);
scanning = false;
} }
public static void showRescanPrompt(ServerPlayer player) { 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) { 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<String> getReinforcedGlassBlacklist() { public static List<String> getReinforcedGlassBlacklist() {
@@ -90,21 +134,109 @@ public class BlockIndexManager {
} }
public static boolean isRescanMode() { public static boolean isRescanMode() {
return false; return rescanMode;
} }
public static void register(ServerLevel level, BlockPos pos, BlockType type) { 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) { public static void unregister(ServerLevel level, BlockPos pos, BlockType type) {
bucket(level, type).remove(pos);
} }
public static List<BlockPos> getNearby(ServerLevel level, BlockPos center, int radius, BlockType type) { public static List<BlockPos> 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<BlockPos> 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) { 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<BlockType, Set<BlockPos>> map = new EnumMap<>(BlockType.class);
for (BlockType type : BlockType.values()) {
map.put(type, ConcurrentHashMap.newKeySet());
}
INDEX.put(level.dimension(), map);
List<ServerPlayer> players = level.players();
int horizontalRadius = players.isEmpty() ? 192 : 256;
Set<BlockPos> 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<BlockPos> scanNearby(ServerLevel level, BlockPos center, int radius, BlockType type) {
List<BlockPos> 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<BlockPos> bucket(ServerLevel level, BlockType type) {
EnumMap<BlockType, Set<BlockPos>> map = INDEX.computeIfAbsent(level.dimension(), ignored -> {
EnumMap<BlockType, Set<BlockPos>> 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<BlockType, Set<BlockPos>> map : INDEX.values()) {
count += map.getOrDefault(type, Collections.emptySet()).size();
}
return count;
} }
public enum BlockType { public enum BlockType {

View File

@@ -3,11 +3,14 @@
*/ */
package com.vinlanx.explosionoverhaul; package com.vinlanx.explosionoverhaul;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.neoforge.client.event.sound.PlaySoundEvent; import net.neoforged.neoforge.client.event.sound.PlaySoundEvent;
import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.bus.api.EventPriority; import net.neoforged.bus.api.EventPriority;
import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
@EventBusSubscriber(modid = ExplosionOverhaul.MODID, value = Dist.CLIENT)
public class ClientSoundHandler { public class ClientSoundHandler {
private static boolean suppressVanillaExplosionSound = false; private static boolean suppressVanillaExplosionSound = false;
private static int suppressionTicksRemaining = 0; private static int suppressionTicksRemaining = 0;
@@ -18,7 +21,7 @@ public class ClientSoundHandler {
} }
@SubscribeEvent @SubscribeEvent
public void onClientTick(ClientTickEvent.Post event) { public static void onClientTick(ClientTickEvent.Post event) {
if (suppressionTicksRemaining > 0) { if (suppressionTicksRemaining > 0) {
--suppressionTicksRemaining; --suppressionTicksRemaining;
} else if (suppressionTicksRemaining == 0 && suppressVanillaExplosionSound) { } else if (suppressionTicksRemaining == 0 && suppressVanillaExplosionSound) {
@@ -27,7 +30,7 @@ public class ClientSoundHandler {
} }
@SubscribeEvent(priority=EventPriority.HIGH) @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) { if (event.getSound() != null && event.getSound().getLocation().getPath().equals("entity.generic.explode") && suppressVanillaExplosionSound) {
event.setSound(null); event.setSound(null);
} }

View File

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

View File

@@ -1,27 +1,95 @@
package com.vinlanx.explosionoverhaul; package com.vinlanx.explosionoverhaul;
import java.util.Collections; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel; 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; import net.minecraft.world.phys.Vec3;
public class CraterDeformer { public class CraterDeformer {
public static Set<BlockPos> getCraterBlocks(ServerLevel level, Vec3 explosionPos, float power) { public static Set<BlockPos> getCraterBlocks(ServerLevel level, Vec3 explosionPos, float power) {
return Collections.emptySet(); Set<BlockPos> 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) { 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) { 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) { 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) { 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;
} }
} }

View File

@@ -16,12 +16,16 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import net.minecraft.core.particles.BlockParticleOption; import net.minecraft.core.particles.BlockParticleOption;
import net.minecraft.core.registries.BuiltInRegistries; 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.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource; import net.minecraft.sounds.SoundSource;
import net.minecraft.world.level.block.Block; 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.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import net.neoforged.bus.api.IEventBus; 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 CONFIG_DIR = Paths.get("config", "explosionoverhaul");
private static final Path BLACKLIST_JSON = CONFIG_DIR.resolve("DestroyingBlacklist.json"); 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 Path SOURCE_MODES_JSON = CONFIG_DIR.resolve("ExplosionSourceBlacklist.json");
private static final List<DelayedSound> DELAYED_SOUNDS = new ArrayList<>();
private static final List<DelayedParticle> DELAYED_PARTICLES = new ArrayList<>();
public ExplosionOverhaul(IEventBus modEventBus, ModContainer modContainer) { public ExplosionOverhaul(IEventBus modEventBus, ModContainer modContainer) {
modContainer.registerConfig(ModConfig.Type.COMMON, Config.COMMON_SPEC, "explosionoverhaul/explosionoverhaul-common.toml"); modContainer.registerConfig(ModConfig.Type.COMMON, Config.COMMON_SPEC, "explosionoverhaul/explosionoverhaul-common.toml");
@@ -77,6 +83,7 @@ public class ExplosionOverhaul {
ModItems.register(modEventBus); ModItems.register(modEventBus);
ModBlockEntities.register(modEventBus); ModBlockEntities.register(modEventBus);
ModCreativeTabs.register(modEventBus); ModCreativeTabs.register(modEventBus);
modEventBus.addListener(PacketHandler::registerPayloads);
modEventBus.addListener(this::commonSetup); 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) { 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) { 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 { public enum ExplosionSourceMode {
@@ -209,6 +237,82 @@ public class ExplosionOverhaul {
public static class CaveEffects { public static class CaveEffects {
public static void spawnFallingBlocksAndDust(ServerLevel level, Vec3 explosionPos, ServerPlayer player, float power, long initialDelayTicks) { 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;
} }
} }
} }

View File

@@ -29,6 +29,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import com.vinlanx.explosionoverhaul.compat.network.NetworkRegistry; import com.vinlanx.explosionoverhaul.compat.network.NetworkRegistry;
import com.vinlanx.explosionoverhaul.compat.network.simple.SimpleChannel; import com.vinlanx.explosionoverhaul.compat.network.simple.SimpleChannel;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
public class PacketHandler { public class PacketHandler {
private static final String PROTOCOL_VERSION = "1"; 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); 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) { public static void sendToPlayer(ServerPlayer player, Object message) {
INSTANCE.send(null, message); INSTANCE.send(player, message);
} }
public static void sendToAll(Object message) { public static void sendToAll(Object message) {

View File

@@ -1,5 +1,8 @@
package com.vinlanx.explosionoverhaul; package com.vinlanx.explosionoverhaul;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
@@ -10,36 +13,95 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
public class RedstoneLampEffects { public class RedstoneLampEffects {
private static final List<FlickerTask> TASKS = new ArrayList<>();
public static void triggerLampFlicker(ServerLevel level, ServerPlayer player, float power, long delayTicks, double distance) { 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) { 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()) { if (!((Boolean)Config.COMMON.enableLampFlicker.get()).booleanValue()) {
return; return;
} }
int radius = Math.min((Integer)Config.COMMON.lampFlickerSearchRadius.get(), Math.max(8, Math.round(power * 6.0f))); int radius = Math.min((Integer)Config.COMMON.lampFlickerSearchRadius.get(), Math.max(8, Math.round(power * 6.0f)));
BlockPos origin = BlockPos.containing(explosionPos); BlockPos origin = BlockPos.containing(explosionPos);
int changed = 0; List<BlockPos> candidates = new ArrayList<>(BlockIndexManager.getNearby(level, origin, radius, BlockIndexManager.BlockType.LAMP));
for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-radius, -radius, -radius), origin.offset(radius, radius, radius))) { if (candidates.isEmpty()) {
if (changed >= 96) { for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-radius, -radius, -radius), origin.offset(radius, radius, radius))) {
break; candidates.add(pos.immutable());
} }
BlockState state = level.getBlockState(pos); }
if (!state.is(Blocks.REDSTONE_LAMP) || !state.hasProperty(RedstoneLampBlock.LIT)) { int scheduled = 0;
continue; 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() { public static void onServerTick() {
synchronized (TASKS) {
Iterator<FlickerTask> 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;
}
} }
} }

View File

@@ -29,7 +29,7 @@ public class ScanControlPacket {
ServerPlayer player = ((NetworkEvent.Context)ctx.get()).getSender(); ServerPlayer player = ((NetworkEvent.Context)ctx.get()).getSender();
if (player != null && BlockIndexManager.isPlayerAuthorized(player)) { if (player != null && BlockIndexManager.isPlayerAuthorized(player)) {
if (msg.startScan) { if (msg.startScan) {
BlockIndexManager.startManualScan(); BlockIndexManager.forceServerScan(player.getServer());
} else { } else {
BlockIndexManager.cancelManualScan(); BlockIndexManager.cancelManualScan();
} }

View File

@@ -31,7 +31,7 @@ public class ScanLoadControlPacket {
if (msg.loadExisting) { if (msg.loadExisting) {
BlockIndexManager.loadExistingData(); BlockIndexManager.loadExistingData();
} else { } else {
BlockIndexManager.startNewScan(); BlockIndexManager.forceServerScan(player.getServer());
} }
} }
}); });

View File

@@ -6,70 +6,237 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import net.minecraft.core.BlockPos; 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.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.Explosion;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.phys.HitResult;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
public class ServerExplosionHandler { 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<BlockPos> affectedBlocks) { public static void handleExplosion(ServerLevel level, Explosion explosion, List<BlockPos> affectedBlocks) {
if (!((Boolean)Config.COMMON.enableCraterDestruction.get()).booleanValue()) {
affectedBlocks.clear();
return;
}
float power = 4.0f; float power = 4.0f;
if (explosion instanceof IExplosionPower explosionPower) { if (explosion instanceof IExplosionPower explosionPower) {
power = Math.max(1.0f, explosionPower.getPower()); power = Math.max(1.0f, explosionPower.getPower());
} }
Vec3 center = explosion instanceof ExplosionAccessor accessor ? accessor.explosionoverhaul$getCenter() : Vec3.ZERO; 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); 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 {}", center, power);
ExplosionOverhaul.LOGGER.info("Explosion Overhaul handling explosion at {} with power {} and radius {}", center, power, radius);
dispatchPlayerEffects(level, center, power);
GlassBreakingEffects.trigger(level, center, power); GlassBreakingEffects.trigger(level, center, power);
DripstoneEffects.handleDripstoneFall(level, BlockPos.containing(center), Math.round(power), level.random); DripstoneEffects.handleDripstoneFall(level, BlockPos.containing(center), Math.round(power), level.random);
RedstoneLampEffects.triggerNearbyLamps(level, center, power); RedstoneLampEffects.triggerNearbyLamps(level, center, power);
Set<BlockPos> existing = new HashSet<>(affectedBlocks); ExplosionOverhaul.CaveEffects.spawnFallingBlocksAndDust(level, center, nearestPlayer(level, center), power, 8L);
BlockPos origin = BlockPos.containing(center);
double radiusSq = radius * radius; boolean noDestruction = sourceMode == ExplosionOverhaul.ExplosionSourceMode.NO_DESTRUCTION
int directBlocksChanged = 0; || sourceMode == ExplosionOverhaul.ExplosionSourceMode.NO_DESTRUCTION_GLASSWORKS
for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-radius, -radius, -radius), origin.offset(radius, radius, radius))) { || !((Boolean)Config.COMMON.enableCraterDestruction.get()).booleanValue();
double distanceSq = pos.distToCenterSqr(center); if (noDestruction) {
if (distanceSq > radiusSq) { affectedBlocks.clear();
continue; } else if (((Boolean)Config.COMMON.enableAsyncCrater.get()).booleanValue()) {
} affectedBlocks.clear();
BlockState state = level.getBlockState(pos); AsyncCraterManager.submit(level, center, power);
if (state.isAir() || state.getDestroySpeed(level, pos) < 0.0f) { } else {
continue; Set<BlockPos> craterBlocks = new HashSet<>(CraterDeformer.getCraterBlocks(level, center, power));
} affectedBlocks.clear();
BlockPos immutable = pos.immutable(); affectedBlocks.addAll(craterBlocks);
if (existing.add(immutable)) { if (power >= 12.0f) {
affectedBlocks.add(immutable); CraterDeformer.applyLargeExplosionLogic(level, center, power);
} } else {
if (directBlocksChanged < 2500) { CraterDeformer.applySmallExplosionLogic(level, center, power);
level.setBlock(immutable, Blocks.AIR.defaultBlockState(), 3);
++directBlocksChanged;
} }
} }
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); spawnServerParticles(level, center, power);
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);
} }
private static void spawnCustomExplosionParticles(ServerLevel level, Vec3 center, float power, int radius) { private static void dispatchPlayerEffects(ServerLevel level, Vec3 center, float power) {
int glowCount = Math.min(220, Math.max(24, radius * 9)); for (ServerPlayer player : level.players()) {
int smokeCount = Math.min(260, Math.max(32, radius * 11)); double distance = player.position().distanceTo(center);
int plasmaCount = Math.min(120, Math.max(12, radius * 5)); if (distance > Math.max(96.0, power * 90.0)) {
int sparkCount = Math.min(80, Math.max(10, radius * 3)); 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); float maxRadius = Math.max(4.0f, radius);
for (int i = 0; i < 3; ++i) { 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 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.16, radius * 0.1, radius * 0.16, 0.45); 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.22, radius * 0.16, radius * 0.22, 0.55); 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() { public static void register() {

View File

@@ -1,18 +1,44 @@
package com.vinlanx.explosionoverhaul.client; package com.vinlanx.explosionoverhaul.client;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;
public class CameraShakeConcussionEffect { public class CameraShakeConcussionEffect {
private static int ticksRemaining;
private static int totalTicks;
private static float intensity;
public static void start(int seconds, 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() { public static void stop() {
ticksRemaining = 0;
totalTicks = 0;
intensity = 0.0f;
} }
public static void onClientTick() { 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) { 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) { public static float easeInOutCubic(float t) {

View File

@@ -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());
}
}

View File

@@ -1,52 +1,247 @@
package com.vinlanx.explosionoverhaul.client; 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.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.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.entity.player.Player;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.client.event.RenderGuiEvent; import net.neoforged.neoforge.client.event.RenderGuiEvent;
public class ClientEffects { public class ClientEffects {
private static final List<DelayedSound> TRACKED_SOUNDS = new ArrayList<>();
private static final List<DelayedShake> DELAYED_SHAKES = new ArrayList<>();
private static final List<PhysicsBasedExplosionEffect> 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) { 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) { 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) { 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) { 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) { 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) { 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() { 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<PhysicsBasedExplosionEffect> 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) { public static void renderFlash(RenderGuiEvent.Post event) {
renderFlash(event.getGuiGraphics());
} }
public static void renderFlash(GuiGraphics graphics) { 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) { 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) { 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) { 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) { 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) { 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;
}
} }
} }

View File

@@ -1,28 +1,97 @@
package com.vinlanx.explosionoverhaul.client; package com.vinlanx.explosionoverhaul.client;
import com.vinlanx.explosionoverhaul.Config;
import com.vinlanx.explosionoverhaul.ModSounds;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundSource;
public class ConcussionAudioEffect { 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) { 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() { 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) { 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() { public static float getCurrentHeartbeatVisual() {
return 0.0f; return Math.max(0.0f, Math.min(1.0f, currentIntensity));
} }
public static void renderHeartbeatHUD(GuiGraphics guiGraphics) { 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) { 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) { 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) { 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) { public static double applyOcclusion(double percent) {
@@ -42,7 +111,7 @@ public class ConcussionAudioEffect {
} }
public static double computeBaseSilentSeconds(double effectivePercent, boolean explosionInCave) { 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) { public static double computeBaseIntensityPercent(double effectivePercent) {
@@ -54,31 +123,31 @@ public class ConcussionAudioEffect {
} }
public static double computeLowpassPowerChance(float power, boolean explosionInCave) { 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) { 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) { public static double computeBaseBlurSilentSeconds(double effectivePercent, boolean explosionInCave) {
return 0.0; return computeBaseSilentSeconds(effectivePercent, explosionInCave) * 0.65;
} }
public static double computeBlurPowerMultiplier(float power) { public static double computeBlurPowerMultiplier(float power) {
return 1.0; return Math.max(1.0, power / 16.0);
} }
public static double computeBaseBlurIntensityPercent(double effectivePercent) { public static double computeBaseBlurIntensityPercent(double effectivePercent) {
return 0.0; return Math.min(100.0, effectivePercent * 0.8);
} }
public static double computeBaseSwayIntensityPercent(double effectivePercent) { public static double computeBaseSwayIntensityPercent(double effectivePercent) {
return 0.0; return Math.min(100.0, effectivePercent * 0.75);
} }
public static double computeBaseLowpassIntensityPercent(double effectivePercent) { public static double computeBaseLowpassIntensityPercent(double effectivePercent) {
return 0.0; return Math.min(100.0, effectivePercent * 0.9);
} }
public static ChatFormatting pickColor(double percent) { public static ChatFormatting pickColor(double percent) {

View File

@@ -48,7 +48,6 @@ public class CustomGlowParticle extends TextureSheetParticle {
return; return;
} }
float progress = this.age / (float)this.lifetime; 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.alpha = Mth.clamp(1.0f - progress, 0.0f, 1.0f) * 0.92f;
this.quadSize = this.baseSize * (0.8f + progress * 1.8f); this.quadSize = this.baseSize * (0.8f + progress * 1.8f);
} }

View File

@@ -5,24 +5,48 @@ import net.minecraft.client.Minecraft;
public class DeafnessConcussionEffect { public class DeafnessConcussionEffect {
public static boolean debugShowChat = false; public static boolean debugShowChat = false;
public static volatile boolean enabled = true; 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) { 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() { public static void stop() {
ticksRemaining = 0;
intensity = 0.0f;
resetVolume();
} }
public static boolean isActive() { public static boolean isActive() {
return false; return enabled && ticksRemaining > 0 && intensity > 0.01f;
} }
public static void resetVolume() { public static void resetVolume() {
LowPassConcussionEffect.setDeafnessGain(1.0f);
} }
public static void applyMasterVolume(Minecraft mc, double volume) { 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) { public static double lerp(double a, double b, double t) {
return a + (b - a) * t; return a + (b - a) * t;
} }

View File

@@ -15,9 +15,9 @@ public class FirstTimeScreen extends Screen {
private static final int BORDER = 0xFFFF7A70; private static final int BORDER = 0xFFFF7A70;
private static final int BORDER_HOVER = 0xFFFF9B91; private static final int BORDER_HOVER = 0xFFFF9B91;
private static final Choice[] CHOICES = new Choice[] { private static final Choice[] CHOICES = new Choice[] {
new Choice("gui_screen_1.png", 10, 18, "REALISTIC", Config.Client.ParticleRenderMode.REALISTIC), new Choice("gui_screen_1.png", 0, 18, "REALISTIC", Config.Client.ParticleRenderMode.REALISTIC),
new Choice("gui_screen_2.png", 8, 18, "VANILLA-LIKE", Config.Client.ParticleRenderMode.VANILA), new Choice("gui_screen_2.png", 0, 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_3.png", 0, 35, "REALISTIC 2", Config.Client.ParticleRenderMode.REALISTIC_2)
}; };
private final Rect[] cardRects = new Rect[] { Rect.empty(), Rect.empty(), Rect.empty() }; 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("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); 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 availableCardH = Math.max(54, (this.height - top - 178) / 2);
int cardW = Math.min(300, Math.max(120, (this.width - 190) / 2)); int cardW = Math.min(260, Math.max(110, (this.width - 220) / 2));
cardW = Math.min(cardW, availableCardH * FRAME_WIDTH / FRAME_HEIGHT); cardW = Math.min(cardW, availableCardH * FRAME_WIDTH / FRAME_HEIGHT);
int cardH = cardW * FRAME_HEIGHT / FRAME_WIDTH; int cardH = cardW * FRAME_HEIGHT / FRAME_WIDTH;
int gap = Math.max(34, this.width / 28); int gap = Math.max(34, this.width / 28);
int firstX = this.width / 2 - cardW - gap / 2; int firstX = this.width / 2 - cardW - gap / 2;
int secondX = this.width / 2 + gap / 2; int secondX = this.width / 2 + gap / 2;
int firstRowY = top + 70; 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[0] = new Rect(firstX, firstRowY, cardW, cardH + 34);
this.cardRects[1] = new Rect(secondX, firstRowY, cardW, cardH + 34); this.cardRects[1] = new Rect(secondX, firstRowY, cardW, cardH + 34);

View File

@@ -41,8 +41,8 @@ public class GuideSlidesScreen extends Screen {
this.drawAmbient(graphics); this.drawAmbient(graphics);
int imageY = Math.max(28, this.height / 9); int imageY = Math.max(28, this.height / 9);
int maxImageH = Math.max(96, this.height - imageY - 135); int maxImageH = Math.max(90, this.height - imageY - 185);
int imageW = Math.min(Math.min(this.width - 290, 860), maxImageH * FRAME_WIDTH / FRAME_HEIGHT); int imageW = Math.min(Math.min(this.width - 360, 820), maxImageH * FRAME_WIDTH / FRAME_HEIGHT);
imageW = Math.max(180, imageW); imageW = Math.max(180, imageW);
int imageH = imageW * FRAME_HEIGHT / FRAME_WIDTH; int imageH = imageW * FRAME_HEIGHT / FRAME_WIDTH;
int imageX = (this.width - imageW) / 2; int imageX = (this.width - imageW) / 2;
@@ -61,7 +61,7 @@ public class GuideSlidesScreen extends Screen {
int textY = imageY + imageH + 24; int textY = imageY + imageH + 24;
int textBottom = this.drawCenteredWrappedText(graphics, slide.text, this.width / 2, textY, textWidth, 0xFFFFFFFF); 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; int arrowY = imageY + imageH / 2 - arrowSize / 2;
this.leftArrow = new Rect(Math.max(18, imageX / 2 - arrowSize / 2), arrowY, arrowSize, arrowSize); 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); 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)); this.drawArrow(graphics, this.rightArrow, ">", this.rightArrow.contains(mouseX, mouseY));
} }
int skipW = 96; int skipW = 78;
int skipH = 24; int skipH = 20;
int skipY = Math.min(Math.max(textBottom + 10, this.height - skipH - 18), this.height - skipH - 8); 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.skipButton = new Rect((this.width - skipW) / 2, skipY, skipW, skipH);
this.drawFlatButton(graphics, this.skipButton, "Skip Guide", this.skipButton.contains(mouseX, mouseY)); this.drawFlatButton(graphics, this.skipButton, "Skip Guide", this.skipButton.contains(mouseX, mouseY));
} }

View File

@@ -3,21 +3,45 @@ package com.vinlanx.explosionoverhaul.client;
public class LowPassConcussionEffect { public class LowPassConcussionEffect {
public static boolean debugShowChat = false; public static boolean debugShowChat = false;
public static volatile boolean enabled = true; 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) { 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) { 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() { public static void stop() {
ticksRemaining = 0;
targetIntensity = 0.0f;
currentHfMultiplier = 1.0f;
currentGainMultiplier = 1.0f;
} }
public static boolean isActive() { public static boolean isActive() {
return false; return enabled && ticksRemaining > 0 && targetIntensity > 0.01f;
} }
public static void onClientTick() { public static void onClientTick() {
if (ticksRemaining <= 0) {
stop();
return;
}
--ticksRemaining;
updateMultipliers();
} }
public static void ensureFilterExists() { public static void ensureFilterExists() {
@@ -30,20 +54,24 @@ public class LowPassConcussionEffect {
} }
public static void applyFilterParamsOnAudioThread(float hf, float gain) { public static void applyFilterParamsOnAudioThread(float hf, float gain) {
currentHfMultiplier = hf;
currentGainMultiplier = gain;
} }
public static void setCompatibilityMode(boolean enabled) { public static void setCompatibilityMode(boolean enabled) {
compatibilityMode = enabled;
} }
public static void setDeafnessGain(float gain) { public static void setDeafnessGain(float gain) {
currentGainMultiplier = Math.min(currentGainMultiplier, gain);
} }
public static float getCurrentHfMultiplier() { public static float getCurrentHfMultiplier() {
return 1.0f; return currentHfMultiplier;
} }
public static float getCurrentGainMultiplier() { public static float getCurrentGainMultiplier() {
return 1.0f; return currentGainMultiplier;
} }
public static float lerp(float a, float b, double t) { public static float lerp(float a, float b, double t) {
@@ -57,4 +85,12 @@ public class LowPassConcussionEffect {
public static float easeInQuad(double t) { public static float easeInQuad(double t) {
return (float)(t * 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);
}
} }

View File

@@ -1,15 +1,87 @@
package com.vinlanx.explosionoverhaul.client; 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; import net.minecraft.world.phys.Vec3;
public class PhysicsBasedExplosionEffect { public class PhysicsBasedExplosionEffect {
private final Vec3 position;
private final float power;
private final int lifetime;
private int age;
public PhysicsBasedExplosionEffect(Vec3 position, float power) { 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() { 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() { 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);
} }
} }

View File

@@ -7,12 +7,22 @@ public final class NetworkEvent {
} }
public static class Context { public static class Context {
private final ServerPlayer sender;
public Context() {
this(null);
}
public Context(ServerPlayer sender) {
this.sender = sender;
}
public void enqueueWork(Runnable runnable) { public void enqueueWork(Runnable runnable) {
runnable.run(); runnable.run();
} }
public ServerPlayer getSender() { public ServerPlayer getSender() {
return null; return this.sender;
} }
public void setPacketHandled(boolean handled) { public void setPacketHandled(boolean handled) {

View File

@@ -1,18 +1,119 @@
package com.vinlanx.explosionoverhaul.compat.network.simple; package com.vinlanx.explosionoverhaul.compat.network.simple;
import com.vinlanx.explosionoverhaul.ExplosionOverhaul;
import com.vinlanx.explosionoverhaul.compat.network.NetworkEvent; 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.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf; 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 { public class SimpleChannel {
private static final int MAX_PAYLOAD_SIZE = 1_048_576;
private final Map<Integer, MessageRegistration<?>> registrationsById = new HashMap<>();
private final Map<Class<?>, Integer> idsByType = new HashMap<>();
public <MSG> void registerMessage(int id, Class<MSG> messageType, BiConsumer<MSG, FriendlyByteBuf> encoder, Function<FriendlyByteBuf, MSG> decoder, BiConsumer<MSG, Supplier<NetworkEvent.Context>> handler) { public <MSG> void registerMessage(int id, Class<MSG> messageType, BiConsumer<MSG, FriendlyByteBuf> encoder, Function<FriendlyByteBuf, MSG> decoder, BiConsumer<MSG, Supplier<NetworkEvent.Context>> handler) {
MessageRegistration<MSG> 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) { 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) { 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<Object> 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<Object> 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<Object> registration(int id) {
return (MessageRegistration<Object>)this.registrationsById.get(id);
}
private record MessageRegistration<MSG>(
Class<MSG> messageType,
BiConsumer<MSG, FriendlyByteBuf> encoder,
Function<FriendlyByteBuf, MSG> decoder,
BiConsumer<MSG, Supplier<NetworkEvent.Context>> handler) {
}
public record LegacyPayload(int messageId, byte[] data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<LegacyPayload> TYPE =
new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath(ExplosionOverhaul.MODID, "legacy_packet"));
public static final StreamCodec<RegistryFriendlyByteBuf, LegacyPayload> 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<? extends CustomPacketPayload> type() {
return TYPE;
}
} }
} }

View File

@@ -1,8 +1,5 @@
{ {
"variants": { "variants": {
"facing=north": { "model": "explosionoverhaul:block/vinlanx_the_light" }, "": { "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 }
} }
} }

View File

@@ -1,54 +1,6 @@
{ {
"textures": [ "textures": [
"explosionoverhaul:soft_glow", "explosionoverhaul:soft_glow",
"explosionoverhaul:soft_glow_e", "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"
] ]
} }