TheWildBackport/common/src/main/java/com/cursedcauldron/wildbackport/common/blocks/SculkSpreadManager.java
2022-08-25 17:53:18 -04:00

388 lines
17 KiB
Java

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<Block> replaceableBlocks;
private final int extraBlockChance;
private final int maxDistance;
private final int spreadChance;
private final int decayChance;
private List<Cursor> cursors = new ArrayList<>();
public SculkSpreadManager(boolean isWorldGen, TagKey<Block> 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<Block> 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<Cursor> 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<Cursor> cursors = new ArrayList<>();
Map<BlockPos, Cursor> cursorPositions = new HashMap<>();
Object2IntMap<BlockPos> 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<BlockPos> entry : positions.object2IntEntrySet()) {
BlockPos position = entry.getKey();
int exp = entry.getIntValue();
Cursor cursor = cursorPositions.get(position);
Set<Direction> 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<Vec3i> 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<Direction> facings;
private static final Codec<Set<Direction>> DIRECTION_SET = Direction.CODEC.listOf().xmap(directions -> Sets.newEnumSet(directions, Direction.class), Lists::newArrayList);
public static final Codec<Cursor> 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<Set<Direction>> 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<Direction> 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<Vec3i> 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.isClientSide() && 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<Vec3> 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);
}
}
}
}