@@ -956,6 +956,40 @@ class NPCImpl(
956956 }
957957 }
958958
959+ // Validate that NPC can stand at the new position (both feet and head must be air)
960+ if (world != null && ! canStandAt(world, newPosition.blockX, newPosition.blockY, newPosition.blockZ)) {
961+ logDebug(" [NPC] moveTowards: Cannot stand at ${newPosition.blockX} ,${newPosition.blockY} ,${newPosition.blockZ} - collision detected, aborting movement" )
962+ // Try to find a valid Y position nearby
963+ // Search downward first (most common case - ceiling collision)
964+ var foundValidY: Int? = null
965+ for (offset in 0 .. 3 ) {
966+ val testY = newPosition.blockY - offset
967+ if (testY >= 0 && canStandAt(world, newPosition.blockX, testY, newPosition.blockZ)) {
968+ foundValidY = testY
969+ break
970+ }
971+ }
972+ // If not found downward, try upward (floor collision)
973+ if (foundValidY == null ) {
974+ for (offset in 1 .. 3 ) {
975+ val testY = newPosition.blockY + offset
976+ if (canStandAt(world, newPosition.blockX, testY, newPosition.blockZ)) {
977+ foundValidY = testY
978+ break
979+ }
980+ }
981+ }
982+
983+ if (foundValidY != null ) {
984+ newPosition.y = foundValidY.toDouble() + 0.5 // Center in block
985+ logDebug(" [NPC] moveTowards: Adjusted Y to ${newPosition.y} to avoid collision" )
986+ } else {
987+ // No valid position found, abort movement for this tick
988+ logDebug(" [NPC] moveTowards: No valid position found, aborting movement" )
989+ return
990+ }
991+ }
992+
959993 // Make entity look at target
960994 val lookDirection = target.toVector().subtract(newPosition.toVector())
961995 val yaw = Math .toDegrees(- atan2(lookDirection.x, lookDirection.z)).toFloat()
@@ -972,19 +1006,40 @@ class NPCImpl(
9721006 /* *
9731007 * Finds the ground level (top solid block) at the given X, Z coordinates.
9741008 * Returns null if no solid ground is found within reasonable range.
1009+ * Only returns a ground level if there's sufficient air space above for the NPC to stand.
9751010 */
9761011 private fun findGroundLevel (world : World , x : Int , z : Int , startY : Int ): Double? {
9771012 // Search from startY + 2 down to startY - 10
9781013 for (y in (startY + 2 ).downTo(startY - 10 )) {
9791014 val block = world.getBlockAt(x, y, z)
9801015 if (block.type.isSolid && block.type != Material .BARRIER ) {
981- // Found solid ground, return the top of this block
982- return (y + 1 ).toDouble()
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()
1023+ }
9831024 }
9841025 }
9851026 return null
9861027 }
9871028
1029+ /* *
1030+ * Checks if the NPC can stand at the given position (both feet and head blocks must be air).
1031+ * @param world The world to check in
1032+ * @param x The X coordinate
1033+ * @param y The Y coordinate (feet level)
1034+ * @param z The Z coordinate
1035+ * @return true if both the feet and head positions are air blocks
1036+ */
1037+ private fun canStandAt (world : World , x : Int , y : Int , z : Int ): Boolean {
1038+ val blockAtFeet = world.getBlockAt(x, y, z)
1039+ val blockAtHead = world.getBlockAt(x, y + 1 , z)
1040+ return blockAtFeet.type.isAir && blockAtHead.type.isAir
1041+ }
1042+
9881043 /* *
9891044 * Enable or disable pathfinding for this NPC.
9901045 * When enabled, NPCs will use A* pathfinding to navigate around obstacles and jump when needed.
0 commit comments