22
33import com .github .retrooper .packetevents .PacketEventsAPI ;
44import com .github .retrooper .packetevents .protocol .world .Location ;
5- import com .github .retrooper .packetevents .util .Vector3f ;
65import java .util .Map ;
76import java .util .UUID ;
8- import java .util .concurrent .CompletableFuture ;
97import java .util .concurrent .ConcurrentHashMap ;
10-
118import com .github .retrooper .packetevents .wrapper .play .server .WrapperPlayServerEntityHeadLook ;
129import me .tofaa .entitylib .EntityLib ;
13- import me .tofaa .entitylib .meta .display .AbstractDisplayMeta ;
1410import me .tofaa .entitylib .movement .MovementEngine ;
15- import me .tofaa .entitylib .movement .MovementEngine .MovementSettings ;
16- import me .tofaa .entitylib .movement .MovementEngine .Path ;
17- import me .tofaa .entitylib .movement .MovementEngine .PathfindSettings ;
1811import me .tofaa .entitylib .movement .SpigotMovementEngine ;
1912import me .tofaa .entitylib .npc .path .NPCPath ;
2013import me .tofaa .entitylib .npc .NPCRegistry ;
2114import me .tofaa .entitylib .wrapper .WrapperEntity ;
2215import org .bukkit .Bukkit ;
23- import org .bukkit .Material ;
2416import org .bukkit .World ;
2517import org .bukkit .block .Block ;
2618import org .bukkit .scheduler .BukkitTask ;
2719import org .bukkit .entity .Player ;
2820import org .jetbrains .annotations .NotNull ;
29- import org .jetbrains .annotations .Nullable ;
3021
3122public class NPCMovement {
3223
@@ -103,15 +94,7 @@ private static void processViewerSync() {
10394 Location npcLocation = entity .getLocation ();
10495
10596 npc .getHologram ().ifPresent (hologram -> {
106- double yOffset = npc .getOptions ().isSitting () ? 2.76 : 2.26 ;
107- Location holoLoc = new Location (
108- npcLocation .getX (),
109- npcLocation .getY () + yOffset ,
110- npcLocation .getZ (),
111- npcLocation .getYaw (),
112- npcLocation .getPitch ()
113- );
114- hologram .teleport (holoLoc );
97+ hologram .setParent (npc .getEntity ().get ());
11598 });
11699
117100 boolean permanentlyVisible = npc .getOptions ().isPermanentlyVisible ();
@@ -169,26 +152,13 @@ private static void processAllNPCHeadRotation() {
169152 yaw = npcLocation .getYaw ();
170153 }
171154 } else if (npc .getOptions ().isLookAtPath ()) {
172- yaw = npcLocation .getYaw ();
155+ // Gets handled by the movement itself
156+ continue ;
173157 } else {
174158 continue ;
175159 }
176160
177- PacketEventsAPI <?> api = EntityLib .getApi ().getPacketEvents ();
178- for (UUID viewerId : entity .getViewers ()) {
179- Player player = org .bukkit .Bukkit .getPlayer (viewerId );
180- if (player == null || player .getWorld () != world ) continue ;
181-
182- WrapperPlayServerEntityHeadLook headPacket = new WrapperPlayServerEntityHeadLook (
183- entity .getEntityId (),
184- yaw
185- );
186-
187- Object channel = api .getProtocolManager ().getChannel (viewerId );
188- if (channel != null ) {
189- api .getProtocolManager ().sendPacket (channel , headPacket );
190- }
191- }
161+ entity .rotateHead (yaw , 0 );
192162 }
193163 }
194164
@@ -210,92 +180,131 @@ private static void processPathFollowing() {
210180 .getEntity ()
211181 .ifPresent (entity -> {
212182 Location current = entity .getLocation ();
213- double distance = Math .sqrt (
183+
184+ // Use horizontal (XZ) distance for waypoint arrival check
185+ double hDistSq =
214186 Math .pow (target .getX () - current .getX (), 2 ) +
215- Math .pow (target .getY () - current .getY (), 2 ) +
216- Math .pow (target .getZ () - current .getZ (), 2 )
217- );
187+ Math .pow (target .getZ () - current .getZ (), 2 );
218188
219- if (distance < 0.5 ) {
189+ if (hDistSq < 0.25 && Math . abs ( target . getY () - current . getY ()) < 1 .5 ) {
220190 path .advanceToNext ();
191+ return ;
192+ }
193+
194+ double speed = npc .getOptions ().getMovementSpeed () * 0.1 ;
195+
196+ // --- Horizontal movement (XZ only) ---
197+ double dx = target .getX () - current .getX ();
198+ double dz = target .getZ () - current .getZ ();
199+ double hLen = Math .sqrt (dx * dx + dz * dz );
200+
201+ double newX , newZ ;
202+ if (hLen > 0.01 ) {
203+ dx /= hLen ;
204+ dz /= hLen ;
205+ newX = current .getX () + dx * speed ;
206+ newZ = current .getZ () + dz * speed ;
221207 } else {
222- double speed =
223- npc .getOptions ().getMovementSpeed () * 0.1 ;
224- double dx = target .getX () - current .getX ();
225- double dy = target .getY () - current .getY ();
226- double dz = target .getZ () - current .getZ ();
227- double len = Math .sqrt (dx * dx + dy * dy + dz * dz );
228-
229- dx /= len ;
230- dy /= len ;
231- dz /= len ;
232-
233- // float yaw = (float) Math.toDegrees(Math.atan2(dz, dx));
234- float yaw = getYawTowards (
235- target ,
236- new org .bukkit .Location (
237- null ,
238- target .getX (),
239- target .getY (),
240- target .getZ (),
241- target .getYaw (),
242- target .getPitch ()
243- )
244- );
245-
246- double newY = current .getY () + dy * speed ;
247- double newX = current .getX () + dx * speed ;
248- double newZ = current .getZ () + dz * speed ;
249-
250- if (npc .getOptions ().isClampToGround ()) {
251- World world = npc .getWorld ();
252- if (world != null ) {
253- int cx = (int ) Math .floor (newX );
254- int cz = (int ) Math .floor (newZ );
255- int cy = (int ) Math .floor (current .getY ());
256-
257- Block feet = world .getBlockAt (
258- cx ,
259- cy ,
260- cz
261- );
262- Block below = world .getBlockAt (
263- cx ,
264- cy - 1 ,
265- cz
266- );
267-
268- if (
269- below .getType () == Material .AIR
270- ) {
271- newY = newY - 1 ;
272- }
273-
274- if (
275- target .getY () > current .getY () + 0.1 &&
276- feet .getType () != org .bukkit .Material .AIR
277- ) {
278- newY = current .getY () + 0.5 ;
279- }
208+ newX = current .getX ();
209+ newZ = current .getZ ();
210+ dx = 0 ;
211+ dz = 0 ;
212+ }
213+
214+ // --- Vertical movement (jump + gravity physics) ---
215+ double newY = current .getY ();
216+
217+ if (npc .getOptions ().isClampToGround ()) {
218+ var world = npc .getWorld ();
219+ if (world != null ) {
220+ int bx = (int ) Math .floor (newX );
221+ int bz = (int ) Math .floor (newZ );
222+ int feetY = (int ) Math .floor (current .getY ());
223+
224+ // Check if there's a solid block ahead at feet level (obstacle)
225+ var blockAtFeet = world .getBlockAt (bx , feetY , bz );
226+ var blockAboveFeet = world .getBlockAt (bx , feetY + 1 , bz );
227+
228+ boolean obstacleAhead = !blockAtFeet .isPassable ()
229+ && blockAboveFeet .isPassable ();
230+
231+ // If we hit a 1-block obstacle and we're on the ground, jump
232+ if (obstacleAhead && follower .isOnGround ()) {
233+ follower .jump ();
280234 }
281- }
282235
283- Location newLoc = new Location (
284- newX ,
285- newY ,
286- newZ ,
287- yaw ,
288- 0
289- );
236+ // Apply vertical physics (gravity + velocity)
237+ newY = follower .applyVerticalPhysics (current .getY ());
238+
239+ // Resolve ground collision: find solid ground below new position
240+ int newFeetY = (int ) Math .floor (newY );
241+ double groundLevel = findGroundLevel (world , bx , newFeetY , bz , feetY + 2 );
290242
291- entity .teleport (newLoc );
243+ if (newY <= groundLevel ) {
244+ // Landed on ground
245+ newY = groundLevel ;
246+ follower .land (groundLevel );
247+ } else {
248+ // Still airborne
249+ follower .setOnGround (false );
250+ }
292251
293- entity .rotateHead (yaw , 0 );
252+ // If jumping into a ceiling (block above head), stop upward velocity
253+ int headY = (int ) Math .floor (newY + 1.8 );
254+ var blockAtHead = world .getBlockAt (bx , headY , bz );
255+ if (!blockAtHead .isPassable () && follower .verticalVelocity > 0 ) {
256+ follower .verticalVelocity = 0 ;
257+ }
258+
259+ // If obstacle is 2+ blocks tall, don't move horizontally into it
260+ if (!blockAtFeet .isPassable () && !blockAboveFeet .isPassable ()) {
261+ newX = current .getX ();
262+ newZ = current .getZ ();
263+ }
264+ }
265+ } else {
266+ // No ground clamping: use linear Y interpolation (original behavior)
267+ double dy = target .getY () - current .getY ();
268+ double fullLen = Math .sqrt (dx * dx + dy * dy + dz * dz );
269+ if (fullLen > 0.01 ) {
270+ newY = current .getY () + (dy / fullLen ) * speed ;
271+ }
294272 }
273+
274+ float yaw = npc .getOptions ().isLookAtPath () ? npc .getPath ().getYaw () : current .getYaw ();
275+ Location newLoc = new Location (
276+ newX ,
277+ newY ,
278+ newZ ,
279+ yaw ,
280+ 0
281+ );
282+
283+ entity .teleport (newLoc );
284+ entity .rotateHead (yaw , 0 );
295285 });
296286 }
297287 }
298288
289+ /**
290+ * Finds the Y level of the ground (top of highest solid block) at the given XZ,
291+ * searching downward from startY. Returns the Y on top of the first solid block found.
292+ * If no solid block is found down to y=minY or world min, returns startY (no change).
293+ */
294+ private static double findGroundLevel (org .bukkit .World world , int bx , int startY , int bz , int maxY ) {
295+ // Don't search too far down - limit to 4 blocks below current position
296+ int minSearch = Math .max (world .getMinHeight (), startY - 4 );
297+ for (int y = startY ; y >= minSearch ; y --) {
298+ var block = world .getBlockAt (bx , y , bz );
299+ if (!block .isPassable ()) {
300+ // Ground found: NPC stands on top of this block
301+ return y + 1 ;
302+ }
303+ }
304+ // No ground found within search range, keep falling
305+ return startY ;
306+ }
307+
299308 private static void updateGlobalHeadRotation (
300309 NPC npc ,
301310 WrapperEntity entity ,
@@ -426,23 +435,6 @@ private static float getYawTowards(
426435 return (float ) (Math .toDegrees (Math .atan2 (dz , dx )) - 90 );
427436 }
428437
429- private static double getGroundY (
430- org .bukkit .World world ,
431- double x ,
432- double y ,
433- double z
434- ) {
435- int cx = (int ) Math .floor (x );
436- int cz = (int ) Math .floor (z );
437- for (int cy = (int ) Math .floor (y ); cy >= 0 ; cy --) {
438- org .bukkit .block .Block block = world .getBlockAt (cx , cy , cz );
439- if (block .getType () != org .bukkit .Material .AIR ) {
440- return cy + 1 ;
441- }
442- }
443- return y ;
444- }
445-
446438 public static void startPathFollowing (NPC npc ) {
447439 NPCPath path = npc .getPath ();
448440 if (path .getWaypointCount () == 0 ) {
@@ -469,8 +461,13 @@ public static boolean isMoving(NPC npc) {
469461
470462 public static class PathFollowing {
471463
464+ private static final double GRAVITY = 0.08 ;
465+ private static final double JUMP_VELOCITY = 0.42 ;
466+
472467 private final NPC npc ;
473468 private final long startTime ;
469+ private double verticalVelocity = 0.0 ;
470+ private boolean onGround = true ;
474471
475472 public PathFollowing (NPC npc ) {
476473 this .npc = npc ;
@@ -484,5 +481,35 @@ public PathFollowing(NPC npc) {
484481 public long getStartTime () {
485482 return startTime ;
486483 }
484+
485+ public void jump () {
486+ if (onGround ) {
487+ verticalVelocity = JUMP_VELOCITY ;
488+ onGround = false ;
489+ }
490+ }
491+
492+ public double applyVerticalPhysics (double currentY ) {
493+ if (onGround && verticalVelocity <= 0 ) {
494+ verticalVelocity = 0 ;
495+ return currentY ;
496+ }
497+ verticalVelocity -= GRAVITY ;
498+ double newY = currentY + verticalVelocity ;
499+ return newY ;
500+ }
501+
502+ public void land (double groundY ) {
503+ onGround = true ;
504+ verticalVelocity = 0.0 ;
505+ }
506+
507+ public boolean isOnGround () {
508+ return onGround ;
509+ }
510+
511+ public void setOnGround (boolean onGround ) {
512+ this .onGround = onGround ;
513+ }
487514 }
488515}
0 commit comments