Skip to content

Commit d3e3ad0

Browse files
committed
fix: npc teleport
1 parent ebf6cec commit d3e3ad0

1 file changed

Lines changed: 258 additions & 18 deletions

File tree

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

Lines changed: 258 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import org.bukkit.Bukkit
99
import org.bukkit.Location
1010
import org.bukkit.Material
1111
import 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
1216
import org.bukkit.entity.Entity
1317
import org.bukkit.entity.LivingEntity
1418
import 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

Comments
 (0)