@@ -1002,14 +1002,15 @@ class NPCImpl(
10021002 }
10031003 }
10041004
1005- // Validate that NPC can stand at the new position (both feet and head must be air)
1006- if (world != null && ! canStandAt(world, newPosition.blockX, newPosition.blockY, newPosition.blockZ)) {
1007- logDebug(" [NPC] moveTowards: Cannot stand at ${newPosition.blockX} ,${newPosition.blockY} ,${newPosition.blockZ} - collision detected, aborting movement" )
1005+ // Validate that NPC can stand at the new position (both feet and head must have no collision)
1006+ // Use the actual Y position (can be fractional for slabs/stairs)
1007+ if (world != null && ! canStandAt(world, newPosition.blockX, newPosition.y, newPosition.blockZ)) {
1008+ logDebug(" [NPC] moveTowards: Cannot stand at ${newPosition.blockX} ,${newPosition.y} ,${newPosition.blockZ} - collision detected, aborting movement" )
10081009 // Try to find a valid Y position nearby
10091010 // Search downward first (most common case - ceiling collision)
1010- var foundValidY: Int ? = null
1011+ var foundValidY: Double ? = null
10111012 for (offset in 0 .. 3 ) {
1012- val testY = newPosition.blockY - offset
1013+ val testY = newPosition.y - offset
10131014 if (testY >= 0 && canStandAt(world, newPosition.blockX, testY, newPosition.blockZ)) {
10141015 foundValidY = testY
10151016 break
@@ -1018,7 +1019,7 @@ class NPCImpl(
10181019 // If not found downward, try upward (floor collision)
10191020 if (foundValidY == null ) {
10201021 for (offset in 1 .. 3 ) {
1021- val testY = newPosition.blockY + offset
1022+ val testY = newPosition.y + offset
10221023 if (canStandAt(world, newPosition.blockX, testY, newPosition.blockZ)) {
10231024 foundValidY = testY
10241025 break
@@ -1027,7 +1028,7 @@ class NPCImpl(
10271028 }
10281029
10291030 if (foundValidY != null ) {
1030- newPosition.y = foundValidY.toDouble() + 0.5 // Center in block
1031+ newPosition.y = foundValidY
10311032 logDebug(" [NPC] moveTowards: Adjusted Y to ${newPosition.y} to avoid collision" )
10321033 } else {
10331034 // No valid position found, abort movement for this tick
@@ -1152,16 +1153,20 @@ class NPCImpl(
11521153 // Get the top Y of this block using collision shape
11531154 val blockTopY = getBlockTopY(block, y)
11541155
1155- // Check if there's passable space above the block for NPC to stand
1156- // We need space at the block top level (feet) and above ( head) for a 2-block tall NPC
1157- val feetY = blockTopY.toInt()
1158- val headY = feetY + 1
1156+ // NPC needs space for head (2 blocks tall)
1157+ // Feet are at blockTopY, head is at blockTopY + 1.8
1158+ val headY = blockTopY + 1.8
1159+ val headBlockY = headY.toInt()
11591160
1160- // Check if blocks at feet and head level are passable
1161- val feetBlock = world.getBlockAt(x, feetY, z)
1162- val headBlock = world.getBlockAt(x, headY, z)
1161+ // Check if there's enough headroom - the block at head level must be passable
1162+ // For feet: NPC stands ON the block (at blockTopY), so we don't need to check
1163+ // if the block itself is passable - it's the walkable surface
1164+ val headBlock = world.getBlockAt(x, headBlockY, z)
11631165
1164- if (isPassable(feetBlock) && isPassable(headBlock)) {
1166+ // Also check the block above head (for 2-block tall NPC)
1167+ val headBlockAbove = world.getBlockAt(x, headBlockY + 1 , z)
1168+
1169+ if (isPassable(headBlock) && isPassable(headBlockAbove)) {
11651170 // Sufficient space, return the top of this block
11661171 return blockTopY
11671172 }
@@ -1173,19 +1178,67 @@ class NPCImpl(
11731178 /* *
11741179 * Checks if the NPC can stand at the given position using collision shapes.
11751180 * Checks if both feet and head positions have no collision.
1181+ * Uses collision shapes to accurately detect if there's collision at the specific Y coordinates.
11761182 * @param world The world to check in
11771183 * @param x The X coordinate
1178- * @param y The Y coordinate (feet level)
1184+ * @param y The Y coordinate (feet level, can be fractional like 100.5 for slabs )
11791185 * @param z The Z coordinate
11801186 * @return true if both the feet and head positions have no collision
11811187 */
1182- private fun canStandAt (world : World , x : Int , y : Int , z : Int ): Boolean {
1183- val blockAtFeet = world.getBlockAt(x, y, z)
1184- val blockAtHead = world.getBlockAt(x, y + 1 , z)
1188+ private fun canStandAt (world : World , x : Int , y : Double , z : Int ): Boolean {
1189+ // For head position, check if the block is passable
1190+ val headY = (y + 1.8 ).toInt() // NPC is ~1.8 blocks tall
1191+ val blockAtHead = world.getBlockAt(x, headY, z)
1192+ if (! isPassable(blockAtHead)) {
1193+ return false
1194+ }
1195+
1196+ // Also check the block above head
1197+ val blockAboveHead = world.getBlockAt(x, headY + 1 , z)
1198+ if (! isPassable(blockAboveHead)) {
1199+ return false
1200+ }
11851201
1186- // Check collision shapes at both positions
1187- // Both blocks must be passable (no collision)
1188- return isPassable(blockAtFeet) && isPassable(blockAtHead)
1202+ // For feet position, use collision shape to check if there's collision at the specific Y
1203+ // NPC stands ON blocks, not IN them, so we need to check collision at the feet Y coordinate
1204+ val feetBlockY = y.toInt()
1205+ val blockAtFeet = world.getBlockAt(x, feetBlockY, z)
1206+ val relativeY = y - feetBlockY // Y position relative to the block (0.0 to 1.0)
1207+
1208+ try {
1209+ val collisionShape = blockAtFeet.collisionShape
1210+ if (collisionShape != null ) {
1211+ // Create a small bounding box representing the NPC's feet at this Y position
1212+ // NPC feet are roughly 0.6x0.6 at the center of the block
1213+ val feetCheckBox = org.bukkit.util.BoundingBox (
1214+ 0.2 , relativeY - 0.1 , 0.2 , // min (relative to block)
1215+ 0.8 , relativeY + 0.1 , 0.8 // max (relative to block)
1216+ )
1217+
1218+ // If the collision shape overlaps at the feet position, there's collision
1219+ // But if relativeY is at or above the top of the collision shape, it's fine (NPC stands ON it)
1220+ val boundingBoxes = collisionShape.boundingBoxes
1221+ if (boundingBoxes.isNotEmpty()) {
1222+ val maxBlockY = boundingBoxes.maxOfOrNull { it.maxY } ? : 1.0
1223+ // If feet Y is at or above the top of the collision shape, NPC can stand on it
1224+ if (relativeY >= maxBlockY - 0.1 ) {
1225+ return true // Standing on top of the block
1226+ }
1227+ // Otherwise, check if there's collision
1228+ if (collisionShape.overlaps(feetCheckBox)) {
1229+ return false // Collision at feet
1230+ }
1231+ }
1232+ }
1233+ } catch (e: Exception ) {
1234+ // Fallback: if block is passable, no collision
1235+ // But if NPC is standing ON the block (relativeY > 0.5), allow it
1236+ if (relativeY <= 0.5 && ! isPassable(blockAtFeet)) {
1237+ return false
1238+ }
1239+ }
1240+
1241+ return true
11891242 }
11901243
11911244 /* *
0 commit comments