Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import de.bsommerfeld.pathetic.api.pathing.result.PathfinderResult;
- import de.bsommerfeld.pathetic.api.wrapper.PathPosition;
- import de.bsommerfeld.pathetic.bukkit.context.BukkitEnvironmentContext;
- import de.bsommerfeld.pathetic.bukkit.mapper.BukkitMapper;
- import net.citizensnpcs.api.ai.tree.BehaviorGoalAdapter;
- import net.citizensnpcs.api.ai.tree.BehaviorStatus;
- import net.citizensnpcs.api.trait.trait.Equipment;
- import net.citizensnpcs.trait.waypoint.Waypoints;
- import org.bukkit.*;
- import org.bukkit.entity.Entity;
- import org.bukkit.entity.EntityType;
- import org.bukkit.entity.LivingEntity;
- import org.bukkit.entity.Player;
- import org.bukkit.inventory.ItemStack;
- import org.bukkit.inventory.meta.ItemMeta;
- import net.citizensnpcs.api.npc.NPC;
- import org.bukkit.util.Vector;
- import org.mcmonkey.sentinel.SentinelTrait;
- import java.util.*;
- import java.util.concurrent.CompletionStage;
- public class HandSwitchBehavior extends BehaviorGoalAdapter {
- private static final Random random = new Random();
- private static final int SEARCH_COOLDOWN_MAX = 35; // Time between searching bucket of players
- private static final int CUTOFF_DISTANCE = 50; // Used as a box around npc to get bucket of players
- private static final int CUTOFF_DISTANCE2 = 80000; // 200 blocks of aggressive pathfinding to players
- private static final int CUTOFF_DISTANCE3 = 8100; // 90-105 blocks becomes aggressive toward a player
- private static final int MELEE_DISTANCE = 900; // 30 blocks melee weapon comes out
- private static final double MIN_PROGRESS = 0.02; // Blocks moved per tick expected or stuck
- private static final int NO_PROGRESS_THRESHOLD = 10; // Ticks before rngWeapon comes out
- private static final double DISTANCE_MARGIN = 0.8; // How close to a point before considered arrived
- private final NPC npc;
- private Entity target;
- private final ItemStack rngWeapon;
- private final ItemStack meleeWeapon;
- private int searchCooldown = 0;
- private Location lastTargetLocation;
- private int pathCooldown = 0;
- private int tickCounter = 0;
- private int noProgressTicks = 0;
- private int nodeStuckTicks = 0;
- private List<Vector> simplifiedPath = new ArrayList<>();
- private int pathIndex = 0;
- private int lastPathIndex = 0;
- private Vector lastProgressPos = null;
- // --- Target acquisition delay ---
- private boolean targetJustAcquired = false;
- private long targetAcquireTime = 0L;
- private long targetDelayMs = 0L;
- private boolean wandering = true;
- // Return to home
- private Location homeLocation = null;
- private void setSimplifiedPath(List<Vector> path) {
- this.simplifiedPath = path;
- }
- public void setTarget(Entity newTarget) {
- target = newTarget;
- }
- /**
- * This behavior cycles through weapons
- * @param npcWanderer that switches weapons
- * @param g weapon 1
- * @param g2 weapon 2
- */
- public HandSwitchBehavior(NPC npcWanderer, ItemStack g, ItemStack g2) {
- this.npc = npcWanderer;
- // more fields initialized here
- }
- @Override
- public void reset() {
- target = null;
- }
- @Override
- public BehaviorStatus run() {
- Entity entity = npc.getEntity();
- if (entity == null || !entity.isValid() || entity.isDead()) {
- return BehaviorStatus.RUNNING; // stop pathfinding entirely
- }
- SentinelTrait st = npc.getOrAddTrait(SentinelTrait.class);
- // Don't wander or do anything until we are spawned
- if (homeLocation == null) {
- return BehaviorStatus.RUNNING;
- }
- // SEARCH FOR TARGET
- if (target == null) {
- if (searchCooldown <= 0 && npc.isSpawned()) {
- Location npcLoc = npc.getEntity().getLocation();
- Entity bestCandidate = null;
- // Boss awareness radius: 30–45 blocks
- double maxDist = CUTOFF_DISTANCE3 + random.nextDouble() * 225.0;
- double closestDistSq = maxDist * maxDist;
- for (Entity e : npc.getEntity().getNearbyEntities(
- CUTOFF_DISTANCE, CUTOFF_DISTANCE, CUTOFF_DISTANCE)) {
- // Must be living & not armor stand
- if (!(e instanceof LivingEntity) || e.getType() == EntityType.ARMOR_STAND)
- continue;
- // Must be real player
- if (!isRealPlayerEntity(e))
- continue;
- // Ignore creative / spectator
- if (e instanceof Player p &&
- (p.getGameMode() == GameMode.CREATIVE || p.getGameMode() == GameMode.SPECTATOR))
- continue;
- // npcWanderer aggro rules
- npcWandererPlayer npcWandererPlayer = npcWandererPlayer.npcWanderer_PLAYERS.get(e.getUniqueId());
- if (npcWandererPlayer != null && !npcWandererPlayer.getAggro())
- continue;
- double distSq = e.getLocation().distanceSquared(npcLoc);
- if (distSq > closestDistSq)
- continue;
- // --- BOSS SCAN MODEL ---
- // Bosses are very good but not psychic
- double scanChance = 0.85;
- if (distSq > 400) { // >20 blocks
- scanChance -= (distSq - 400) / closestDistSq;
- }
- if (random.nextDouble() > scanChance)
- continue;
- bestCandidate = e;
- closestDistSq = distSq;
- }
- // --- TARGET ACQUIRED ---
- if (bestCandidate != null) {
- target = bestCandidate;
- wandering = false; // exit wander mode
- npc.getOrAddTrait(Waypoints.class).setWaypointProvider(null);
- lastTargetLocation = null;
- st.faceLocation(target.getLocation());
- // Boss reaction delay: nearly instant but perceptible
- targetJustAcquired = true;
- targetAcquireTime = System.currentTimeMillis();
- targetDelayMs = 120 + random.nextInt(120); // 120–240 ms
- }
- // Boss scans frequently
- searchCooldown = (int) (SEARCH_COOLDOWN_MAX * 0.6 + random.nextInt(10));
- } else {
- searchCooldown--;
- }
- }
- // TARGET FOUND
- if (target instanceof Player player) {
- npcWandererPlayer npcWandererPlayer = npcWandererPlayer.npcWanderer_PLAYERS.get(player.getUniqueId());
- if (npcWandererPlayer != null && !npcWandererPlayer.getAggro()) {
- target = null;
- simplifiedPath = new ArrayList<>();
- npc.getNavigator().setPaused(false);
- npc.getNavigator().getLocalParameters().speedModifier(1f);
- }
- }
- // TARGET INVALID OR OUT OF RANGE
- if (target != null && (target.isDead()
- || target.getLocation().distanceSquared(npc.getEntity().getLocation()) >= CUTOFF_DISTANCE2
- || target.getWorld() != npc.getEntity().getWorld()
- || (target instanceof Player creative && (creative.getGameMode() == GameMode.SPECTATOR || creative.getGameMode() == GameMode.CREATIVE)))) {
- target = null;
- simplifiedPath = new ArrayList<>();
- npc.getNavigator().setPaused(false);
- npc.getNavigator().getLocalParameters().speedModifier(1f);
- }
- // --- PATHFINDING TO TARGET ---
- if (target != null) {
- // Only recompute if pathCooldown <= 0 and target moved enough
- Location finalNPCLoc = npc.getEntity().getLocation().clone();
- World finalWorld = npc.getEntity().getWorld();
- if (pathCooldown <= 0 && (lastTargetLocation == null || lastTargetLocation.distanceSquared(target.getLocation()) > 4.0)) {
- lastTargetLocation = target.getLocation().clone();
- CompletionStage<PathfinderResult> pathFuture = MyPlugin.getInstance()
- .getPathfinder()
- .findPath(
- BukkitMapper.toPathPosition(finalNPCLoc),
- BukkitMapper.toPathPosition(lastTargetLocation),
- new BukkitEnvironmentContext(finalWorld)
- );
- pathFuture.thenAccept(result -> {
- if (result.successful() || result.hasFallenBack()) {
- PathPosition[] pathArray = result.getPath().collect().toArray(new PathPosition[0]);
- List<Vector> pathVectors = Arrays.stream(pathArray)
- .map(pos -> {
- // Convert to location first
- Location loc = BukkitMapper.toLocation(pos, finalWorld);
- // Center X/Z to block center
- loc.setX(Math.floor(loc.getX()) + 0.5);
- loc.setZ(Math.floor(loc.getZ()) + 0.5);
- return loc.toVector();
- })
- .toList();
- // Schedule setting path on main thread
- Bukkit.getScheduler().runTask(MyPlugin.getInstance(), () -> {
- // Do not set a path if target is now null
- if (target == null) return;
- if (!pathVectors.isEmpty() && (npc.getEntity() != null && npc.isSpawned())) {
- setSimplifiedPath(pathVectors);
- pathIndex = 1; // start from the second node to avoid zero-length movement
- lastPathIndex = 1;
- lastProgressPos = finalNPCLoc.toVector(); // reset progress tracking
- noProgressTicks = 0;
- nodeStuckTicks = 0;
- }
- });
- }
- });
- pathCooldown = 5; // recompute every 1/4s
- } else {
- pathCooldown--;
- }
- }
- // --- NODE PROGRESS CHECK ---
- boolean movementStuck;
- if (!simplifiedPath.isEmpty() && pathIndex < simplifiedPath.size()) {
- Vector currentTarget = simplifiedPath.get(pathIndex);
- Location targetLoc = currentTarget.toLocation(npc.getEntity().getWorld());
- // Update every tick with setMoveDestination()
- npc.getNavigator().getLocalParameters().speedModifier(1.95f);
- npc.getNavigator().getLocalParameters().range(0.5f);
- npc.setMoveDestination(targetLoc);
- // Check progress by movement
- Vector currentPos = npc.getEntity().getLocation().toVector();
- if (lastProgressPos != null) {
- double moved = currentPos.distance(lastProgressPos);
- if (moved < MIN_PROGRESS) {
- noProgressTicks++;
- } else {
- noProgressTicks = 0;
- }
- }
- lastProgressPos = currentPos;
- // Check progress by node advancement
- if (lastPathIndex == pathIndex) {
- nodeStuckTicks++;
- } else {
- nodeStuckTicks = 0;
- }
- lastPathIndex = pathIndex;
- // Advance pathIndex if close enough to current waypoint
- if (currentPos.distanceSquared(currentTarget) < DISTANCE_MARGIN * DISTANCE_MARGIN) {
- pathIndex++;
- }
- // Determine if NPC is stuck (either physically or on same node)
- movementStuck = noProgressTicks >= NO_PROGRESS_THRESHOLD || nodeStuckTicks >= NO_PROGRESS_THRESHOLD;
- // --- HANDLE STUCK IN DOOR / OBSTRUCTION ---
- if (movementStuck && target != null) {
- // Teleport slightly above the node to avoid clipping
- npc.getEntity().teleport(targetLoc.add(0, 0.1, 0));
- // Reset movement tracking
- noProgressTicks = 0;
- nodeStuckTicks = 0;
- lastProgressPos = targetLoc.toVector();
- lastPathIndex = pathIndex;
- }
- } else {
- // Path finished, use melee if close
- movementStuck = false;
- }
- if (target == null) {
- if (!wandering) {
- npc.getOrAddTrait(Waypoints.class).setWaypointProvider("wander");
- wandering = true;
- }
- return BehaviorStatus.RUNNING;
- }
- tickCounter ++;
- if (tickCounter % 5 == 0) {
- for (int i = 0; i < simplifiedPath.size() - 1; i++) {
- Location point = simplifiedPath.get(i).toLocation(npc.getEntity().getWorld());
- npc.getEntity().getWorld().spawnParticle(Particle.DUST, point, 1, dustOptions2);
- }
- }
- // Out of reach check for melee boss
- Vector lastNode = simplifiedPath.isEmpty()
- ? null
- : simplifiedPath.getLast();
- boolean finalNodeTooFar = false;
- if (lastNode != null) {
- double distSq = lastNode.toLocation(npc.getEntity().getWorld())
- .distanceSquared(target.getLocation());
- finalNodeTooFar = distSq > 9.0; // 3 blocks squared
- }
- // We are within melee distance and can path to player (final 3 blocks)
- if (!movementStuck && target.getLocation().distanceSquared(npc.getEntity().getLocation()) < MELEE_DISTANCE && !finalNodeTooFar) {
- if (!npc.getOrAddTrait(Equipment.class).get(Equipment.EquipmentSlot.HAND).equals(meleeWeapon)) {
- npc.getOrAddTrait(Equipment.class).set(Equipment.EquipmentSlot.HAND, meleeWeapon);
- }
- // We path nearby and Sentinel will auto attack when close
- return BehaviorStatus.RUNNING;
- }
- // We are stuck, further than 30 blocks, or being strung along out of reach = rngWeapon mode
- npc.getOrAddTrait(Equipment.class).set(Equipment.EquipmentSlot.HAND, rngWeapon);
- // FACE TARGET ENTITY
- st.faceLocation(target.getLocation());
- if (target instanceof Player player) {
- npcWandererPlayer npcWandererPlayer = npcWandererPlayer.npcWanderer_PLAYERS.get(player.getUniqueId());
- if (npcWandererPlayer == null || npcWandererPlayer.getAggro()) {
- // --- TARGETING DELAY ---
- if (targetJustAcquired) {
- long elapsed = System.currentTimeMillis() - targetAcquireTime;
- if (elapsed < targetDelayMs) {
- return BehaviorStatus.RUNNING; // Track, move, aim - no attack yet
- }
- targetJustAcquired = false; // Delay satisfied
- }
- if (st.attackHelper.tryAttack((LivingEntity) target)) {
- return BehaviorStatus.RUNNING;
- }
- return BehaviorStatus.RUNNING;
- }
- }
- return BehaviorStatus.RUNNING;
- }
- @Override
- public boolean shouldExecute() {
- return true;
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment