diff --git a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java index 44e6c342b..2309ec90f 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/ThickBranchBlock.java @@ -7,7 +7,7 @@ import com.ferreusveritas.dynamictrees.init.DTRegistries; import com.ferreusveritas.dynamictrees.systems.BranchConnectables; import com.ferreusveritas.dynamictrees.util.CoordUtils; -import com.ferreusveritas.dynamictrees.util.CoordUtils.Surround; +import com.ferreusveritas.dynamictrees.util.CoordUtils.ShellDirection; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; @@ -16,6 +16,7 @@ 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.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.IntegerProperty; @@ -31,10 +32,14 @@ public class ThickBranchBlock extends BasicBranchBlock implements Musable { - public static final int MAX_RADIUS_THICK = 24; + public static final int MAX_RADIUS_THICK = 56; + public static final int RADIUS_TO_INNER_SHELL = 8; // > 8 needs 3×3 + public static final int RADIUS_TO_OUTER_SHELL = 24; // > 24 needs 5×5 + public static final int RADIUS_TO_OUTERMOST_SHELL = 40; // > 40 needs 7×7 - protected static final IntegerProperty RADIUS_DOUBLE = IntegerProperty.create("radius", 1, MAX_RADIUS_THICK); //39 ? + protected static final IntegerProperty RADIUS_DOUBLE = IntegerProperty.create("radius", 1, MAX_RADIUS_THICK); + @Deprecated public ThickBranchBlock(ResourceLocation name, MapColor mapColor) { this(name, Properties.of().mapColor(mapColor)); } @@ -52,8 +57,8 @@ public void createBlockStateDefinition(StateDefinition.Builder RADIUS_TO_INNER_SHELL; // > 8 + boolean needsOuterRing = radius > RADIUS_TO_OUTER_SHELL; // > 24 + boolean needsOutermostRing = radius > RADIUS_TO_OUTERMOST_SHELL; // > 40 + + // No shells needed + if (!needsInnerRing) { return true; } - boolean setable = true; - final ReplaceableState[] repStates = new ReplaceableState[8]; - - for (Surround dir : Surround.values()) { - final BlockPos dPos = pos.offset(dir.getOffset()); - final ReplaceableState rep = getReplaceability(level, dPos, pos); - - repStates[dir.ordinal()] = rep; - + // === Check inner ring === + final ReplaceableState[] innerRepStates = new ReplaceableState[8]; + ShellDirection[] innerDirs = ShellDirection.innerValues(); + for (int i = 0; i < innerDirs.length; i++) { + ShellDirection dir = innerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = getReplaceability(level, dPos, pos, 1); + innerRepStates[i] = rep; if (rep == ReplaceableState.BLOCKING) { - setable = false; - break; + return false; } } - if (setable) { - BlockState trunkState = level.getBlockState(pos); - boolean isWaterlogged = trunkState.hasProperty(WATERLOGGED) && trunkState.getValue(WATERLOGGED); - for (Surround dir : Surround.values()) { - final BlockPos dPos = pos.offset(dir.getOffset()); - final ReplaceableState rep = repStates[dir.ordinal()]; - final boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); + // === Check outer ring (if needed) === + final ReplaceableState[] outerRepStates = new ReplaceableState[16]; + ShellDirection[] outerDirs = ShellDirection.outerValues(); + if (needsOuterRing) { + for (int i = 0; i < outerDirs.length; i++) { + ShellDirection dir = outerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = getReplaceability(level, dPos, pos, 2); + outerRepStates[i] = rep; + if (rep == ReplaceableState.BLOCKING) { + return false; + } + } + } - if (rep == ReplaceableState.REPLACEABLE) { - level.setBlock(dPos, getTrunkShell().defaultBlockState().setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()).setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + // === Check outermost ring (if needed) === + final ReplaceableState[] outermostRepStates = new ReplaceableState[24]; + ShellDirection[] outermostDirs = ShellDirection.outermostValues(); + if (needsOutermostRing) { + for (int i = 0; i < outermostDirs.length; i++) { + ShellDirection dir = outermostDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = getReplaceability(level, dPos, pos, 3); + outermostRepStates[i] = rep; + if (rep == ReplaceableState.BLOCKING) { + return false; } } - return true; } - return false; - } - @Override - public int getRadiusForConnection(BlockState state, BlockGetter level, BlockPos pos, BranchBlock from, Direction side, int fromRadius) { - if (from instanceof ThickBranchBlock) { - return getRadius(state); + // === Place shells === + BlockState trunkState = level.getBlockState(pos); + boolean isWaterlogged = trunkState.hasProperty(WATERLOGGED) && trunkState.getValue(WATERLOGGED); + + // Place inner ring + for (int i = 0; i < innerDirs.length; i++) { + ShellDirection dir = innerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = innerRepStates[i]; + boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); + + if (rep == ReplaceableState.REPLACEABLE) { + level.setBlock(dPos, getTrunkShell().defaultBlockState() + .setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()) + .setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + } } - return Math.min(getRadius(state), MAX_RADIUS); - } - @Override - protected int getSideConnectionRadius(BlockGetter level, BlockPos pos, int radius, Direction side) { - final BlockPos deltaPos = pos.relative(side); - final BlockState blockState = CoordUtils.getStateSafe(level, deltaPos); + // Place outer ring (if needed) + if (needsOuterRing) { + for (int i = 0; i < outerDirs.length; i++) { + ShellDirection dir = outerDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = outerRepStates[i]; + boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); - if (blockState == null) { - return 0; + if (rep == ReplaceableState.REPLACEABLE) { + level.setBlock(dPos, getTrunkShell().defaultBlockState() + .setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()) + .setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + } + } } - final int connectionRadius = TreeHelper.getTreePart(blockState).getRadiusForConnection(blockState, level, deltaPos, this, side, radius); + // Place outermost ring (if needed) + if (needsOutermostRing) { + for (int i = 0; i < outermostDirs.length; i++) { + ShellDirection dir = outermostDirs[i]; + BlockPos dPos = pos.offset(dir.getOffset()); + ReplaceableState rep = outermostRepStates[i]; + boolean replacingWater = isWaterlogged || level.getBlockState(dPos).getFluidState() == Fluids.WATER.getSource(false); - return Math.min(MAX_RADIUS, connectionRadius); - } + if (rep == ReplaceableState.REPLACEABLE) { + level.setBlock(dPos, getTrunkShell().defaultBlockState() + .setValue(TrunkShellBlock.CORE_DIR, dir.getOpposite()) + .setValue(TrunkShellBlock.WATERLOGGED, replacingWater), flags); + } + } + } - public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, BlockPos corePos) { + return true; + } + public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, BlockPos corePos, int ringLevel) { final BlockState state = level.getBlockState(pos); final Block block = state.getBlock(); if (block instanceof TrunkShellBlock) { - // Determine if this shell belongs to the trunk. Block otherwise. - Surround surr = state.getValue(TrunkShellBlock.CORE_DIR); - return pos.offset(surr.getOffset()).equals(corePos) ? ReplaceableState.SHELL : ReplaceableState.BLOCKING; + ShellDirection dir = state.getValue(TrunkShellBlock.CORE_DIR); + return pos.offset(dir.getOffset()).equals(corePos) ? ReplaceableState.SHELL : ReplaceableState.BLOCKING; } if (state.canBeReplaced() || state.is(DTBlockTags.FOLIAGE)) { @@ -165,7 +214,7 @@ public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, Blo return ReplaceableState.TREEPART; } - if (block instanceof FruitBlock || block instanceof PodBlock){ + if (block instanceof FruitBlock || block instanceof PodBlock) { return ReplaceableState.TREEPART; } @@ -173,14 +222,57 @@ public ReplaceableState getReplaceability(LevelAccessor level, BlockPos pos, Blo return ReplaceableState.REPLACEABLE; } + if (ringLevel == 1) { + float hardness = state.getDestroySpeed(level, pos); + if (hardness >= 0 && hardness < 1) { + return ReplaceableState.REPLACEABLE; + } + } + + if (ringLevel == 2) { + float hardness = state.getDestroySpeed(level, pos); + if (hardness >= 0 && hardness < 3) { + return ReplaceableState.REPLACEABLE; + } + } + + // Outermost ring can break most hard blocks + if (ringLevel == 3) { + float hardness = state.getDestroySpeed(level, pos); + if (hardness >= 0 && hardness < 5) { + return ReplaceableState.REPLACEABLE; + } + } + return ReplaceableState.BLOCKING; } enum ReplaceableState { - SHELL, // This indicates that the block is already a shell. - REPLACEABLE, // This indicates that the block is truly replaceable and will be erased. - BLOCKING, // This indicates that the block is not replaceable, will NOT be erased, and will prevent the tree from growing. - TREEPART // This indicates that the block is part of a tree, will NOT be erase, and will NOT prevent the tree from growing. + SHELL, + REPLACEABLE, + BLOCKING, + TREEPART + } + + @Override + public int getRadiusForConnection(BlockState state, BlockGetter level, BlockPos pos, BranchBlock from, Direction side, int fromRadius) { + if (from instanceof ThickBranchBlock) { + return getRadius(state); + } + return Math.min(getRadius(state), MAX_RADIUS); + } + + @Override + protected int getSideConnectionRadius(BlockGetter level, BlockPos pos, int radius, Direction side) { + final BlockPos deltaPos = pos.relative(side); + final BlockState blockState = CoordUtils.getStateSafe(level, deltaPos); + + if (blockState == null) { + return 0; + } + + final int connectionRadius = TreeHelper.getTreePart(blockState).getRadiusForConnection(blockState, level, deltaPos, this, side, radius); + return Math.min(MAX_RADIUS, connectionRadius); } @Override @@ -188,9 +280,8 @@ public int getMaxRadius() { return MAX_RADIUS_THICK; } - - /////////////////////////////////////////// - // PHYSICAL BOUNDS +/////////////////////////////////////////// +// PHYSICAL BOUNDS /////////////////////////////////////////// @Nonnull @@ -209,5 +300,4 @@ public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, Co public boolean isMusable(BlockGetter level, BlockState state, BlockPos pos) { return getRadius(state) > 8; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java index ae5cc16b2..0289c286f 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/block/branch/TrunkShellBlock.java @@ -2,12 +2,12 @@ import com.ferreusveritas.dynamictrees.block.BlockWithDynamicHardness; import com.ferreusveritas.dynamictrees.util.CoordUtils; +import com.ferreusveritas.dynamictrees.util.CoordUtils.ShellDirection; import com.ferreusveritas.dynamictrees.util.CoordUtils.Surround; import com.ferreusveritas.dynamictrees.util.Null; import net.minecraft.client.particle.ParticleEngine; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; -import net.minecraft.core.Vec3i; import net.minecraft.core.particles.BlockParticleOption; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.server.level.ServerLevel; @@ -34,7 +34,6 @@ import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; -import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -48,22 +47,27 @@ @SuppressWarnings("deprecation") public class TrunkShellBlock extends BlockWithDynamicHardness implements SimpleWaterloggedBlock { - public static final EnumProperty CORE_DIR = EnumProperty.create("coredir", Surround.class); + // Single unified property for all 24 shell directions + public static final EnumProperty CORE_DIR = EnumProperty.create("coredir", ShellDirection.class); public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; public static class ShellMuse { public final BlockState state; public final BlockPos pos; public final BlockPos museOffset; - public final Surround dir; + public final ShellDirection dir; - public ShellMuse(BlockState state, BlockPos pos, Surround dir, BlockPos museOffset) { + public ShellMuse(BlockState state, BlockPos pos, ShellDirection dir, BlockPos museOffset) { this.state = state; this.pos = pos; this.dir = dir; this.museOffset = museOffset; } + public boolean isOuter() { + return dir.isOuter(); + } + public int getRadius() { final Block block = this.state.getBlock(); return block instanceof BranchBlock ? ((BranchBlock) block).getRadius(state) : 0; @@ -72,7 +76,9 @@ public int getRadius() { public TrunkShellBlock() { super(Properties.of().ignitedByLava().pushReaction(PushReaction.BLOCK).noOcclusion()); - registerDefaultState(defaultBlockState().setValue(WATERLOGGED, false)); + registerDefaultState(defaultBlockState() + .setValue(CORE_DIR, ShellDirection.N) + .setValue(WATERLOGGED, false)); } /////////////////////////////////////////// @@ -80,21 +86,38 @@ public TrunkShellBlock() { /////////////////////////////////////////// protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(CORE_DIR).add(WATERLOGGED); + builder.add(CORE_DIR, WATERLOGGED); } @Override public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { ShellMuse muse = this.getMuseUnchecked(level, state, pos); - if (!isValid(muse)) { - if (state.getValue(WATERLOGGED)) { - level.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState()); - } else { - level.removeBlock(pos, false); + + if (muse == null) { + ShellDirection museDir = getMuseDir(state); + BlockPos targetPos = pos.offset(museDir.getOffset()); + + if (!CoordUtils.canAccessStateSafely(level, targetPos)) { + level.scheduleTick(pos, this, 20); + return; } + // Chunk accessible, muse gone - remove shell + removeShell(state, level, pos); + return; + } + + if (!isValid(muse)) { + removeShell(state, level, pos); } } + private void removeShell(BlockState state, ServerLevel level, BlockPos pos) { + if (state.getValue(WATERLOGGED)) { + level.setBlockAndUpdate(pos, Blocks.WATER.defaultBlockState()); + } else { + level.removeBlock(pos, false); + } + } /////////////////////////////////////////// // INTERACTION /////////////////////////////////////////// @@ -130,17 +153,16 @@ public boolean canBeReplaced(BlockState state, BlockPlaceContext useContext) { final BlockPos clickedPos = useContext.getClickedPos(); if (this.museDoesNotExist(level, state, clickedPos)) { this.scheduleUpdateTick(level, clickedPos); - return false; } return false; } - public Surround getMuseDir(BlockState state, BlockPos pos) { + public ShellDirection getMuseDir(BlockState state) { return state.getValue(CORE_DIR); } public boolean museDoesNotExist(BlockGetter level, BlockState state, BlockPos pos) { - final BlockPos musePos = pos.offset(this.getMuseDir(state, pos).getOffset()); + final BlockPos musePos = pos.offset(this.getMuseDir(state).getOffset()); return CoordUtils.getStateSafe(level, musePos) == null; } @@ -156,7 +178,7 @@ public ShellMuse getMuseUnchecked(BlockGetter level, BlockState state, BlockPos @Nullable public ShellMuse getMuseUnchecked(BlockGetter level, BlockState state, BlockPos pos, BlockPos originalPos) { - final Surround museDir = getMuseDir(state, pos); + final ShellDirection museDir = getMuseDir(state); final BlockPos musePos = pos.offset(museDir.getOffset()); final BlockState museState = CoordUtils.getStateSafe(level, musePos); @@ -167,11 +189,9 @@ public ShellMuse getMuseUnchecked(BlockGetter level, BlockState state, BlockPos final Block block = museState.getBlock(); if (block instanceof Musable && ((Musable) block).isMusable(level, museState, musePos)) { return new ShellMuse(museState, musePos, museDir, musePos.subtract(originalPos)); - } else if (block instanceof TrunkShellBlock) { // If its another trunkshell, then this trunkshell is on another layer. IF they share a common direction, we return that shell's muse. - final Vec3i offset = ((TrunkShellBlock) block).getMuseDir(museState, musePos).getOffset(); - if (new Vec3(offset.getX(), offset.getY(), offset.getZ()).add(new Vec3(museDir.getOffset().getX(), museDir.getOffset().getY(), museDir.getOffset().getZ())).lengthSqr() > 2.25) { - return (((TrunkShellBlock) block).getMuseUnchecked(level, museState, musePos, originalPos)); - } + } else if (block instanceof TrunkShellBlock shellBlock) { + // If it's another trunk shell, follow the chain to find the core + return shellBlock.getMuseUnchecked(level, museState, musePos, originalPos); } return null; } @@ -185,7 +205,6 @@ public ShellMuse getMuse(BlockGetter level, BlockPos pos) { public ShellMuse getMuse(BlockGetter level, BlockState state, BlockPos pos) { final ShellMuse muse = this.getMuseUnchecked(level, state, pos); - // Check the muse for validity. if (!isValid(muse)) { this.scheduleUpdateTick(level, pos); } @@ -193,20 +212,35 @@ public ShellMuse getMuse(BlockGetter level, BlockState state, BlockPos pos) { return muse; } + /** + * Validates a shell muse based on radius thresholds. + * Inner shells (distance 1) require radius > 8 + * Outer shells (distance 2) require radius > 24 + */ protected boolean isValid(@Nullable ShellMuse muse) { - return muse != null && muse.getRadius() > 8; + if (muse == null) { + return false; + } + int radius = muse.getRadius(); + int shellLevel = muse.dir.getShellLevel(); + + return switch (shellLevel) { + case 1 -> radius > ThickBranchBlock.RADIUS_TO_INNER_SHELL; // > 8 + case 2 -> radius > ThickBranchBlock.RADIUS_TO_OUTER_SHELL; // > 24 + case 3 -> radius > ThickBranchBlock.RADIUS_TO_OUTERMOST_SHELL; // > 40 + default -> false; + }; } public void scheduleUpdateTick(BlockGetter level, BlockPos pos) { if (!(level instanceof LevelAccessor)) { return; } - ((LevelAccessor) level).getBlockTicks().schedule(new ScheduledTick<>(this, pos.immutable(), 0, TickPriority.HIGH, 0)); } @Override - public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean p_220069_6_) { + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean isMoving) { this.scheduleUpdateTick(level, pos); } @@ -225,30 +259,25 @@ public void onBlockExploded(BlockState state, Level level, BlockPos pos, Explosi Null.consumeIfNonnull(this.getMuse(level, state, pos), muse -> muse.state.getBlock().onBlockExploded(muse.state, level, muse.pos, explosion)); } - //TODO: This may not even be necessary @Nullable - protected Surround findDetachedMuse(Level level, BlockPos pos) { - for (Surround s : Surround.values()) { - final BlockState state = level.getBlockState(pos.offset(s.getOffset())); - + protected ShellDirection findDetachedMuse(Level level, BlockPos pos) { + for (ShellDirection dir : ShellDirection.values()) { + final BlockState state = level.getBlockState(pos.offset(dir.getOffset())); if (state.getBlock() instanceof Musable) { - return s; + return dir; } } return null; } - //TODO: This may not even be necessary @Override public void destroy(LevelAccessor level, BlockPos pos, BlockState state) { final BlockState newState = level.getBlockState(pos); - if (newState.getBlock() != Blocks.AIR) { return; } - Null.consumeIfNonnull(this.findDetachedMuse((Level) level, pos), - surround -> level.setBlock(pos, defaultBlockState().setValue(CORE_DIR, surround), 1)); + dir -> level.setBlock(pos, defaultBlockState().setValue(CORE_DIR, dir), 1)); } @Override @@ -258,12 +287,12 @@ public InteractionResult use(BlockState state, Level level, BlockPos pos, Player @Override public boolean isFlammable(BlockState state, BlockGetter level, BlockPos pos, Direction face) { - return false; // This is the simple solution to the problem. Maybe I'll work it out later. + return false; } @Override public int getFlammability(BlockState state, BlockGetter level, BlockPos pos, Direction face) { - return 0; // This is the simple solution to the problem. Maybe I'll work it out later. + return 0; } public boolean isFullBlockShell(BlockGetter level, BlockPos pos) { @@ -295,7 +324,7 @@ public FluidState getFluidState(BlockState state) { @Override public BlockState updateShape(BlockState state, Direction facing, BlockState facingState, LevelAccessor level, BlockPos currentPos, BlockPos facingPos) { if (state.getValue(WATERLOGGED)) { - level.getFluidTicks().schedule(new ScheduledTick<>(Fluids.WATER,currentPos, Fluids.WATER.getTickDelay(level),0)); + level.getFluidTicks().schedule(new ScheduledTick<>(Fluids.WATER, currentPos, Fluids.WATER.getTickDelay(level), 0)); } return super.updateShape(state, facing, facingState, level, currentPos, facingPos); } @@ -326,7 +355,6 @@ public boolean addLandingEffects(BlockState state1, ServerLevel level, BlockPos return true; } - @Override public void initializeClient(Consumer consumer) { consumer.accept(new IClientBlockExtensions() { @@ -339,9 +367,8 @@ public boolean addHitEffects(BlockState state, Level level, HitResult target, Pa return false; } - if (state.getBlock() instanceof TrunkShellBlock) { - final ShellMuse muse = ((TrunkShellBlock)state.getBlock()).getMuseUnchecked(level, state, shellPos); - + if (state.getBlock() instanceof TrunkShellBlock shellBlock) { + final ShellMuse muse = shellBlock.getMuseUnchecked(level, state, shellPos); if (muse == null) { return true; } @@ -367,30 +394,22 @@ public boolean addHitEffects(BlockState state, Level level, HitResult target, Pa case EAST -> d0 = x + axisalignedbb.maxX + 0.1D; } - // Safe to spawn particles here since this is a client side only member function. level.addParticle(new BlockParticleOption(ParticleTypes.BLOCK, museState), d0, d1, d2, 0, 0, 0); } - return true; } @Override public boolean addDestroyEffects(BlockState state, Level level, BlockPos pos, ParticleEngine manager) { - if (state.getBlock() instanceof TrunkShellBlock) { - final ShellMuse muse = ((TrunkShellBlock)state.getBlock()).getMuseUnchecked(level, state, pos); - + if (state.getBlock() instanceof TrunkShellBlock shellBlock) { + final ShellMuse muse = shellBlock.getMuseUnchecked(level, state, pos); if (muse == null) { return true; } - - final BlockState museState = muse.state; - final BlockPos musePos = muse.pos; - - manager.destroy(musePos, museState); + manager.destroy(muse.pos, muse.state); } return true; } }); } - } diff --git a/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java b/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java index f6d8068ac..5f4f0c8e4 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/models/baked/ThickBranchBlockBakedModel.java @@ -45,10 +45,10 @@ @OnlyIn(Dist.CLIENT) public class ThickBranchBlockBakedModel extends BasicBranchBlockBakedModel { - private final BakedModel[] trunksBark = new BakedModel[16]; // The trunk will always feature bark on its sides. - private final BakedModel[] trunksTopBark = new BakedModel[16]; // The trunk will feature bark on its top when there's a branch on top of it. - private final BakedModel[] trunksTopRings = new BakedModel[16]; // The trunk will feature rings on its top when there's no branches on top of it. - private final BakedModel[] trunksBotRings = new BakedModel[16]; // The trunk will always feature rings on its bottom surface if nothing is below it. + private final BakedModel[] trunksBark = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; + private final BakedModel[] trunksTopBark = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; + private final BakedModel[] trunksTopRings = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; + private final BakedModel[] trunksBotRings = new BakedModel[ThickBranchBlock.MAX_RADIUS_THICK - BranchBlock.MAX_RADIUS]; public ThickBranchBlockBakedModel(IGeometryBakingContext customData, ResourceLocation modelLocation, ResourceLocation barkTextureLocation, ResourceLocation ringsTextureLocation, ResourceLocation thickRingsTextureLocation, Function spriteGetter) { @@ -74,27 +74,56 @@ private boolean isTextureNull(@Nullable TextureAtlasSprite sprite) { return sprite == null || sprite.equals(ModelUtils.getTexture(new ResourceLocation(""))); } - public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean side) { + /** + * Determines grid size based on radius. + * @return 3 for radius 9-24, 5 for radius 25-40, 7 for radius 41-56 + */ + private int getGridSize(int radius) { + if (radius > ThickBranchBlock.RADIUS_TO_OUTERMOST_SHELL) { + return 7; + } else if (radius > ThickBranchBlock.RADIUS_TO_OUTER_SHELL) { + return 5; + } else { + return 3; + } + } + + /** + * Generates grid offsets based on grid size. + */ + private ArrayList getGridOffsets(int gridSize) { + ArrayList offsets = new ArrayList<>(); + int halfGrid = gridSize / 2; + for (int x = -halfGrid; x <= halfGrid; x++) { + for (int z = -halfGrid; z <= halfGrid; z++) { + offsets.add(new Vec3i(x, 0, z)); + } + } + return offsets; + } + public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean side) { IModelBuilder builder = ModelUtils.getModelBuilder(this.blockModel.customData, bark); AABB wholeVolume = new AABB(8 - radius, 0, 8 - radius, 8 + radius, 16, 8 + radius); final Direction[] run = side ? CoordUtils.HORIZONTALS : new Direction[]{Direction.UP, Direction.DOWN}; - ArrayList offsets = new ArrayList<>(); - for (Surround dir : Surround.values()) { - offsets.add(dir.getOffset()); // 8 surrounding component pieces - } - offsets.add(new Vec3i(0, 0, 0));//Center + int gridSize = getGridSize(radius); + ArrayList offsets = getGridOffsets(gridSize); for (Direction face : run) { final Vec3i dirVector = face.getNormal(); for (Vec3i offset : offsets) { - if (face.getAxis() == Axis.Y || new Vec3(dirVector.getX(), dirVector.getY(), dirVector.getZ()).add(new Vec3(offset.getX(), offset.getY(), offset.getZ())).lengthSqr() > 2.25) { //This means that the dir and face share a common direction - Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16);//Scale the dimensions to match standard minecraft texels + if (face.getAxis() == Axis.Y || new Vec3(dirVector.getX(), dirVector.getY(), dirVector.getZ()).add(new Vec3(offset.getX(), offset.getY(), offset.getZ())).lengthSqr() > 2.25) { + Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16); AABB partBoundary = new AABB(0, 0, 0, 16, 16, 16).move(scaledOffset).intersect(wholeVolume); + // Skip if intersection is empty + if (partBoundary.getXsize() <= 0 || partBoundary.getYsize() <= 0 || partBoundary.getZsize() <= 0) { + continue; + } + Vector3f[] limits = ModelUtils.AABBLimits(partBoundary); Map mapFacesIn = Maps.newEnumMap(Direction.class); @@ -105,7 +134,6 @@ public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean sid BlockElement part = new BlockElement(limits[0], limits[1], mapFacesIn, null, true); builder.addCulledFace(face, ModelUtils.makeBakedQuad(part, part.faces.get(face), bark, face, BlockModelRotation.X0_Y0, this.modelLocation)); } - } } @@ -115,34 +143,38 @@ public BakedModel bakeTrunkBark(int radius, TextureAtlasSprite bark, boolean sid public BakedModel bakeTrunkRings(int radius, TextureAtlasSprite ring, Direction face) { IModelBuilder builder = ModelUtils.getModelBuilder(this.blockModel.customData, ring); AABB wholeVolume = new AABB(8 - radius, 0, 8 - radius, 8 + radius, 16, 8 + radius); - int wholeVolumeWidth = 48; - ArrayList offsets = new ArrayList<>(); + int gridSize = getGridSize(radius); + // Texture width: 48 for 3×3, 80 for 5×5, 112 for 7×7 + int wholeVolumeWidth = gridSize * 16; - for (Surround dir : Surround.values()) { - offsets.add(dir.getOffset()); // 8 surrounding component pieces - } - offsets.add(new Vec3i(0, 0, 0)); // Center + ArrayList offsets = getGridOffsets(gridSize); + + // Texture offset based on grid size + float textureOffset = -(gridSize / 2) * 16f; for (Vec3i offset : offsets) { - Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16); // Scale the dimensions to match standard minecraft texels + Vec3 scaledOffset = new Vec3(offset.getX() * 16, offset.getY() * 16, offset.getZ() * 16); AABB partBoundary = new AABB(0, 0, 0, 16, 16, 16).move(scaledOffset).intersect(wholeVolume); + // Skip if intersection is empty + if (partBoundary.getXsize() <= 0 || partBoundary.getYsize() <= 0 || partBoundary.getZsize() <= 0) { + continue; + } + Vector3f posFrom = new Vector3f((float) partBoundary.minX, (float) partBoundary.minY, (float) partBoundary.minZ); Vector3f posTo = new Vector3f((float) partBoundary.maxX, (float) partBoundary.maxY, (float) partBoundary.maxZ); Map mapFacesIn = Maps.newEnumMap(Direction.class); - float textureOffsetX = -16f; - float textureOffsetZ = -16f; - float minX = ((float) ((partBoundary.minX - textureOffsetX) / wholeVolumeWidth)) * 16f; - float maxX = ((float) ((partBoundary.maxX - textureOffsetX) / wholeVolumeWidth)) * 16f; - float minZ = ((float) ((partBoundary.minZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; - float maxZ = ((float) ((partBoundary.maxZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; + float minX = ((float) ((partBoundary.minX - textureOffset) / wholeVolumeWidth)) * 16f; + float maxX = ((float) ((partBoundary.maxX - textureOffset) / wholeVolumeWidth)) * 16f; + float minZ = ((float) ((partBoundary.minZ - textureOffset) / wholeVolumeWidth)) * 16f; + float maxZ = ((float) ((partBoundary.maxZ - textureOffset) / wholeVolumeWidth)) * 16f; if (face == Direction.DOWN) { - minZ = ((float) ((partBoundary.maxZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; - maxZ = ((float) ((partBoundary.minZ - textureOffsetZ) / wholeVolumeWidth)) * 16f; + minZ = ((float) ((partBoundary.maxZ - textureOffset) / wholeVolumeWidth)) * 16f; + maxZ = ((float) ((partBoundary.minZ - textureOffset) / wholeVolumeWidth)) * 16f; } float[] uvs = new float[]{minX, minZ, maxX, maxZ}; @@ -170,7 +202,7 @@ public List getQuads(@Nullable final BlockState state, @Nullable fina return super.getQuads(state, null, rand, extraData, renderType); } - coreRadius = Mth.clamp(coreRadius, 9, 24); + coreRadius = Mth.clamp(coreRadius, 9, ThickBranchBlock.MAX_RADIUS_THICK); List quads = new ArrayList<>(30); @@ -198,19 +230,21 @@ public List getQuads(@Nullable final BlockState state, @Nullable fina return quads; } + int arrayIndex = coreRadius - 9; + if (forceRingDir != null) { connections[forceRingDir.get3DDataValue()] = 0; - quads.addAll(this.trunksBotRings[coreRadius - 9].getQuads(state, forceRingDir, rand, extraData, renderType)); + quads.addAll(this.trunksBotRings[arrayIndex].getQuads(state, forceRingDir, rand, extraData, renderType)); } boolean branchesAround = connections[2] + connections[3] + connections[4] + connections[5] != 0; for (Direction face : Direction.values()) { - quads.addAll(this.trunksBark[coreRadius - 9].getQuads(state, face, rand, extraData, renderType)); + quads.addAll(this.trunksBark[arrayIndex].getQuads(state, face, rand, extraData, renderType)); if (face == Direction.UP || face == Direction.DOWN) { if (connections[face.get3DDataValue()] < twigRadius && !branchesAround) { - quads.addAll(this.trunksTopRings[coreRadius - 9].getQuads(state, face, rand, extraData, renderType)); + quads.addAll(this.trunksTopRings[arrayIndex].getQuads(state, face, rand, extraData, renderType)); } else if (connections[face.get3DDataValue()] < coreRadius) { - quads.addAll(this.trunksTopBark[coreRadius - 9].getQuads(state, face, rand, extraData, renderType)); + quads.addAll(this.trunksTopBark[arrayIndex].getQuads(state, face, rand, extraData, renderType)); } } } @@ -218,4 +252,4 @@ public List getQuads(@Nullable final BlockState state, @Nullable fina return quads; } -} \ No newline at end of file +} diff --git a/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java b/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java index 265e54368..ad5b5f5b1 100644 --- a/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java +++ b/src/main/java/com/ferreusveritas/dynamictrees/util/CoordUtils.java @@ -83,6 +83,182 @@ public Surround getOpposite() { } } + /** + * Unified shell direction enum for TrunkShellBlock. + * Contains all 24 positions: 8 inner (distance 1) + 16 outer (distance 2) + */ + /** + * Unified shell direction enum for TrunkShellBlock. + * Contains all 48 positions: 8 inner (distance 1) + 16 outer (distance 2) + 24 outermost (distance 3) + */ + public enum ShellDirection implements StringRepresentable { + // Inner ring (distance 1) - 8 positions + N(0, -1, 1), + NE(1, -1, 1), + E(1, 0, 1), + SE(1, 1, 1), + S(0, 1, 1), + SW(-1, 1, 1), + W(-1, 0, 1), + NW(-1, -1, 1), + + // Outer ring (distance 2) - 16 positions + N2(0, -2, 2), + NE2(1, -2, 2), + E2(2, 0, 2), + SE2(1, 2, 2), + S2(0, 2, 2), + SW2(-1, 2, 2), + W2(-2, 0, 2), + NW2(-1, -2, 2), + NNE(2, -2, 2), + ENE(2, -1, 2), + ESE(2, 1, 2), + SSE(2, 2, 2), + SSW(-2, 2, 2), + WSW(-2, 1, 2), + WNW(-2, -1, 2), + NNW(-2, -2, 2), + + // Outermost ring (distance 3) - 24 positions + N3(0, -3, 3), + E3(3, 0, 3), + S3(0, 3, 3), + W3(-3, 0, 3), + CORNER_NE(3, -3, 3), + CORNER_SE(3, 3, 3), + CORNER_SW(-3, 3, 3), + CORNER_NW(-3, -3, 3), + N3E(1, -3, 3), + N3EE(2, -3, 3), + N3W(-1, -3, 3), + N3WW(-2, -3, 3), + E3N(3, -1, 3), + E3NN(3, -2, 3), + E3S(3, 1, 3), + E3SS(3, 2, 3), + S3E(1, 3, 3), + S3EE(2, 3, 3), + S3W(-1, 3, 3), + S3WW(-2, 3, 3), + W3N(-3, -1, 3), + W3NN(-3, -2, 3), + W3S(-3, 1, 3), + W3SS(-3, 2, 3); + + private static final ShellDirection[] INNER = {N, NE, E, SE, S, SW, W, NW}; + private static final ShellDirection[] OUTER = {N2, NE2, E2, SE2, S2, SW2, W2, NW2, NNE, ENE, ESE, SSE, SSW, WSW, WNW, NNW}; + private static final ShellDirection[] OUTERMOST = {N3, E3, S3, W3, CORNER_NE, CORNER_SE, CORNER_SW, CORNER_NW, N3E, N3EE, N3W, N3WW, E3N, E3NN, E3S, E3SS, S3E, S3EE, S3W, S3WW, W3N, W3NN, W3S, W3SS}; + + private final BlockPos offset; + private final String name; + private final int shellLevel; + + ShellDirection(int x, int z, int shellLevel) { + this.offset = new BlockPos(x, 0, z); + this.name = name().toLowerCase(java.util.Locale.ROOT); + this.shellLevel = shellLevel; + } + + public BlockPos getOffset() { + return offset; + } + + /** Returns 1 for inner, 2 for outer, 3 for outermost */ + public int getShellLevel() { + return shellLevel; + } + + public boolean isInner() { + return shellLevel == 1; + } + + public boolean isOuter() { + return shellLevel == 2; + } + + public boolean isOutermost() { + return shellLevel == 3; + } + + @Override + @Nonnull + public String getSerializedName() { + return name; + } + + public ShellDirection getOpposite() { + return switch (this) { + // Inner ring + case N -> S; + case NE -> SW; + case E -> W; + case SE -> NW; + case S -> N; + case SW -> NE; + case W -> E; + case NW -> SE; + // Outer ring + case N2 -> S2; + case NE2 -> SW2; + case E2 -> W2; + case SE2 -> NW2; + case S2 -> N2; + case SW2 -> NE2; + case W2 -> E2; + case NW2 -> SE2; + case NNE -> SSW; + case ENE -> WSW; + case ESE -> WNW; + case SSE -> NNW; + case SSW -> NNE; + case WSW -> ENE; + case WNW -> ESE; + case NNW -> SSE; + // Outermost ring + case N3 -> S3; + case E3 -> W3; + case S3 -> N3; + case W3 -> E3; + case CORNER_NE -> CORNER_SW; + case CORNER_SE -> CORNER_NW; + case CORNER_SW -> CORNER_NE; + case CORNER_NW -> CORNER_SE; + case N3E -> S3W; + case N3EE -> S3WW; + case N3W -> S3E; + case N3WW -> S3EE; + case E3N -> W3S; + case E3NN -> W3SS; + case E3S -> W3N; + case E3SS -> W3NN; + case S3E -> N3W; + case S3EE -> N3WW; + case S3W -> N3E; + case S3WW -> N3EE; + case W3N -> E3S; + case W3NN -> E3SS; + case W3S -> E3N; + case W3SS -> E3NN; + }; + } + + /** Get inner ring directions only (8 positions at distance 1) */ + public static ShellDirection[] innerValues() { + return INNER; + } + + /** Get outer ring directions only (16 positions at distance 2) */ + public static ShellDirection[] outerValues() { + return OUTER; + } + + /** Get outermost ring directions only (24 positions at distance 3) */ + public static ShellDirection[] outermostValues() { + return OUTERMOST; + } + } + public static boolean isSurroundedByLoadedChunks(Level level, BlockPos pos) { for (Surround surr : CoordUtils.Surround.values()) { Vec3i dir = surr.getOffset();