Skip to content

Commit bc5f6ea

Browse files
committed
fix: npc movement slaps
1 parent 91a8a1f commit bc5f6ea

1 file changed

Lines changed: 75 additions & 22 deletions

File tree

src/main/kotlin/cc/modlabs/kpaper/npc/NPCImpl.kt

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)