Skip to content

API AI Goals

Eisi05 edited this page May 3, 2026 · 2 revisions

AI Goals System

NpcAPI provides a powerful AI goal system that allows NPCs to perform autonomous behaviors. Goals represent specific behaviors that an NPC can execute, such as walking to locations, attacking entities, or looking around.

Goal Basics

Creating a Custom Goal

import de.eisi05.npc.api.ai.Goal;
import de.eisi05.npc.api.objects.NPC;
import org.jetbrains.annotations.NotNull;

public class MyCustomGoal extends Goal {
    
    public MyCustomGoal() {
        super(Priority.MEDIUM); // Priority: ALWAYS, HIGHEST, HIGH, MEDIUM, LOW
    }
    
    @Override
    protected boolean canUse(@NotNull NPC npc) {
        // Check if this goal can be used right now
        // Called each tick to determine if goal should be considered
        return npc.getLocation().distance(target) < 50;
    }
    
    @Override
    protected void start(@NotNull NPC npc) {
        // Called when this goal starts executing
        // Initialize any state needed for the goal
        startTime = System.currentTimeMillis();
    }
    
    @Override
    protected void tick(@NotNull NPC npc) {
        // Called each tick while this goal is active
        // Update the goal's behavior
        npc.lookAt(target);
    }
    
    @Override
    protected void stop(@NotNull NPC npc) {
        // Called when this goal stops executing
        // Clean up any state or cancel ongoing tasks
        target = null;
    }
    
    @Override
    protected boolean canContinue(@NotNull NPC npc) {
        // Check if this goal can continue executing
        // If false, goal will be stopped and new goal selected
        return canUse(npc) && (System.currentTimeMillis() - startTime) < 10000;
    }
    
    @Override
    protected boolean canBeInterrupted(@NotNull NPC npc) {
        // Check if this goal can be interrupted by higher priority goals
        // Default is false (goals cannot be interrupted)
        return true;
    }
    
    @Override
    protected boolean canBeRemovedNow(@NotNull NPC npc) {
        // Check if this goal can be removed immediately without being queued
        // Default implementation returns !canContinue(npc)
        // Override this for goals that need special removal logic
        return !canContinue(npc);
    }
}

Goal Priority System

Goals use a priority-based selection system:

  • ALWAYS (100): Always selected over other goals
  • HIGHEST (4): Very high selection chance
  • HIGH (3): High selection chance
  • MEDIUM (2): Medium selection chance
  • LOW (1): Low selection chance

For non-ALWAYS priorities, the system uses weighted random selection.

Goal Management

Adding Goals

import de.eisi05.npc.api.ai.goals.WalkToLocationGoal;
import de.eisi05.npc.api.ai.goals.LookAtGoal;

// Add goals to NPC
npc.addGoal(new WalkToLocationGoal(targetLocation, 0.3));
npc.addGoal(new LookAtGoal(player));

Removing Goals

// Remove a goal - if currently running, it will be queued for removal
// and removed once it finishes naturally to prevent weird positions
boolean removed = npc.removeGoal(goal);

// The system uses a queue-based removal:
// - If goal is not running: removed immediately
// - If goal is running: queued for removal, removed when it finishes
// - Removal happens before new goal selection

Goal Selector

import de.eisi05.npc.api.ai.GoalSelector;

GoalSelector selector = npc.getGoalSelector();

// Start/stop goal evaluation
selector.start();
selector.stop();

// Check if running
boolean running = selector.isRunning();

// Set tick interval (default: 1 tick)
selector.setTickInterval(5); // Evaluate every 5 ticks

// Get current goal
Goal current = selector.getCurrentGoal();

// Force a specific goal to start
selector.forceGoal(myGoal);

Built-in Goals

WalkToLocationGoal

Makes the NPC walk to a specific location using pathfinding:

import de.eisi05.npc.api.ai.goals.WalkToLocationGoal;

Location target = new Location(world, 100, 64, 100);
WalkToLocationGoal walkGoal = new WalkToLocationGoal.Builder(target)
    .speed(0.4)
    .build();

