Guest User

Wander Code

a guest
Dec 31st, 2025
11
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.64 KB | None | 0 0
  1. import de.bsommerfeld.pathetic.api.pathing.result.PathfinderResult;
  2. import de.bsommerfeld.pathetic.api.wrapper.PathPosition;
  3. import de.bsommerfeld.pathetic.bukkit.context.BukkitEnvironmentContext;
  4. import de.bsommerfeld.pathetic.bukkit.mapper.BukkitMapper;
  5. import net.citizensnpcs.api.ai.tree.BehaviorGoalAdapter;
  6. import net.citizensnpcs.api.ai.tree.BehaviorStatus;
  7. import net.citizensnpcs.api.trait.trait.Equipment;
  8. import net.citizensnpcs.trait.waypoint.Waypoints;
  9. import org.bukkit.*;
  10. import org.bukkit.entity.Entity;
  11. import org.bukkit.entity.EntityType;
  12. import org.bukkit.entity.LivingEntity;
  13. import org.bukkit.entity.Player;
  14. import org.bukkit.inventory.ItemStack;
  15. import org.bukkit.inventory.meta.ItemMeta;
  16.  
  17. import net.citizensnpcs.api.npc.NPC;
  18. import org.bukkit.util.Vector;
  19. import org.mcmonkey.sentinel.SentinelTrait;
  20.  
  21. import java.util.*;
  22. import java.util.concurrent.CompletionStage;
  23.  
  24. public class HandSwitchBehavior extends BehaviorGoalAdapter {
  25.  
  26. private static final Random random = new Random();
  27.  
  28. private static final int SEARCH_COOLDOWN_MAX = 35; // Time between searching bucket of players
  29. private static final int CUTOFF_DISTANCE = 50; // Used as a box around npc to get bucket of players
  30. private static final int CUTOFF_DISTANCE2 = 80000; // 200 blocks of aggressive pathfinding to players
  31. private static final int CUTOFF_DISTANCE3 = 8100; // 90-105 blocks becomes aggressive toward a player
  32. private static final int MELEE_DISTANCE = 900; // 30 blocks melee weapon comes out
  33. private static final double MIN_PROGRESS = 0.02; // Blocks moved per tick expected or stuck
  34. private static final int NO_PROGRESS_THRESHOLD = 10; // Ticks before rngWeapon comes out
  35. private static final double DISTANCE_MARGIN = 0.8; // How close to a point before considered arrived
  36.  
  37. private final NPC npc;
  38.  
  39. private Entity target;
  40. private final ItemStack rngWeapon;
  41. private final ItemStack meleeWeapon;
  42.  
  43. private int searchCooldown = 0;
  44. private Location lastTargetLocation;
  45. private int pathCooldown = 0;
  46. private int tickCounter = 0;
  47. private int noProgressTicks = 0;
  48. private int nodeStuckTicks = 0;
  49. private List<Vector> simplifiedPath = new ArrayList<>();
  50. private int pathIndex = 0;
  51. private int lastPathIndex = 0;
  52. private Vector lastProgressPos = null;
  53.  
  54. // --- Target acquisition delay ---
  55. private boolean targetJustAcquired = false;
  56. private long targetAcquireTime = 0L;
  57. private long targetDelayMs = 0L;
  58. private boolean wandering = true;
  59.  
  60. // Return to home
  61. private Location homeLocation = null;
  62.  
  63. private void setSimplifiedPath(List<Vector> path) {
  64. this.simplifiedPath = path;
  65. }
  66.  
  67. public void setTarget(Entity newTarget) {
  68. target = newTarget;
  69. }
  70.  
  71. /**
  72. * This behavior cycles through weapons
  73. * @param npcWanderer that switches weapons
  74. * @param g weapon 1
  75. * @param g2 weapon 2
  76. */
  77. public HandSwitchBehavior(NPC npcWanderer, ItemStack g, ItemStack g2) {
  78. this.npc = npcWanderer;
  79. // more fields initialized here
  80. }
  81.  
  82. @Override
  83. public void reset() {
  84. target = null;
  85. }
  86.  
  87. @Override
  88. public BehaviorStatus run() {
  89. Entity entity = npc.getEntity();
  90. if (entity == null || !entity.isValid() || entity.isDead()) {
  91. return BehaviorStatus.RUNNING; // stop pathfinding entirely
  92. }
  93. SentinelTrait st = npc.getOrAddTrait(SentinelTrait.class);
  94.  
  95. // Don't wander or do anything until we are spawned
  96. if (homeLocation == null) {
  97. return BehaviorStatus.RUNNING;
  98. }
  99.  
  100. // SEARCH FOR TARGET
  101. if (target == null) {
  102. if (searchCooldown <= 0 && npc.isSpawned()) {
  103.  
  104. Location npcLoc = npc.getEntity().getLocation();
  105. Entity bestCandidate = null;
  106.  
  107. // Boss awareness radius: 30–45 blocks
  108. double maxDist = CUTOFF_DISTANCE3 + random.nextDouble() * 225.0;
  109. double closestDistSq = maxDist * maxDist;
  110.  
  111. for (Entity e : npc.getEntity().getNearbyEntities(
  112. CUTOFF_DISTANCE, CUTOFF_DISTANCE, CUTOFF_DISTANCE)) {
  113.  
  114. // Must be living & not armor stand
  115. if (!(e instanceof LivingEntity) || e.getType() == EntityType.ARMOR_STAND)
  116. continue;
  117.  
  118. // Must be real player
  119. if (!isRealPlayerEntity(e))
  120. continue;
  121.  
  122. // Ignore creative / spectator
  123. if (e instanceof Player p &&
  124. (p.getGameMode() == GameMode.CREATIVE || p.getGameMode() == GameMode.SPECTATOR))
  125. continue;
  126.  
  127. // npcWanderer aggro rules
  128. npcWandererPlayer npcWandererPlayer = npcWandererPlayer.npcWanderer_PLAYERS.get(e.getUniqueId());
  129. if (npcWandererPlayer != null && !npcWandererPlayer.getAggro())
  130. continue;
  131.  
  132. double distSq = e.getLocation().distanceSquared(npcLoc);
  133. if (distSq > closestDistSq)
  134. continue;
  135.  
  136. // --- BOSS SCAN MODEL ---
  137. // Bosses are very good but not psychic
  138. double scanChance = 0.85;
  139. if (distSq > 400) { // >20 blocks
  140. scanChance -= (distSq - 400) / closestDistSq;
  141. }
  142. if (random.nextDouble() > scanChance)
  143. continue;
  144.  
  145. bestCandidate = e;
  146. closestDistSq = distSq;
  147. }
  148.  
  149. // --- TARGET ACQUIRED ---
  150. if (bestCandidate != null) {
  151. target = bestCandidate;
  152. wandering = false; // exit wander mode
  153. npc.getOrAddTrait(Waypoints.class).setWaypointProvider(null);
  154.  
  155. lastTargetLocation = null;
  156. st.faceLocation(target.getLocation());
  157.  
  158. // Boss reaction delay: nearly instant but perceptible
  159. targetJustAcquired = true;
  160. targetAcquireTime = System.currentTimeMillis();
  161. targetDelayMs = 120 + random.nextInt(120); // 120–240 ms
  162. }
  163.  
  164. // Boss scans frequently
  165. searchCooldown = (int) (SEARCH_COOLDOWN_MAX * 0.6 + random.nextInt(10));
  166. } else {
  167. searchCooldown--;
  168. }
  169. }
  170.  
  171. // TARGET FOUND
  172. if (target instanceof Player player) {
  173. npcWandererPlayer npcWandererPlayer = npcWandererPlayer.npcWanderer_PLAYERS.get(player.getUniqueId());
  174. if (npcWandererPlayer != null && !npcWandererPlayer.getAggro()) {
  175. target = null;
  176. simplifiedPath = new ArrayList<>();
  177. npc.getNavigator().setPaused(false);
  178. npc.getNavigator().getLocalParameters().speedModifier(1f);
  179. }
  180. }
  181.  
  182. // TARGET INVALID OR OUT OF RANGE
  183. if (target != null && (target.isDead()
  184. || target.getLocation().distanceSquared(npc.getEntity().getLocation()) >= CUTOFF_DISTANCE2
  185. || target.getWorld() != npc.getEntity().getWorld()
  186. || (target instanceof Player creative && (creative.getGameMode() == GameMode.SPECTATOR || creative.getGameMode() == GameMode.CREATIVE)))) {
  187. target = null;
  188. simplifiedPath = new ArrayList<>();
  189. npc.getNavigator().setPaused(false);
  190. npc.getNavigator().getLocalParameters().speedModifier(1f);
  191. }
  192.  
  193. // --- PATHFINDING TO TARGET ---
  194. if (target != null) {
  195. // Only recompute if pathCooldown <= 0 and target moved enough
  196. Location finalNPCLoc = npc.getEntity().getLocation().clone();
  197. World finalWorld = npc.getEntity().getWorld();
  198. if (pathCooldown <= 0 && (lastTargetLocation == null || lastTargetLocation.distanceSquared(target.getLocation()) > 4.0)) {
  199. lastTargetLocation = target.getLocation().clone();
  200.  
  201. CompletionStage<PathfinderResult> pathFuture = MyPlugin.getInstance()
  202. .getPathfinder()
  203. .findPath(
  204. BukkitMapper.toPathPosition(finalNPCLoc),
  205. BukkitMapper.toPathPosition(lastTargetLocation),
  206. new BukkitEnvironmentContext(finalWorld)
  207. );
  208.  
  209. pathFuture.thenAccept(result -> {
  210. if (result.successful() || result.hasFallenBack()) {
  211. PathPosition[] pathArray = result.getPath().collect().toArray(new PathPosition[0]);
  212. List<Vector> pathVectors = Arrays.stream(pathArray)
  213. .map(pos -> {
  214. // Convert to location first
  215. Location loc = BukkitMapper.toLocation(pos, finalWorld);
  216.  
  217. // Center X/Z to block center
  218. loc.setX(Math.floor(loc.getX()) + 0.5);
  219. loc.setZ(Math.floor(loc.getZ()) + 0.5);
  220.  
  221. return loc.toVector();
  222. })
  223. .toList();
  224.  
  225. // Schedule setting path on main thread
  226. Bukkit.getScheduler().runTask(MyPlugin.getInstance(), () -> {
  227. // Do not set a path if target is now null
  228. if (target == null) return;
  229. if (!pathVectors.isEmpty() && (npc.getEntity() != null && npc.isSpawned())) {
  230. setSimplifiedPath(pathVectors);
  231. pathIndex = 1; // start from the second node to avoid zero-length movement
  232. lastPathIndex = 1;
  233. lastProgressPos = finalNPCLoc.toVector(); // reset progress tracking
  234. noProgressTicks = 0;
  235. nodeStuckTicks = 0;
  236. }
  237. });
  238. }
  239. });
  240.  
  241. pathCooldown = 5; // recompute every 1/4s
  242. } else {
  243. pathCooldown--;
  244. }
  245. }
  246.  
  247. // --- NODE PROGRESS CHECK ---
  248. boolean movementStuck;
  249. if (!simplifiedPath.isEmpty() && pathIndex < simplifiedPath.size()) {
  250. Vector currentTarget = simplifiedPath.get(pathIndex);
  251. Location targetLoc = currentTarget.toLocation(npc.getEntity().getWorld());
  252. // Update every tick with setMoveDestination()
  253. npc.getNavigator().getLocalParameters().speedModifier(1.95f);
  254. npc.getNavigator().getLocalParameters().range(0.5f);
  255. npc.setMoveDestination(targetLoc);
  256.  
  257. // Check progress by movement
  258. Vector currentPos = npc.getEntity().getLocation().toVector();
  259. if (lastProgressPos != null) {
  260. double moved = currentPos.distance(lastProgressPos);
  261. if (moved < MIN_PROGRESS) {
  262. noProgressTicks++;
  263. } else {
  264. noProgressTicks = 0;
  265. }
  266. }
  267. lastProgressPos = currentPos;
  268.  
  269. // Check progress by node advancement
  270. if (lastPathIndex == pathIndex) {
  271. nodeStuckTicks++;
  272. } else {
  273. nodeStuckTicks = 0;
  274. }
  275. lastPathIndex = pathIndex;
  276.  
  277. // Advance pathIndex if close enough to current waypoint
  278. if (currentPos.distanceSquared(currentTarget) < DISTANCE_MARGIN * DISTANCE_MARGIN) {
  279. pathIndex++;
  280. }
  281.  
  282. // Determine if NPC is stuck (either physically or on same node)
  283. movementStuck = noProgressTicks >= NO_PROGRESS_THRESHOLD || nodeStuckTicks >= NO_PROGRESS_THRESHOLD;
  284.  
  285. // --- HANDLE STUCK IN DOOR / OBSTRUCTION ---
  286. if (movementStuck && target != null) {
  287. // Teleport slightly above the node to avoid clipping
  288. npc.getEntity().teleport(targetLoc.add(0, 0.1, 0));
  289.  
  290. // Reset movement tracking
  291. noProgressTicks = 0;
  292. nodeStuckTicks = 0;
  293. lastProgressPos = targetLoc.toVector();
  294. lastPathIndex = pathIndex;
  295. }
  296. } else {
  297. // Path finished, use melee if close
  298. movementStuck = false;
  299. }
  300.  
  301. if (target == null) {
  302. if (!wandering) {
  303. npc.getOrAddTrait(Waypoints.class).setWaypointProvider("wander");
  304. wandering = true;
  305. }
  306. return BehaviorStatus.RUNNING;
  307. }
  308.  
  309. tickCounter ++;
  310. if (tickCounter % 5 == 0) {
  311. for (int i = 0; i < simplifiedPath.size() - 1; i++) {
  312. Location point = simplifiedPath.get(i).toLocation(npc.getEntity().getWorld());
  313. npc.getEntity().getWorld().spawnParticle(Particle.DUST, point, 1, dustOptions2);
  314. }
  315. }
  316.  
  317. // Out of reach check for melee boss
  318. Vector lastNode = simplifiedPath.isEmpty()
  319. ? null
  320. : simplifiedPath.getLast();
  321.  
  322. boolean finalNodeTooFar = false;
  323.  
  324. if (lastNode != null) {
  325. double distSq = lastNode.toLocation(npc.getEntity().getWorld())
  326. .distanceSquared(target.getLocation());
  327. finalNodeTooFar = distSq > 9.0; // 3 blocks squared
  328. }
  329.  
  330. // We are within melee distance and can path to player (final 3 blocks)
  331. if (!movementStuck && target.getLocation().distanceSquared(npc.getEntity().getLocation()) < MELEE_DISTANCE && !finalNodeTooFar) {
  332. if (!npc.getOrAddTrait(Equipment.class).get(Equipment.EquipmentSlot.HAND).equals(meleeWeapon)) {
  333. npc.getOrAddTrait(Equipment.class).set(Equipment.EquipmentSlot.HAND, meleeWeapon);
  334. }
  335. // We path nearby and Sentinel will auto attack when close
  336. return BehaviorStatus.RUNNING;
  337. }
  338.  
  339. // We are stuck, further than 30 blocks, or being strung along out of reach = rngWeapon mode
  340. npc.getOrAddTrait(Equipment.class).set(Equipment.EquipmentSlot.HAND, rngWeapon);
  341.  
  342. // FACE TARGET ENTITY
  343. st.faceLocation(target.getLocation());
  344.  
  345.  
  346. if (target instanceof Player player) {
  347. npcWandererPlayer npcWandererPlayer = npcWandererPlayer.npcWanderer_PLAYERS.get(player.getUniqueId());
  348. if (npcWandererPlayer == null || npcWandererPlayer.getAggro()) {
  349. // --- TARGETING DELAY ---
  350. if (targetJustAcquired) {
  351. long elapsed = System.currentTimeMillis() - targetAcquireTime;
  352. if (elapsed < targetDelayMs) {
  353. return BehaviorStatus.RUNNING; // Track, move, aim - no attack yet
  354. }
  355. targetJustAcquired = false; // Delay satisfied
  356. }
  357. if (st.attackHelper.tryAttack((LivingEntity) target)) {
  358. return BehaviorStatus.RUNNING;
  359. }
  360. return BehaviorStatus.RUNNING;
  361. }
  362. }
  363.  
  364. return BehaviorStatus.RUNNING;
  365. }
  366.  
  367. @Override
  368. public boolean shouldExecute() {
  369. return true;
  370. }
  371.  
  372. }
Advertisement
Add Comment
Please, Sign In to add comment