package net.minecraft.world.level.block; import com.mojang.serialization.MapCodec; import java.util.Map; import java.util.function.BiConsumer; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.stats.Stats; import net.minecraft.util.RandomSource; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Explosion; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.ScheduledTickAccess; import net.minecraft.world.level.block.entity.BellBlockEntity; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition.Builder; import net.minecraft.world.level.block.state.properties.BellAttachType; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.pathfinder.PathComputationType; import net.minecraft.world.level.redstone.Orientation; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; public class BellBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(BellBlock::new); public static final EnumProperty FACING = HorizontalDirectionalBlock.FACING; public static final EnumProperty ATTACHMENT = BlockStateProperties.BELL_ATTACHMENT; public static final BooleanProperty POWERED = BlockStateProperties.POWERED; private static final VoxelShape BELL_SHAPE = Shapes.or(Block.column(6.0, 6.0, 13.0), Block.column(8.0, 4.0, 6.0)); private static final VoxelShape SHAPE_CEILING = Shapes.or(BELL_SHAPE, Block.column(2.0, 13.0, 16.0)); private static final Map SHAPE_FLOOR = Shapes.rotateHorizontalAxis(Block.cube(16.0, 16.0, 8.0)); private static final Map SHAPE_DOUBLE_WALL = Shapes.rotateHorizontalAxis( Shapes.or(BELL_SHAPE, Block.column(2.0, 16.0, 13.0, 15.0)) ); private static final Map SHAPE_SINGLE_WALL = Shapes.rotateHorizontal(Shapes.or(BELL_SHAPE, Block.boxZ(2.0, 13.0, 15.0, 0.0, 13.0))); public static final int EVENT_BELL_RING = 1; @Override public MapCodec codec() { return CODEC; } public BellBlock(BlockBehaviour.Properties properties) { super(properties); this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(ATTACHMENT, BellAttachType.FLOOR).setValue(POWERED, false)); } @Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { boolean bl = level.hasNeighborSignal(pos); if (bl != (Boolean)state.getValue(POWERED)) { if (bl) { this.attemptToRing(level, pos, null); } level.setBlock(pos, state.setValue(POWERED, bl), 3); } } @Override protected void onProjectileHit(Level level, BlockState state, BlockHitResult hit, Projectile projectile) { Player player2 = projectile.getOwner() instanceof Player player ? player : null; this.onHit(level, state, hit, player2, true); } @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { return (InteractionResult)(this.onHit(level, state, hitResult, player, true) ? InteractionResult.SUCCESS : InteractionResult.PASS); } public boolean onHit(Level level, BlockState state, BlockHitResult result, @Nullable Player player, boolean canRingBell) { Direction direction = result.getDirection(); BlockPos blockPos = result.getBlockPos(); boolean bl = !canRingBell || this.isProperHit(state, direction, result.getLocation().y - blockPos.getY()); if (bl) { boolean bl2 = this.attemptToRing(player, level, blockPos, direction); if (bl2 && player != null) { player.awardStat(Stats.BELL_RING); } return true; } else { return false; } } /** * @return true if the bell can be rung from the given side and vertical position. For example, bells attached to their northern neighbor cannot be rung from the south face, since it can't swing north-south. */ private boolean isProperHit(BlockState pos, Direction direction, double distanceY) { if (direction.getAxis() != Direction.Axis.Y && !(distanceY > 0.8124F)) { Direction direction2 = pos.getValue(FACING); BellAttachType bellAttachType = pos.getValue(ATTACHMENT); switch (bellAttachType) { case FLOOR: return direction2.getAxis() == direction.getAxis(); case SINGLE_WALL: case DOUBLE_WALL: return direction2.getAxis() != direction.getAxis(); case CEILING: return true; default: return false; } } else { return false; } } public boolean attemptToRing(Level level, BlockPos pos, @Nullable Direction direction) { return this.attemptToRing(null, level, pos, direction); } public boolean attemptToRing(@Nullable Entity entity, Level level, BlockPos pos, @Nullable Direction direction) { BlockEntity blockEntity = level.getBlockEntity(pos); if (!level.isClientSide && blockEntity instanceof BellBlockEntity) { if (direction == null) { direction = level.getBlockState(pos).getValue(FACING); } ((BellBlockEntity)blockEntity).onHit(direction); level.playSound(null, pos, SoundEvents.BELL_BLOCK, SoundSource.BLOCKS, 2.0F, 1.0F); level.gameEvent(entity, GameEvent.BLOCK_CHANGE, pos); return true; } else { return false; } } private VoxelShape getVoxelShape(BlockState state) { Direction direction = state.getValue(FACING); return switch ((BellAttachType)state.getValue(ATTACHMENT)) { case FLOOR -> (VoxelShape)SHAPE_FLOOR.get(direction.getAxis()); case SINGLE_WALL -> (VoxelShape)SHAPE_SINGLE_WALL.get(direction); case DOUBLE_WALL -> (VoxelShape)SHAPE_DOUBLE_WALL.get(direction.getAxis()); case CEILING -> SHAPE_CEILING; }; } @Override protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { return this.getVoxelShape(state); } @Override protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { return this.getVoxelShape(state); } @Nullable @Override public BlockState getStateForPlacement(BlockPlaceContext context) { Direction direction = context.getClickedFace(); BlockPos blockPos = context.getClickedPos(); Level level = context.getLevel(); Direction.Axis axis = direction.getAxis(); if (axis == Direction.Axis.Y) { BlockState blockState = this.defaultBlockState() .setValue(ATTACHMENT, direction == Direction.DOWN ? BellAttachType.CEILING : BellAttachType.FLOOR) .setValue(FACING, context.getHorizontalDirection()); if (blockState.canSurvive(context.getLevel(), blockPos)) { return blockState; } } else { boolean bl = axis == Direction.Axis.X && level.getBlockState(blockPos.west()).isFaceSturdy(level, blockPos.west(), Direction.EAST) && level.getBlockState(blockPos.east()).isFaceSturdy(level, blockPos.east(), Direction.WEST) || axis == Direction.Axis.Z && level.getBlockState(blockPos.north()).isFaceSturdy(level, blockPos.north(), Direction.SOUTH) && level.getBlockState(blockPos.south()).isFaceSturdy(level, blockPos.south(), Direction.NORTH); BlockState blockState = this.defaultBlockState() .setValue(FACING, direction.getOpposite()) .setValue(ATTACHMENT, bl ? BellAttachType.DOUBLE_WALL : BellAttachType.SINGLE_WALL); if (blockState.canSurvive(context.getLevel(), context.getClickedPos())) { return blockState; } boolean bl2 = level.getBlockState(blockPos.below()).isFaceSturdy(level, blockPos.below(), Direction.UP); blockState = blockState.setValue(ATTACHMENT, bl2 ? BellAttachType.FLOOR : BellAttachType.CEILING); if (blockState.canSurvive(context.getLevel(), context.getClickedPos())) { return blockState; } } return null; } @Override protected void onExplosionHit(BlockState state, ServerLevel level, BlockPos pos, Explosion explosion, BiConsumer dropConsumer) { if (explosion.canTriggerBlocks()) { this.attemptToRing(level, pos, null); } super.onExplosionHit(state, level, pos, explosion, dropConsumer); } @Override protected BlockState updateShape( BlockState state, LevelReader level, ScheduledTickAccess scheduledTickAccess, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random ) { BellAttachType bellAttachType = state.getValue(ATTACHMENT); Direction direction2 = getConnectedDirection(state).getOpposite(); if (direction2 == direction && !state.canSurvive(level, pos) && bellAttachType != BellAttachType.DOUBLE_WALL) { return Blocks.AIR.defaultBlockState(); } else { if (direction.getAxis() == ((Direction)state.getValue(FACING)).getAxis()) { if (bellAttachType == BellAttachType.DOUBLE_WALL && !neighborState.isFaceSturdy(level, neighborPos, direction)) { return state.setValue(ATTACHMENT, BellAttachType.SINGLE_WALL).setValue(FACING, direction.getOpposite()); } if (bellAttachType == BellAttachType.SINGLE_WALL && direction2.getOpposite() == direction && neighborState.isFaceSturdy(level, neighborPos, state.getValue(FACING))) { return state.setValue(ATTACHMENT, BellAttachType.DOUBLE_WALL); } } return super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random); } } @Override protected boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) { Direction direction = getConnectedDirection(state).getOpposite(); return direction == Direction.UP ? Block.canSupportCenter(level, pos.above(), Direction.DOWN) : FaceAttachedHorizontalDirectionalBlock.canAttach(level, pos, direction); } private static Direction getConnectedDirection(BlockState state) { switch ((BellAttachType)state.getValue(ATTACHMENT)) { case FLOOR: return Direction.UP; case CEILING: return Direction.DOWN; default: return ((Direction)state.getValue(FACING)).getOpposite(); } } @Override protected void createBlockStateDefinition(Builder builder) { builder.add(FACING, ATTACHMENT, POWERED); } @Nullable @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new BellBlockEntity(pos, state); } @Nullable @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { return createTickerHelper(blockEntityType, BlockEntityType.BELL, level.isClientSide ? BellBlockEntity::clientTick : BellBlockEntity::serverTick); } @Override protected boolean isPathfindable(BlockState state, PathComputationType pathComputationType) { return false; } @Override public BlockState rotate(BlockState state, Rotation rotation) { return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); } @Override public BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.getRotation(state.getValue(FACING))); } }