// With custom settings
WalkToLocationGoal walkGoal = new WalkToLocationGoal.Builder(targetLocation)
    .speed(0.4)                    // speed (0.1-1.0)
    .maxIterations(5000)          // max pathfinding iterations
    .allowDiagonal(true)          // allow diagonal movement
    .withRotation(true)           // include rotation packets
    .completionCallback(result -> { // completion callback
        if(result == WalkingResult.SUCCESS) {
            Bukkit.broadcastMessage("NPC reached destination!");
        }
    })
    .build();

LookAtGoal

Makes the NPC look at a specific entity or location:

import de.eisi05.npc.api.ai.goals.LookAtGoal;

// Look at entity
LookAtGoal goal1 = new LookAtGoal(player);

// Look at location
LookAtGoal goal2 = new LookAtGoal(targetLocation);

npc.addGoal(goal1);

AttackEntityGoal

Makes the NPC attack nearby entities that match a predicate. Behavior varies based on held item:

  • Bow/Crossbow/Trident: Long range (15 blocks)
  • Sword/Axe/Other: Short range (3 blocks)
  • Only activates when target is in line of sight
  • Dynamically discovers targets within range
import de.eisi05.npc.api.ai.goals.AttackEntityGoal;

// Attack all players
AttackEntityGoal attackGoal = new AttackEntityGoal(entity -> entity instanceof Player);

// Attack low health players
AttackEntityGoal attackGoal = new AttackEntityGoal(
    entity -> entity instanceof Player && ((Player) entity).getHealth() < 10
);

// With custom attack range
AttackEntityGoal attackGoal = new AttackEntityGoal(
    entity -> entity instanceof Monster,
    15.0  // custom attack range
);

LookAroundGoal

Makes the NPC look around randomly. Can serve as an idle/wait behavior.

import de.eisi05.npc.api.ai.goals.LookAroundGoal;

// Default settings (1-4 seconds)
LookAroundGoal lookGoal = new LookAroundGoal();

// Custom duration
LookAroundGoal lookGoal = new LookAroundGoal(40, 200); // 2-10 seconds

WaitGoal

Makes the NPC wait/idle for a specified duration.

import de.eisi05.npc.api.ai.goals.WaitGoal;

// Wait for 5 seconds (100 ticks)
WaitGoal waitGoal = new WaitGoal(100);

WanderGoal

Makes the NPC wander randomly to nearby locations.

import de.eisi05.npc.api.ai.goals.WanderGoal;

// Default settings (10 block radius)
WanderGoal wanderGoal = new WanderGoal();

// Custom radius and speed
WanderGoal wanderGoal = new WanderGoal(15, 60, 200, 0.3);

FollowEntityGoal

Makes the NPC follow a target entity by UUID, maintaining a specified distance.

import de.eisi05.npc.api.ai.goals.FollowEntityGoal;

// Default settings (10 block follow distance, 1.5 block stop distance)
FollowEntityGoal followGoal = new FollowEntityGoal(playerToFollow.getUniqueId());

// Custom settings
FollowEntityGoal followGoal = new FollowEntityGoal(
    playerToFollow.getUniqueId(),
    5.0,    // follow distance
    2.0,    // stop distance
    0.5     // speed
);

Usage Examples

Basic Setup

// Create NPC
NPC npc = new NPC(location);

// Add goals
npc.addGoal(new WanderGoal(10));
npc.addGoal(new LookAroundGoal());
npc.addGoal(new WalkToLocationGoal.Builder(target).build());

// Start goal system (goals auto-save when added)
npc.startGoals();

Guard NPC Example

NPC guard = new NPC(spawnLocation);

// Equip with sword
Map<EquipmentSlot, ItemStack> equipment = new HashMap<>();
equipment.put(EquipmentSlot.HAND, new ItemStack(Material.IRON_SWORD));
guard.setOption(NpcOption.EQUIPMENT, equipment);

// Add goals - attack has ALWAYS priority, others are randomizable
guard.addGoal(new AttackEntityGoal(entity -> entity instanceof Monster));
guard.addGoal(new WanderGoal(8));
guard.addGoal(new LookAroundGoal());

guard.startGoals();

Following Player Example

NPC companion = new NPC(player.getLocation());

companion.addGoal(new FollowEntityGoal(player.getUniqueId(), 5.0, 2.0, 0.4));
companion.addGoal(new LookAroundGoal());

companion.startGoals();

Goal Removal Queue System

The goal system includes a queue-based removal mechanism to prevent NPCs from ending up in weird positions when goals are removed mid-execution.

