@@ -9,6 +9,10 @@ import org.bukkit.Bukkit
99import org.bukkit.Location
1010import org.bukkit.Material
1111import org.bukkit.World
12+ import org.bukkit.block.Block
13+ import org.bukkit.block.data.Bisected
14+ import org.bukkit.block.data.BlockData
15+ import org.bukkit.block.data.type.Slab
1216import org.bukkit.entity.Entity
1317import org.bukkit.entity.LivingEntity
1418import org.bukkit.entity.Mannequin
@@ -917,7 +921,39 @@ class NPCImpl(
917921 val needsJump = usePathfinding && Pathfinder .needsJump(entity, target)
918922
919923 // Check for ground below the new horizontal position
920- val world = currentLoc.world
924+ val world = currentLoc.world ? : return
925+
926+ // Check collision before attempting to move - ensure we can walk through blocks at the target position
927+ val targetBlockX = newPosition.blockX
928+ val targetBlockZ = newPosition.blockZ
929+ val currentBlockY = currentLoc.blockY
930+
931+ // Check if we can walk through blocks at feet and head level at the target position
932+ if (! canWalkThrough(world, targetBlockX, currentBlockY, targetBlockZ) ||
933+ ! canWalkThrough(world, targetBlockX, currentBlockY + 1 , targetBlockZ)) {
934+ logDebug(" [NPC] moveTowards: Cannot walk through blocks at ${targetBlockX} ,${currentBlockY} ,${targetBlockZ} - collision detected" )
935+ // Try to find a valid path by checking adjacent blocks
936+ // Check if we can move in X or Z direction separately
937+ val canMoveX = canWalkThrough(world, targetBlockX, currentBlockY, currentLoc.blockZ) &&
938+ canWalkThrough(world, targetBlockX, currentBlockY + 1 , currentLoc.blockZ)
939+ val canMoveZ = canWalkThrough(world, currentLoc.blockX, currentBlockY, targetBlockZ) &&
940+ canWalkThrough(world, currentLoc.blockX, currentBlockY + 1 , targetBlockZ)
941+
942+ if (canMoveX && Math .abs(horizontalDirection.x) > Math .abs(horizontalDirection.z)) {
943+ // Can move in X direction, adjust position
944+ newPosition.z = currentLoc.z
945+ logDebug(" [NPC] moveTowards: Adjusted to move only in X direction" )
946+ } else if (canMoveZ && Math .abs(horizontalDirection.z) > Math .abs(horizontalDirection.x)) {
947+ // Can move in Z direction, adjust position
948+ newPosition.x = currentLoc.x
949+ logDebug(" [NPC] moveTowards: Adjusted to move only in Z direction" )
950+ } else {
951+ // Cannot move in either direction, abort
952+ logDebug(" [NPC] moveTowards: Cannot move in any direction, aborting" )
953+ return
954+ }
955+ }
956+
921957 val groundY = findGroundLevel(world, newPosition.blockX, newPosition.blockZ, currentLoc.blockY.toInt())
922958
923959 // Handle vertical positioning
@@ -934,6 +970,11 @@ class NPCImpl(
934970 } else if (groundY != null ) {
935971 // There's ground below - check if we should be on it or falling to it
936972 val distanceToGround = currentLoc.y - groundY
973+
974+ // Check if the ground is a slab for smoother positioning
975+ val groundBlock = world.getBlockAt(newPosition.blockX, (groundY - 1 ).toInt(), newPosition.blockZ)
976+ val isGroundSlab = isSlab(groundBlock)
977+
937978 if (distanceToGround > 0.1 ) {
938979 // We're above ground - simulate gravity (fall down)
939980 // Gravity: 0.08 blocks per tick (1.6 blocks per second)
@@ -942,12 +983,24 @@ class NPCImpl(
942983 logDebug(" [NPC] moveTowards: Falling - distanceToGround=$distanceToGround , fallDistance=$fallDistance , newY=${newPosition.y} " )
943984 } else if (distanceToGround < - 0.1 ) {
944985 // We're below ground - place on ground
945- newPosition.y = groundY
946- logDebug(" [NPC] moveTowards: Below ground, placed on ground at Y=$groundY " )
986+ // For slabs, use the exact slab top Y for smoother walking
987+ if (isGroundSlab) {
988+ newPosition.y = groundY
989+ } else {
990+ newPosition.y = groundY
991+ }
992+ logDebug(" [NPC] moveTowards: Below ground, placed on ground at Y=$groundY (slab=$isGroundSlab )" )
947993 } else {
948- // We're on ground - stay there
949- newPosition.y = currentLoc.y
950- logDebug(" [NPC] moveTowards: On ground, maintaining Y=${newPosition.y} " )
994+ // We're on ground - stay there or adjust to slab height if needed
995+ if (isGroundSlab && Math .abs(currentLoc.y - groundY) > 0.1 ) {
996+ // Adjust to slab height for smoother walking
997+ newPosition.y = groundY
998+ logDebug(" [NPC] moveTowards: Adjusted to slab height Y=$groundY " )
999+ } else {
1000+ // We're on ground - stay there
1001+ newPosition.y = currentLoc.y
1002+ logDebug(" [NPC] moveTowards: On ground, maintaining Y=${newPosition.y} " )
1003+ }
9511004 }
9521005 } else {
9531006 // No ground found - keep current Y (might be in air, let it fall naturally if gravity is enabled)
@@ -1004,40 +1057,227 @@ class NPCImpl(
10041057 }
10051058
10061059 /* *
1007- * Finds the ground level (top solid block) at the given X, Z coordinates.
1060+ * Checks if a block is a slab (half-height block).
1061+ */
1062+ private fun isSlab (block : Block ): Boolean {
1063+ return block.blockData is Slab
1064+ }
1065+
1066+ /* *
1067+ * Gets the top Y coordinate of a slab block.
1068+ * @param block The slab block
1069+ * @param blockY The Y coordinate of the block
1070+ * @return The Y coordinate where the top of the slab is (0.5 for bottom, 1.0 for top, 0.5 for double)
1071+ */
1072+ private fun getSlabTopY (block : Block , blockY : Int ): Double {
1073+ val blockData = block.blockData
1074+ if (blockData is Slab ) {
1075+ return when (blockData.type) {
1076+ Slab .Type .BOTTOM -> blockY + 0.5
1077+ Slab .Type .TOP -> blockY + 1.0
1078+ Slab .Type .DOUBLE -> blockY + 1.0
1079+ }
1080+ }
1081+ return blockY + 1.0
1082+ }
1083+
1084+ /* *
1085+ * Checks if a block is passable (can be walked through) using the official Paper API.
1086+ * Uses Block.isPassable() which checks if the block has no colliding parts.
1087+ * @see org.bukkit.block.Block#isPassable()
1088+ */
1089+ private fun isPassable (block : Block ): Boolean {
1090+ // Use the built-in isPassable() method from Paper API
1091+ // This is more accurate as it uses the block's collision shape internally
1092+ return block.isPassable()
1093+ }
1094+
1095+ /* *
1096+ * Checks if a block can be walked on using collision shape.
1097+ * A block is walkable if it has a collision shape (even if partial, like slabs).
1098+ * Uses Block.isCollidable() and collision shape for accurate detection.
1099+ */
1100+ private fun isWalkable (block : Block ): Boolean {
1101+ val type = block.type
1102+ if (type == Material .BARRIER ) return false
1103+
1104+ // Use isCollidable() to check if block has collision
1105+ // This is more accurate than checking isSolid
1106+ if (block.isCollidable()) {
1107+ return true
1108+ }
1109+
1110+ // Also check collision shape directly for blocks that might be walkable
1111+ // but not marked as collidable (edge cases)
1112+ try {
1113+ val collisionShape = block.collisionShape
1114+ // If collision shape exists and has bounding boxes, block can be walked on
1115+ // This handles slabs, stairs, and other partial blocks correctly
1116+ if (collisionShape != null ) {
1117+ val boundingBoxes = collisionShape.boundingBoxes
1118+ return boundingBoxes.isNotEmpty()
1119+ }
1120+ return false
1121+ } catch (e: Exception ) {
1122+ // Fallback to material check if collision shape API is not available
1123+ return type.isSolid || isSlab(block)
1124+ }
1125+ }
1126+
1127+ /* *
1128+ * Gets the top Y coordinate of a block based on its collision shape.
1129+ * Uses the collision shape's bounding box for accurate height detection.
1130+ * For slabs, this returns the actual top of the slab.
1131+ * For full blocks, returns blockY + 1.0.
1132+ * @see org.bukkit.block.Block#getCollisionShape()
1133+ */
1134+ private fun getBlockTopY (block : Block , blockY : Int ): Double {
1135+ // Handle slabs specially for precise positioning
1136+ if (isSlab(block)) {
1137+ return getSlabTopY(block, blockY)
1138+ }
1139+
1140+ // For other blocks, use collision shape to determine top
1141+ try {
1142+ val collisionShape = block.collisionShape
1143+ if (collisionShape != null ) {
1144+ // Get all bounding boxes from the collision shape
1145+ // VoxelShape.getBoundingBoxes() returns a Collection<BoundingBox>
1146+ val boundingBoxes = collisionShape.boundingBoxes
1147+ if (boundingBoxes.isNotEmpty()) {
1148+ // Find the maximum Y from all bounding boxes
1149+ val maxY = boundingBoxes.maxOfOrNull { it.maxY } ? : 1.0
1150+ return blockY + maxY
1151+ }
1152+ }
1153+ } catch (e: Exception ) {
1154+ // Fallback: use block's approximate bounding box
1155+ try {
1156+ val boundingBox = block.boundingBox
1157+ if (boundingBox != null ) {
1158+ // Check if bounding box has volume (not degenerate)
1159+ val volume = boundingBox.volume
1160+ if (volume > 0.0 ) {
1161+ return blockY + boundingBox.maxY
1162+ }
1163+ }
1164+ } catch (e2: Exception ) {
1165+ // If all else fails, assume full block
1166+ }
1167+ }
1168+
1169+ // Default: full block
1170+ return blockY + 1.0
1171+ }
1172+
1173+ /* *
1174+ * Finds the ground level (top solid block or slab) at the given X, Z coordinates.
10081175 * Returns null if no solid ground is found within reasonable range.
10091176 * Only returns a ground level if there's sufficient air space above for the NPC to stand.
1177+ * Uses collision shapes for accurate detection.
10101178 */
10111179 private fun findGroundLevel (world : World , x : Int , z : Int , startY : Int ): Double? {
10121180 // Search from startY + 2 down to startY - 10
10131181 for (y in (startY + 2 ).downTo(startY - 10 )) {
10141182 val block = world.getBlockAt(x, y, z)
1015- if (block.type.isSolid && block.type != Material .BARRIER ) {
1016- // Found solid ground, check if there's air space above for NPC to stand
1017- // Check air at y + 1 (feet) and y + 2 (head) for a 2-block tall NPC
1018- val above = world.getBlockAt(x, y + 1 , z)
1019- val above2 = world.getBlockAt(x, y + 2 , z)
1020- if (above.type.isAir && above2.type.isAir) {
1021- // Sufficient air space, return the top of this block
1022- return (y + 1 ).toDouble()
1183+
1184+ // Check if this is a walkable block using collision shape
1185+ if (isWalkable(block)) {
1186+ // Get the top Y of this block using collision shape
1187+ val blockTopY = getBlockTopY(block, y)
1188+
1189+ // Check if there's passable space above the block for NPC to stand
1190+ // We need space at the block top level (feet) and above (head) for a 2-block tall NPC
1191+ val feetY = blockTopY.toInt()
1192+ val headY = feetY + 1
1193+
1194+ // Check if blocks at feet and head level are passable
1195+ val feetBlock = world.getBlockAt(x, feetY, z)
1196+ val headBlock = world.getBlockAt(x, headY, z)
1197+
1198+ if (isPassable(feetBlock) && isPassable(headBlock)) {
1199+ // Sufficient space, return the top of this block
1200+ return blockTopY
10231201 }
10241202 }
10251203 }
10261204 return null
10271205 }
10281206
10291207 /* *
1030- * Checks if the NPC can stand at the given position (both feet and head blocks must be air).
1208+ * Checks if the NPC can stand at the given position using collision shapes.
1209+ * Checks if both feet and head positions have no collision.
10311210 * @param world The world to check in
10321211 * @param x The X coordinate
10331212 * @param y The Y coordinate (feet level)
10341213 * @param z The Z coordinate
1035- * @return true if both the feet and head positions are air blocks
1214+ * @return true if both the feet and head positions have no collision
10361215 */
10371216 private fun canStandAt (world : World , x : Int , y : Int , z : Int ): Boolean {
10381217 val blockAtFeet = world.getBlockAt(x, y, z)
10391218 val blockAtHead = world.getBlockAt(x, y + 1 , z)
1040- return blockAtFeet.type.isAir && blockAtHead.type.isAir
1219+
1220+ // Check collision shapes at both positions
1221+ // Both blocks must be passable (no collision)
1222+ return isPassable(blockAtFeet) && isPassable(blockAtHead)
1223+ }
1224+
1225+ /* *
1226+ * Checks if the NPC can walk through the block at the given position using collision shape.
1227+ * This checks if the block has no collision at this position.
1228+ * @param world The world to check in
1229+ * @param x The X coordinate
1230+ * @param y The Y coordinate
1231+ * @param z The Z coordinate
1232+ * @return true if the block is passable (can walk through)
1233+ */
1234+ private fun canWalkThrough (world : World , x : Int , y : Int , z : Int ): Boolean {
1235+ val block = world.getBlockAt(x, y, z)
1236+ // Use collision shape to determine if block can be walked through
1237+ return isPassable(block)
1238+ }
1239+
1240+ /* *
1241+ * Checks if a position within a block has collision using the block's collision shape.
1242+ * This provides more precise collision detection than just checking if the block is passable.
1243+ * Uses Block.getCollisionShape() and VoxelShape.overlaps() for accurate collision detection.
1244+ * @param block The block to check
1245+ * @param relativeY The Y position relative to the block (0.0 to 1.0)
1246+ * @return true if there's collision at this relative Y position
1247+ * @see org.bukkit.block.Block#getCollisionShape()
1248+ * @see org.bukkit.util.VoxelShape#overlaps(org.bukkit.util.BoundingBox)
1249+ */
1250+ private fun hasCollisionAt (block : Block , relativeY : Double ): Boolean {
1251+ try {
1252+ val collisionShape = block.collisionShape
1253+ if (collisionShape == null ) {
1254+ return false // No collision
1255+ }
1256+ // Check if collision shape has any bounding boxes
1257+ val boundingBoxes = collisionShape.boundingBoxes
1258+ if (boundingBoxes.isEmpty()) {
1259+ return false // No collision
1260+ }
1261+
1262+ // Create a bounding box representing the NPC's collision area at this Y position
1263+ // We check a small area (0.6x0.6) at the center of the block at this Y level
1264+ // This represents the NPC's collision box at this height
1265+ val minX = 0.2
1266+ val maxX = 0.8
1267+ val minZ = 0.2
1268+ val maxZ = 0.8
1269+ val minY = relativeY - 0.1
1270+ val maxY = relativeY + 0.1
1271+
1272+ // Create a BoundingBox for the NPC's collision area
1273+ val npcBoundingBox = org.bukkit.util.BoundingBox (minX, minY, minZ, maxX, maxY, maxZ)
1274+
1275+ // Use VoxelShape.overlaps() to check if the collision shape intersects with our bounding box
1276+ return collisionShape.overlaps(npcBoundingBox)
1277+ } catch (e: Exception ) {
1278+ // Fallback to simple passable check using official API
1279+ return ! block.isPassable()
1280+ }
10411281 }
10421282
10431283 /* *
0 commit comments