Package: com.argonathsystems.framework.npc
Module ID: npc
Version: 1.0.0
Dependencies: core, config, storage
The NPC Framework provides a comprehensive system for creating interactive non-player characters with dialog trees, behaviors, quest integration, and custom interactions. Features include dialog branching, conditions, animations, and pathfinding.
Represents a non-player character with properties, behaviors, and dialog.
public interface NPC {
String getId();
String getName();
EntityType getEntityType();
Location getLocation();
void setLocation(Location location);
String getSkin();
void setSkin(String skin);
DialogTree getDialogTree();
void setDialogTree(DialogTree dialogTree);
List<NPCBehavior> getBehaviors();
void addBehavior(NPCBehavior behavior);
void removeBehavior(NPCBehavior behavior);
Map<String, Object> getMetadata();
void setMetadata(String key, Object value);
Object getMetadata(String key);
boolean isSpawned();
void spawn();
void despawn();
void lookAt(Location location);
void lookAt(Entity entity);
void playAnimation(NPCAnimation animation);
}
getId()Returns the unique NPC identifier.
NPC npc = npcManager.getNPC("blacksmith_john");
String id = npc.getId(); // "blacksmith_john"
Returns: Unique NPC ID
Immutability: Immutable after creation
getName()Returns the display name shown above the NPC.
String name = npc.getName(); // "§6John the Blacksmith"
Returns: Display name with color code support
Visibility: Shown in entity nameplate
getEntityType()Returns the entity type used for the NPC.
EntityType type = npc.getEntityType();
// PLAYER, VILLAGER, ZOMBIE, SKELETON, etc.
Returns: NPC entity type
Default: EntityType.PLAYER
setLocation(Location location)Moves the NPC to a new location.
Location newLocation = new Location(world, 100, 64, 200);
npc.setLocation(newLocation);
Parameters:
location - New location for the NPCSide Effects: Teleports NPC if spawned
Thread Safety: Thread-safe
getDialogTree()Returns the NPC’s dialog tree.
DialogTree dialog = npc.getDialogTree();
DialogNode root = dialog.getRootNode();
Returns: DialogTree instance or null if no dialog
See Also: DialogTree
spawn()Spawns the NPC in the world.
npc.spawn();
// NPC now visible and interactable
Events: Fires NPCSpawnEvent
Side Effects: Creates entity in world
Thread Safety: Must be called on main thread
despawn()Removes the NPC from the world.
npc.despawn();
// NPC no longer visible
Events: Fires NPCDespawnEvent
Side Effects: Removes entity from world
Preservation: Dialog state and metadata preserved
lookAt(Entity entity)Makes the NPC look at an entity.
eventBus.subscribe(PlayerInteractNPCEvent.class, event -> {
NPC npc = event.getNPC();
Player player = event.getPlayer();
npc.lookAt(player);
});
Parameters:
entity - Entity to look atAnimation: Smoothly rotates head and body
Thread Safety: Thread-safe
playAnimation(NPCAnimation animation)Plays an animation.
npc.playAnimation(NPCAnimation.WAVE);
// NPC waves at player
Parameters:
animation - Animation to playAnimations: WAVE, NOD, SHAKE_HEAD, POINT, ATTACK, CROUCH, etc.
Duration: Varies by animation type
public class QuestGiverNPC {
public void createQuestGiver(Location location) {
NPC npc = NPC.builder()
.id("quest_giver_aldric")
.name("§6Aldric the Wise")
.entityType(EntityType.PLAYER)
.location(location)
.skin("ewogICJ0aW1lc3RhbXAiIDogMTY0...") // Base64 skin data
.dialogTree(createDialogTree())
.behavior(new LookAtPlayerBehavior(5.0))
.behavior(new WaveOnApproachBehavior())
.metadata("quest_giver", true)
.metadata("available_quests", Arrays.asList("main_quest_1", "side_quest_3"))
.build();
npcManager.registerNPC(npc);
npc.spawn();
}
private DialogTree createDialogTree() {
return DialogTree.builder()
.rootNode(
DialogNode.builder()
.text("Greetings, traveler! I have tasks for those brave enough.")
.choice(
DialogChoice.builder()
.text("What quests do you have?")
.action(DialogAction.SHOW_QUEST_LIST)
.build()
)
.choice(
DialogChoice.builder()
.text("Tell me about yourself.")
.nextNode("about")
.build()
)
.choice(
DialogChoice.builder()
.text("Goodbye.")
.action(DialogAction.END_CONVERSATION)
.build()
)
.build()
)
.node("about",
DialogNode.builder()
.text("I am Aldric, keeper of ancient knowledge and quests.")
.choice(
DialogChoice.builder()
.text("Back.")
.nextNode("root")
.build()
)
.build()
)
.build();
}
}
Fluent builder for creating NPC instances.
public interface NPCBuilder {
NPCBuilder id(String id);
NPCBuilder name(String name);
NPCBuilder entityType(EntityType type);
NPCBuilder location(Location location);
NPCBuilder skin(String skinData);
NPCBuilder dialogTree(DialogTree dialogTree);
NPCBuilder behavior(NPCBehavior behavior);
NPCBuilder behaviors(NPCBehavior... behaviors);
NPCBuilder metadata(String key, Object value);
NPCBuilder interactionHandler(InteractionHandler handler);
NPC build();
}
id(String id)Sets the unique NPC identifier.
NPCBuilder builder = NPC.builder()
.id("blacksmith_john");
Parameters:
id - Unique NPC ID (required)Returns: Builder for chaining
Validation: Must be unique, alphanumeric with underscores
name(String name)Sets the NPC display name.
builder.name("§6John the Blacksmith");
Parameters:
name - Display name (supports color codes)Returns: Builder for chaining
entityType(EntityType type)Sets the entity type for the NPC.
builder.entityType(EntityType.VILLAGER);
Parameters:
type - Entity typeReturns: Builder for chaining
Default: EntityType.PLAYER
location(Location location)Sets the spawn location.
Location spawn = new Location(world, 100, 64, 200, 90, 0);
builder.location(spawn);
Parameters:
location - Spawn location with yaw/pitchReturns: Builder for chaining
Required: Yes
skin(String skinData)Sets the skin for player NPCs.
builder.skin("ewogICJ0aW1lc3RhbXAiIDogMTY0...");
Parameters:
skinData - Base64-encoded skin texture dataReturns: Builder for chaining
Applies To: EntityType.PLAYER only
behavior(NPCBehavior behavior)Adds a behavior to the NPC.
builder.behavior(new LookAtPlayerBehavior(5.0))
.behavior(new RandomWalkBehavior())
.behavior(new GreetPlayerBehavior());
Parameters:
behavior - Behavior instanceReturns: Builder for chaining
See Also: NPCBehavior
public class NPCFactory {
public static NPC createVillageGuard(World world) {
return NPC.builder()
.id("village_guard_1")
.name("§4Guard Captain")
.entityType(EntityType.PLAYER)
.location(new Location(world, 100, 64, 100, 180, 0))
.skin(SkinLibrary.GUARD_SKIN)
// Dialog
.dialogTree(
DialogTree.builder()
.rootNode(
DialogNode.builder()
.text("Halt! State your business.")
.condition(player -> !player.hasPermission("village.resident"))
.choice(
DialogChoice.builder()
.text("I'm just passing through.")
.nextNode("passing_through")
.build()
)
.build()
)
.build()
)
// Behaviors
.behavior(new LookAtPlayerBehavior(8.0))
.behavior(new PatrolBehavior(Arrays.asList(
new Location(world, 100, 64, 100),
new Location(world, 120, 64, 100),
new Location(world, 120, 64, 120),
new Location(world, 100, 64, 120)
)))
// Metadata
.metadata("guard", true)
.metadata("patrol_route", "village_perimeter")
.build();
}
}
Central registry for NPC definitions.
public interface NPCRegistry {
void registerNPC(NPC npc);
void unregisterNPC(String npcId);
NPC getNPC(String npcId);
Collection<NPC> getAllNPCs();
Collection<NPC> getNPCsInWorld(World world);
Collection<NPC> getNPCsNearLocation(Location location, double radius);
boolean isRegistered(String npcId);
void spawnAll();
void despawnAll();
}
registerNPC(NPC npc)Registers an NPC.
NPCRegistry registry = npcManager.getRegistry();
NPC npc = createBlacksmith();
registry.registerNPC(npc);
Parameters:
npc - NPC to registerThrows: IllegalArgumentException if NPC ID already registered
Thread Safety: Thread-safe
getNPC(String npcId)Retrieves an NPC by ID.
NPC blacksmith = registry.getNPC("blacksmith_john");
if (blacksmith == null) {
logger.warn("NPC not found: blacksmith_john");
}
Parameters:
npcId - NPC identifierReturns: NPC instance or null if not found
Thread Safety: Thread-safe
getNPCsNearLocation(Location location, double radius)Finds NPCs near a location.
Location center = player.getLocation();
Collection<NPC> nearbyNPCs = registry.getNPCsNearLocation(center, 10.0);
for (NPC npc : nearbyNPCs) {
npc.lookAt(player);
}
Parameters:
location - Center locationradius - Search radius in blocksReturns: Collection of NPCs within radius
Performance: Uses spatial indexing for efficiency
spawnAll()Spawns all registered NPCs.
// On server start
registry.spawnAll();
logger.info("Spawned {} NPCs", registry.getAllNPCs().size());
Events: Fires NPCSpawnEvent for each NPC
Thread Safety: Must be called on main thread
public class NPCLoader {
private final NPCRegistry registry;
public void loadNPCs(Path npcDir) {
logger.info("Loading NPCs from {}", npcDir);
try (Stream<Path> paths = Files.walk(npcDir)) {
paths.filter(p -> p.toString().endsWith(".yml"))
.forEach(path -> {
try {
NPC npc = parseNPCFile(path);
registry.registerNPC(npc);
logger.debug("Loaded NPC: {}", npc.getId());
} catch (Exception e) {
logger.error("Failed to load NPC from {}", path, e);
}
});
} catch (IOException e) {
logger.error("Failed to load NPCs", e);
}
// Spawn all loaded NPCs
registry.spawnAll();
logger.info("Spawned {} NPCs", registry.getAllNPCs().size());
}
}
High-level NPC management interface.
public interface NPCManager {
NPCRegistry getRegistry();
void handleInteraction(Player player, NPC npc);
void startDialog(Player player, NPC npc);
void endDialog(Player player);
DialogState getDialogState(Player player);
void selectChoice(Player player, int choiceIndex);
void updateBehaviors();
}
handleInteraction(Player player, NPC npc)Handles player-NPC interaction.
npcManager.handleInteraction(player, npc);
// Starts dialog or triggers custom interaction
Parameters:
player - Interacting playernpc - NPC being interacted withEvents: Fires PlayerInteractNPCEvent
Side Effects: May start dialog
startDialog(Player player, NPC npc)Starts a dialog between player and NPC.
npcManager.startDialog(player, npc);
Parameters:
player - Playernpc - NPCEvents: Fires DialogStartEvent
Side Effects: Opens dialog interface, stores dialog state
getDialogState(Player player)Gets the current dialog state for a player.
DialogState state = npcManager.getDialogState(player);
if (state != null) {
DialogNode currentNode = state.getCurrentNode();
NPC npc = state.getNPC();
logger.info("Player in dialog with {} at node {}",
npc.getName(), currentNode.getId());
}
Parameters:
player - PlayerReturns: DialogState or null if not in dialog
selectChoice(Player player, int choiceIndex)Selects a dialog choice.
// Player clicked choice #2
npcManager.selectChoice(player, 1);
Parameters:
player - PlayerchoiceIndex - Zero-based choice indexEvents: Fires DialogChoiceEvent
Side Effects: Advances dialog, may execute actions
public class NPCInteractionListener {
private final NPCManager npcManager;
public void register() {
eventBus.subscribe(PlayerInteractEntityEvent.class, this::onEntityInteract);
eventBus.subscribe(DialogChoiceEvent.class, this::onDialogChoice);
}
private void onEntityInteract(PlayerInteractEntityEvent event) {
Entity entity = event.getEntity();
Player player = event.getPlayer();
// Check if entity is an NPC
String npcId = entity.getMetadata("npc_id");
if (npcId != null) {
NPC npc = npcManager.getRegistry().getNPC(npcId);
if (npc != null) {
event.setCancelled(true);
npcManager.handleInteraction(player, npc);
}
}
}
private void onDialogChoice(DialogChoiceEvent event) {
Player player = event.getPlayer();
DialogChoice choice = event.getChoice();
logger.info("Player {} selected: {}",
player.getName(), choice.getText());
// Execute custom actions based on choice
if (choice.getAction() == DialogAction.SHOW_QUEST_LIST) {
showAvailableQuests(player, event.getNPC());
}
}
}
Represents a node in a dialog tree.
public interface DialogNode {
String getId();
String getText();
List<DialogChoice> getChoices();
Predicate<Player> getCondition();
Consumer<Player> getAction();
boolean isEndNode();
static DialogNodeBuilder builder() {
return new DialogNodeBuilderImpl();
}
}
getText()Returns the dialog text displayed to the player.
DialogNode node = dialogTree.getCurrentNode();
String text = node.getText();
player.sendMessage("§e[NPC] §f" + text);
Returns: Dialog text with color code support
Placeholders: Supports {player}, {npc}, etc.
getChoices()Returns available dialog choices.
List<DialogChoice> choices = node.getChoices();
for (int i = 0; i < choices.size(); i++) {
DialogChoice choice = choices.get(i);
player.sendMessage(String.format("§7[%d] §f%s", i + 1, choice.getText()));
}
Returns: Immutable list of dialog choices
Filtering: Choices with unsatisfied conditions are excluded
getCondition()Returns the condition that must be met to show this node.
Predicate<Player> condition = node.getCondition();
if (condition.test(player)) {
// Show dialog
}
Returns: Condition predicate or null if no condition
Default: Always returns true if no condition set
DialogNode questNode = DialogNode.builder()
.id("quest_offer")
.text("I have a quest for you, {player}. Are you interested?")
.condition(player -> {
// Only show if player doesn't have active quest
QuestManager qm = getQuestManager();
return !qm.isActive(player, "main_quest_1");
})
.choice(
DialogChoice.builder()
.text("Yes, I'll help you!")
.action(player -> {
QuestManager qm = getQuestManager();
qm.startQuest(player, "main_quest_1");
})
.nextNode("quest_accepted")
.build()
)
.choice(
DialogChoice.builder()
.text("Not right now.")
.nextNode("root")
.build()
)
.build();
Represents a dialog choice/option.
public interface DialogChoice {
String getText();
String getNextNodeId();
Predicate<Player> getCondition();
Consumer<Player> getAction();
DialogAction getAction();
static DialogChoiceBuilder builder() {
return new DialogChoiceBuilderImpl();
}
}
getText()Returns the choice text displayed to player.
String choiceText = choice.getText();
// "Tell me about the quest."
Returns: Choice display text
getNextNodeId()Returns the ID of the node to navigate to.
String nextNode = choice.getNextNodeId();
// Navigate to next node after choice selected
Returns: Next node ID or null to end dialog
getCondition()Returns the condition for showing this choice.
DialogChoice premiumChoice = DialogChoice.builder()
.text("I'd like to see premium quests.")
.condition(player -> player.hasPermission("quests.premium"))
.nextNode("premium_quests")
.build();
Returns: Condition predicate or null
Behavior: Choice hidden if condition fails
getAction()Returns the action to execute when choice is selected.
DialogChoice acceptQuest = DialogChoice.builder()
.text("I accept the quest!")
.action(player -> {
questManager.startQuest(player, "main_quest_1");
player.sendMessage("§aQuest started!");
})
.nextNode("quest_accepted")
.build();
Returns: Action consumer or null
Execution: Runs when choice is selected
public enum DialogAction {
END_CONVERSATION,
SHOW_QUEST_LIST,
SHOW_SHOP,
GIVE_ITEM,
TELEPORT,
CUSTOM
}
Represents a complete dialog tree structure.
public interface DialogTree {
DialogNode getRootNode();
DialogNode getNode(String nodeId);
Map<String, DialogNode> getAllNodes();
static DialogTreeBuilder builder() {
return new DialogTreeBuilderImpl();
}
}
public class DialogTreeFactory {
public static DialogTree createQuestGiverDialog() {
return DialogTree.builder()
.rootNode(
DialogNode.builder()
.id("root")
.text("Greetings, brave adventurer! How may I assist you?")
.choice(
DialogChoice.builder()
.text("What quests do you have?")
.nextNode("quest_list")
.build()
)
.choice(
DialogChoice.builder()
.text("Tell me about this place.")
.nextNode("about_location")
.build()
)
.choice(
DialogChoice.builder()
.text("Goodbye.")
.action(DialogAction.END_CONVERSATION)
.build()
)
.build()
)
.node("quest_list",
DialogNode.builder()
.id("quest_list")
.text("I have several quests available:")
.choice(
DialogChoice.builder()
.text("Main Quest: The Lost Artifact")
.condition(player ->
!questManager.isCompleted(player, "main_quest_1"))
.nextNode("quest_main_1")
.build()
)
.choice(
DialogChoice.builder()
.text("Side Quest: Help the Farmers")
.condition(player ->
!questManager.isCompleted(player, "side_quest_1"))
.nextNode("quest_side_1")
.build()
)
.choice(
DialogChoice.builder()
.text("Back.")
.nextNode("root")
.build()
)
.build()
)
.node("quest_main_1",
DialogNode.builder()
.id("quest_main_1")
.text("The ancient artifact was lost in the mountain caves. Will you retrieve it?")
.choice(
DialogChoice.builder()
.text("I accept this quest!")
.action(player -> questManager.startQuest(player, "main_quest_1"))
.nextNode("quest_accepted")
.build()
)
.choice(
DialogChoice.builder()
.text("Not right now.")
.nextNode("quest_list")
.build()
)
.build()
)
.node("quest_accepted",
DialogNode.builder()
.id("quest_accepted")
.text("Excellent! Good luck on your journey.")
.choice(
DialogChoice.builder()
.text("Thank you!")
.action(DialogAction.END_CONVERSATION)
.build()
)
.build()
)
.build();
}
}
Defines autonomous NPC behavior.
public interface NPCBehavior {
void tick(NPC npc);
boolean shouldExecute(NPC npc);
void start(NPC npc);
void stop(NPC npc);
int getPriority();
}
Makes NPC look at nearby players.
NPCBehavior lookAtPlayer = new LookAtPlayerBehavior(5.0); // 5 block radius
npc.addBehavior(lookAtPlayer);
Makes NPC patrol between waypoints.
List<Location> waypoints = Arrays.asList(
new Location(world, 100, 64, 100),
new Location(world, 120, 64, 100),
new Location(world, 120, 64, 120)
);
NPCBehavior patrol = new PatrolBehavior(waypoints);
npc.addBehavior(patrol);
Makes NPC greet nearby players.
NPCBehavior greet = new GreetPlayerBehavior(
3.0, // Radius
"Hello, {player}!",
Duration.ofMinutes(5) // Cooldown
);
npc.addBehavior(greet);
public class QuestReminderBehavior implements NPCBehavior {
private final String questId;
private final double radius;
public QuestReminderBehavior(String questId, double radius) {
this.questId = questId;
this.radius = radius;
}
@Override
public void tick(NPC npc) {
Collection<Player> nearbyPlayers =
npc.getLocation().getNearbyPlayers(radius);
for (Player player : nearbyPlayers) {
QuestManager qm = getQuestManager();
if (qm.isActive(player, questId)) {
QuestProgress progress = qm.getProgress(player, questId);
if (progress.getCompletionPercentage() == 100) {
npc.lookAt(player);
player.sendMessage("§e[" + npc.getName() + "] §fI see you've completed the quest!");
}
}
}
}
@Override
public boolean shouldExecute(NPC npc) {
return npc.isSpawned() && !npc.getLocation().getNearbyPlayers(radius).isEmpty();
}
@Override
public int getPriority() {
return 5;
}
}
Handles custom NPC interactions.
public interface InteractionHandler {
void handleInteraction(Player player, NPC npc, InteractionType type);
}
public enum InteractionType {
RIGHT_CLICK,
LEFT_CLICK,
SHIFT_RIGHT_CLICK,
SHIFT_LEFT_CLICK
}
public class ShopKeeperInteraction implements InteractionHandler {
@Override
public void handleInteraction(Player player, NPC npc, InteractionType type) {
if (type == InteractionType.RIGHT_CLICK) {
openShop(player);
} else if (type == InteractionType.SHIFT_RIGHT_CLICK) {
showShopInfo(player);
}
}
private void openShop(Player player) {
// Open shop GUI
}
}
// Register handler
NPC shopKeeper = NPC.builder()
.id("shop_keeper")
.name("§6Shop Keeper")
.interactionHandler(new ShopKeeperInteraction())
.build();
Fired when an NPC spawns.
public class NPCSpawnEvent extends Event implements Cancellable {
private final NPC npc;
private boolean cancelled;
public NPC getNPC() { return npc; }
public boolean isCancelled() { return cancelled; }
public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
}
Example:
eventBus.subscribe(NPCSpawnEvent.class, event -> {
NPC npc = event.getNPC();
logger.info("NPC spawned: {} at {}", npc.getName(), npc.getLocation());
});
Fired when a player interacts with an NPC.
public class PlayerInteractNPCEvent extends Event implements Cancellable {
private final Player player;
private final NPC npc;
private final InteractionType interactionType;
private boolean cancelled;
public Player getPlayer() { return player; }
public NPC getNPC() { return npc; }
public InteractionType getInteractionType() { return interactionType; }
public boolean isCancelled() { return cancelled; }
public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
}
Example:
eventBus.subscribe(PlayerInteractNPCEvent.class, event -> {
Player player = event.getPlayer();
NPC npc = event.getNPC();
// Custom interaction logic
if (npc.getMetadata("quest_giver") == Boolean.TRUE) {
showAvailableQuests(player, npc);
event.setCancelled(true); // Prevent default dialog
}
});
Fired when dialog starts.
public class DialogStartEvent extends Event implements Cancellable {
private final Player player;
private final NPC npc;
private final DialogTree dialogTree;
private boolean cancelled;
public Player getPlayer() { return player; }
public NPC getNPC() { return npc; }
public DialogTree getDialogTree() { return dialogTree; }
}
Fired when player selects a dialog choice.
public class DialogChoiceEvent extends Event implements Cancellable {
private final Player player;
private final NPC npc;
private final DialogNode currentNode;
private final DialogChoice choice;
private boolean cancelled;
public Player getPlayer() { return player; }
public NPC getNPC() { return npc; }
public DialogNode getCurrentNode() { return currentNode; }
public DialogChoice getChoice() { return choice; }
}