How It Works

  1. Removal Check: When removeGoal() is called, the system checks canBeRemovedNow()
  2. Immediate Removal: If canBeRemovedNow() returns true, the goal is removed immediately
  3. Queued Removal: If canBeRemovedNow() returns false, the goal is added to a removal queue
  4. Continued Execution: The goal continues running until it can be safely removed
  5. Automatic Removal: Once canBeRemovedNow() returns true or the goal naturally finishes, it's removed
  6. New Selection: The system then selects a new goal

The canBeRemovedNow() Method

The canBeRemovedNow(@NotNull NPC npc) method determines if a goal can be removed immediately:

@Override
protected boolean canBeRemovedNow(@NotNull NPC npc) {
    // Default implementation - can be removed if goal can't continue
    return !canContinue(npc);
}

Built-in Goal Behavior

  • AttackEntityGoal: Cannot be removed if NPC is floating during movement
  • WalkToLocationGoal: Cannot be removed if NPC is floating while walking
  • FollowEntityGoal: Cannot be removed if NPC is floating during movement
  • Other goals: Use default behavior (can be removed when can't continue)

Benefits

  • Prevents NPCs from being removed while in unstable positions (like floating)
  • Ensures smooth transitions between goals
  • Maintains realistic NPC behavior
  • Automatic handling of edge cases

Example Scenario

// NPC is currently walking to a location
WalkToLocationGoal walkGoal = new WalkToLocationGoal(target, 0.3);
npc.addGoal(walkGoal);

// Later, you want to remove it while it's still walking
npc.removeGoal(walkGoal);

// The goal will:
// 1. Check if it can be removed immediately (canBeRemovedNow())
// 2. If NPC is floating during movement, it will be queued for removal
// 3. Continue walking until NPC has solid ground beneath it
// 4. Then be automatically removed from the goal list
// 5. A new goal will be selected (if available)

Best Practices for Custom Goals

  1. Goals auto-save: Goals are automatically saved when added or removed via addGoal() and removeGoal(). No manual saving required.

  2. Goals auto-start: The goal system automatically starts when an NPC is loaded from disk if it has saved goals.

  3. Use predicates for target filtering: AttackEntityGoal uses predicates to dynamically discover targets within range, making it more flexible than fixed target suppliers.

  4. Combine goals strategically: Use a mix of ALWAYS priority reactive goals (like AttackEntityGoal) and randomizable idle goals (like WanderGoal, LookAroundGoal).

  5. Set appropriate priorities: Use ALWAYS for critical behaviors that should always execute (like combat), and use HIGH/MEDIUM/LOW for behaviors that can be randomly selected.

  6. Test pathfinding: The WalkToLocationGoal uses A* pathfinding which can be CPU-intensive. Adjust maxIterations based on your server's performance.

  7. Use Appropriate Priorities: Choose priorities that match the goal's importance

  8. Handle Interruption: Override canBeInterrupted() for goals that can pause safely

  9. Clean Resources: Always clean up in stop() method

  10. Check Conditions: Use canUse() and canContinue() for proper goal lifecycle

  11. Avoid Long Operations: Keep tick() method lightweight for performance

  12. Override canBeRemovedNow: For movement-based goals, check if NPC is in stable position before allowing removal

Notes

  • The AStarPathfinder is suitable for constant movement, but for very frequent pathfinding, consider caching paths or using simpler movement for short distances.
  • Goals run on the main server thread, so avoid heavy computations in canUse() or tick().
  • Goals implement Serializable and are saved with the NPC. Transient fields (like entity references) are handled via custom serialization.
  • The goal selector runs every tick by default. Adjust with npc.getGoalSelector().setTickInterval(ticks) (internal API).
  • getGoalSelector() is now private - use the public goal management methods (addGoal, removeGoal, startGoals, stopGoals).

Troubleshooting

Goal Not Executing

  • Check canUse() returns true
  • Verify goal priority isn't being overridden
  • Ensure GoalSelector is started

Goal Stuck

  • Check canContinue() implementation
  • Verify goal isn't marked as non-interruptable when it should be
  • Look for exceptions in goal methods

Performance Issues

  • Limit expensive operations in canUse() and tick()
  • Use appropriate tick intervals
  • Consider goal priorities to reduce frequent switching

Clone this wiki locally