package com.cursedcauldron.wildbackport.common.blocks; import com.cursedcauldron.wildbackport.WildBackport; import com.cursedcauldron.wildbackport.client.particle.SculkChargeParticleOptions; import com.cursedcauldron.wildbackport.client.registry.WBParticleTypes; import com.cursedcauldron.wildbackport.client.registry.WBSoundEvents; import com.cursedcauldron.wildbackport.common.tag.WBBlockTags; import com.cursedcauldron.wildbackport.common.utils.DirectionUtils; import com.cursedcauldron.wildbackport.common.utils.ModUtils; import com.cursedcauldron.wildbackport.common.utils.ParticleUtils; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.mojang.serialization.Codec; import com.mojang.serialization.Dynamic; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.minecraft.Util; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundSource; import net.minecraft.tags.TagKey; import net.minecraft.util.Mth; import net.minecraft.util.valueproviders.UniformInt; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.function.Supplier; public class SculkSpreadManager { final boolean isWorldGen; private final TagKey replaceableBlocks; private final int extraBlockChance; private final int maxDistance; private final int spreadChance; private final int decayChance; private List cursors = new ArrayList<>(); public SculkSpreadManager(boolean isWorldGen, TagKey replaceableBlocks, int extraBlockChance, int maxDistance, int spreadChance, int decayChance) { this.isWorldGen = isWorldGen; this.replaceableBlocks = replaceableBlocks; this.extraBlockChance = extraBlockChance; this.maxDistance = maxDistance; this.spreadChance = spreadChance; this.decayChance = decayChance; } public static SculkSpreadManager create() { return new SculkSpreadManager(false, WBBlockTags.SCULK_REPLACEABLE, 10, 4, 10, 5); } public static SculkSpreadManager createWorldGen() { return new SculkSpreadManager(true, WBBlockTags.SCULK_REPLACEABLE_WORLD_GEN, 50, 1, 5, 10); } public TagKey getReplaceableBlocks() { return this.replaceableBlocks; } public int getExtraBlockChance() { return this.extraBlockChance; } public int getMaxDistance() { return this.maxDistance; } public int getSpreadChance() { return this.spreadChance; } public int getDecayChance() { return this.decayChance; } public boolean isWorldGen() { return this.isWorldGen; } public void clearCursors() { this.cursors.clear(); } public void readTag(CompoundTag tag) { if (tag.contains("cursors", 9)) { this.cursors.clear(); List cursors = Cursor.CODEC.listOf().parse(new Dynamic<>(NbtOps.INSTANCE, tag.getList("cursors", 10))).resultOrPartial(WildBackport.LOGGER::error).orElseGet(ArrayList::new); int size = Math.min(cursors.size(), 32); for (int i = 0; i < size; i++) { this.addCursor(cursors.get(i)); } } } public void writeTag(CompoundTag tag) { Cursor.CODEC.listOf().encodeStart(NbtOps.INSTANCE, this.cursors).resultOrPartial(WildBackport.LOGGER::error).ifPresent(value -> { tag.put("cursors", value); }); } public void spread(BlockPos pos, int charge) { while (charge > 0) { int spread = Math.min(charge, 1000); this.addCursor(new Cursor(pos, spread)); charge -= spread; } } private void addCursor(Cursor cursor) { if (this.cursors.size() < 32) { this.cursors.add(cursor); } } public void tick(LevelAccessor level, BlockPos pos, Random random, boolean shouldConvert) { Level instance = level instanceof Level side ? side : null; if (!this.cursors.isEmpty()) { List cursors = new ArrayList<>(); Map cursorPositions = new HashMap<>(); Object2IntMap positions = new Object2IntOpenHashMap<>(); for (Cursor cursor : this.cursors) { cursor.spread(level, pos, random, this, shouldConvert); if (cursor.charge <= 0) { applySculkCharge(instance, cursor.getPos(), 0); } else { BlockPos position = cursor.getPos(); positions.computeInt(position, (blockPos, charge) -> (charge == null ? 0 : charge) + cursor.charge); Cursor target = cursorPositions.get(position); if (target == null) { cursorPositions.put(position, cursor); cursors.add(cursor); } else if (!this.isWorldGen() && cursor.charge + target.charge <= 1000) { target.merge(cursor); } else { cursors.add(cursor); if (cursor.charge < target.charge) { cursorPositions.put(position, cursor); } } } } for (Object2IntMap.Entry entry : positions.object2IntEntrySet()) { BlockPos position = entry.getKey(); int exp = entry.getIntValue(); Cursor cursor = cursorPositions.get(position); Set directions = cursor == null ? null : cursor.getFacings(); if (exp > 0 && directions != null) { int charge = (int)(Math.log1p(exp) / (double)2.3F) + 1; int data = (charge << 6) + SculkVeinBlock.directionsToFlag(directions); applySculkCharge(instance, cursor.getPos(), data); } } this.cursors = cursors; } } public static class Cursor { private static final ObjectArrayList OFFSETS = Util.make(new ObjectArrayList<>(18), positions -> { BlockPos.betweenClosedStream(new BlockPos(-1, -1, -1), new BlockPos(1, 1, 1)).filter(pos -> { return (pos.getX() == 0 || pos.getY() == 0 || pos.getZ() == 0) && pos != BlockPos.ZERO; }).map(BlockPos::immutable).forEach(positions::add); }); private BlockPos pos; private int charge; private int updateDelay; private int decayDelay; @Nullable private Set facings; private static final Codec> DIRECTION_SET = Direction.CODEC.listOf().xmap(directions -> Sets.newEnumSet(directions, Direction.class), Lists::newArrayList); public static final Codec CODEC = RecordCodecBuilder.create(instance -> { return instance.group(BlockPos.CODEC.fieldOf("pos").forGetter(Cursor::getPos), Codec.intRange(0, 1000).fieldOf("charge").orElse(0).forGetter(Cursor::getCharge), Codec.intRange(0, 1).fieldOf("decay_delay").orElse(1).forGetter(Cursor::getDecayDelay), Codec.intRange(0, Integer.MAX_VALUE).fieldOf("update_delay").orElse(0).forGetter(cursor -> { return cursor.updateDelay; }), DIRECTION_SET.optionalFieldOf("facings").forGetter(cursor -> { return Optional.ofNullable(cursor.getFacings()); })).apply(instance, Cursor::new); }); private Cursor(BlockPos pos, int charge, int decayDelay, int updateDelay, Optional> facings) { this.pos = pos; this.charge = charge; this.decayDelay = decayDelay; this.updateDelay = updateDelay; this.facings = facings.orElse(null); } public Cursor(BlockPos pos, int charge) { this(pos, charge, 1, 0, Optional.empty()); } public BlockPos getPos() { return this.pos; } public int getCharge() { return this.charge; } public int getDecayDelay() { return this.decayDelay; } @Nullable public Set getFacings() { return this.facings; } private boolean canSpread(LevelAccessor level, BlockPos pos, boolean isWorldGen) { if (this.charge <= 0) { return false; } else if (isWorldGen) { return true; } else if (level instanceof ServerLevel server) { return server.shouldTickBlocksAt(ChunkPos.asLong(pos)); } else { return false; } } public void spread(LevelAccessor level, BlockPos pos, Random random, SculkSpreadManager spreadManager, boolean shouldConvert) { if (this.canSpread(level, pos, spreadManager.isWorldGen)) { if (this.updateDelay > 0) { --this.updateDelay; } else { BlockState state = level.getBlockState(this.pos); SculkSpreadable spreadable = getSpreadable(state); if (shouldConvert && spreadable.spread(level, this.pos, state, this.facings, spreadManager.isWorldGen())) { if (spreadable.shouldConvertToSpreadable()) { state = level.getBlockState(this.pos); spreadable = getSpreadable(state); } level.playSound(null, this.pos, WBSoundEvents.BLOCK_SCULK_BREAK, SoundSource.BLOCKS, 1.0F, 1.0F); } this.charge = spreadable.spread(this, level, pos, random, spreadManager, shouldConvert); if (this.charge <= 0) { spreadable.spreadAtSamePosition(level, state, this.pos, random); } else { BlockPos target = getSpreadPos(level, this.pos, random); if (target != null) { spreadable.spreadAtSamePosition(level, state, this.pos, random); this.pos = target.immutable(); if (spreadManager.isWorldGen() && !this.pos.closerThan(new Vec3i(pos.getX(), this.pos.getY(), pos.getZ()), 15.0D)) { this.charge = 0; return; } state = level.getBlockState(target); } if (state.getBlock() instanceof SculkSpreadable) { this.facings = SculkVeinBlock.collectDirections(state); } this.decayDelay = spreadable.getDecay(this.decayDelay); this.updateDelay = spreadable.getUpdate(); } } } } void merge(Cursor cursor) { this.charge += cursor.charge; cursor.charge = 0; this.updateDelay = Math.min(this.updateDelay, cursor.updateDelay); } private static SculkSpreadable getSpreadable(BlockState state) { return state.getBlock() instanceof SculkSpreadable spreadable ? spreadable : SculkSpreadable.DEFAULT; } private static List shuffleOffsets(Random random) { return ModUtils.copyShuffled(OFFSETS, random); } @Nullable private static BlockPos getSpreadPos(LevelAccessor level, BlockPos pos, Random random) { BlockPos.MutableBlockPos target = pos.mutable(); BlockPos.MutableBlockPos source = pos.mutable(); for (Vec3i offset : shuffleOffsets(random)) { source.setWithOffset(pos, offset); BlockState state = level.getBlockState(source); if (state.getBlock() instanceof SculkSpreadable && canSpread(level, pos, source)) { target.set(source); if (SculkVeinBlock.veinCoversSculkReplaceable(level, state, source)) { break; } } } return target.equals(pos) ? null : target; } private static boolean canSpread(LevelAccessor level, BlockPos source, BlockPos target) { if (source.distManhattan(target) == 1) { return true; } else { BlockPos pos = target.subtract(target); Direction xAxis = Direction.fromAxisAndDirection(Direction.Axis.X, pos.getX() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE); Direction yAxis = Direction.fromAxisAndDirection(Direction.Axis.Y, pos.getY() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE); Direction zAxis = Direction.fromAxisAndDirection(Direction.Axis.Z, pos.getZ() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE); if (pos.getX() == 0) { return canSpread(level, source, yAxis) || canSpread(level, source, zAxis); } else if (pos.getY() == 0) { return canSpread(level, source, xAxis) || canSpread(level, source, zAxis); } else { return canSpread(level, source, xAxis) || canSpread(level, source, yAxis); } } } private static boolean canSpread(LevelAccessor level, BlockPos pos, Direction direction) { BlockPos facing = pos.relative(direction); return !level.getBlockState(facing).isFaceSturdy(level, facing, direction.getOpposite()); } } public static void applySculkCharge(Level level, BlockPos pos, int data) { if (level == null) return; Random random = level.getRandom(); ClientLevel client = level instanceof ClientLevel side ? side : null; ServerLevel server = level instanceof ServerLevel side ? side : null; int charge = data >> 6; if (charge > 0) { if (random.nextFloat() < (float)charge * 0.2F) { float volume = 0.15F + 0.05F * (float)charge * (float)charge * random.nextFloat(); float pitch = 0.4F * (float)charge - 0.2F * random.nextFloat(); if (client != null) client.playLocalSound(pos, WBSoundEvents.BLOCK_SCULK_CHARGE, SoundSource.BLOCKS, volume, pitch, false); } int facings = (byte)(data & 63); UniformInt spread = UniformInt.of(0, charge); Supplier velocities = () -> { return new Vec3(Mth.nextDouble(random, -0.005D, 0.005D), Mth.nextDouble(random, -0.005D, 0.005D), Mth.nextDouble(random, -0.005D, 0.005D)); }; if (facings == 0) { for (Direction direction : Direction.values()) { float roll = direction == Direction.DOWN ? (float)Math.PI : 0.0F; double offset = direction == Direction.UP || direction == Direction.DOWN ? 0.32D : 0.57D; ParticleUtils.spawnParticles(level, pos, new SculkChargeParticleOptions(roll), spread, direction, velocities, offset); } } else { for (Direction direction : DirectionUtils.unpack((byte)data)) { float roll = direction == Direction.UP ? (float)Math.PI : 0.0F; ParticleUtils.spawnParticles(level, pos, new SculkChargeParticleOptions(roll), spread, direction, velocities, 0.35D); } } } else { if (client != null) client.playLocalSound(pos, WBSoundEvents.BLOCK_SCULK_CHARGE, SoundSource.BLOCKS, 1.0F, 1.0F, false); boolean fullBlock = level.getBlockState(pos).isCollisionShapeFullBlock(level, pos); int tries = fullBlock ? 40 : 20; float spread = fullBlock ? 0.45F : 0.25F; for (int i = 0; i < tries; i++) { float x = 2.0F * random.nextFloat() - 1.0F; float y = 2.0F * random.nextFloat() - 1.0F; float z = 2.0F * random.nextFloat() - 1.0F; if (server != null) server.sendParticles(WBParticleTypes.SCULK_CHARGE_POP.get(), (double)pos.getX() + 0.5D + (double)(x * spread), (double)pos.getY() + 0.5D + (double)(y * spread), (double)pos.getZ() + 0.5D + (double)(z * spread), 1, x * 0.07F, y * 0.07F, z * 0.07F, 0.0D); } } } }