LunaNPC turns Minecraft worlds into living places. Spawn and edit NPCs in-game, give them zones to roam, alliances and factions to honor, and combat behavior of your own design. No datapacks, no restarts.
Overview
LunaNPC is a Fabric-first NPC framework for building and controlling custom NPCs. It supports combat, factions, spawning, trading, dialog, and custom models straight from in-game menus, and developers get the exact same features through a public API.
- Server ownersFill the world with guards, traders and enemies from in-game menus, no restarts.
- Map makersQuests, dialog trees and patrol routes for story maps and adventures.
- DevelopersThe same features through a public Java API, with versioned docs.
Features
- Combat
Melee, ranged, and a node-based advanced combat editor with rules, triggers, timed attacks, and projectile accuracy curves.
- Factions
Built-in factions; NPCs choose allies and enemies, and can belong to groups.
- Spawning
Place NPCs in biomes, structures, or custom zones on the ground, in water or in the air, with respawn and persistence control.
- Navigation
Ground, swimming, and flight movement, backed by multiple A* pathfinders.
- Trading & Jobs
Trading, quests, farming, and branching dialog trees with action triggers.
- Custom models
Import custom models and animations, with automatic hitboxes.
- Full customization
Health, equipment, per-slot enchanting, drops, sounds and behavior, all tunable.
- In-game or in code
Everything is available through menus and mirrored in a developer API.
Versions
- Minecraft26.3
- Fabric Loader0.19.5or newer
- Fabric API0.164.0+26.3or newer
- AddedPatrol job: placed NPCs walk a saved route in a loop, back and forth or in random order, and pick up at the nearest point after a fight.
- AddedRoute Creator stick: click the ground to place up to 50 points, then sneak-click to name the route. Its menu edits routes, and one route can serve many NPCs.
- AddedBodyguard job: guards chosen players, alliances or NPCs, or whoever hires it with a right-click. It stays close and fights anyone who hurts its protectee.
- AddedBodyguard rules for retreating, teleporting to catch up (also across dimensions), protecting a player through respawn, and what to do when it gives up.
- AddedBodyguard friendly fire switch, so the ones it guards can't hurt it by accident.
- AddedBodyguards can be hired through new dialog actions and
/lunanpc bodyguard assign|dismiss|order. The hirer can tell it to follow, stay, go home or leave. - AddedChunk Loader trait: keeps the chunks around the NPC loaded, so patrols and bodyguards keep going with no player nearby.
- AddedDownload Skin in the Model picker: download a player's skin once by name or URL, and everyone sees it. It is saved with the world, so later skin changes never affect it.
- AddedSkin Settings for downloaded skins: rename, switch between classic and slim arms, or delete.
- AddedEdit access per NPC on the General tab: API only, in-game only, or both. API-only NPCs show a lock in the NPC lists.
- AddedFixed accuracy slider that sets how accurate every shot is (0–100%) when dynamic accuracy is off.
- AddedTarget In Sight value for advanced combat conditions, so attacks can stop firing through walls.
- AddedSpears are carried in the charge pose while running and play their vanilla sounds, with vanilla's charge bonus when gear affects stats.
- AddedMore models can hold items or wear heads, skulls and blocks, and the Inventory tab only enables the slots a model can show. (Suggested By @daekthielen)
- AddedThe Manage Permissions log shows player heads and records skin downloads, edits and deletes.
- AddedOps get a red chat warning when a saved NPC's setup can't work as configured.
- AddedSetup warnings for patrol, bodyguard, Chunk Loader and spawn-area rules, such as a patrol without a route.
- AddedAPI:
RouteRegistry,NpcRoute,NpcPatrolSubType,NpcBodyguardSettingsand patrol and bodyguard controls onNpcInstance, with theonPatrolPointReached,onBodyguardLockedandonBodyguardReleasedevents. - AddedAPI:
LunaNpcApi.skins(server)withSkinRegistryto manage downloaded skins. - AddedAPI:
NpcEditAccesswitheditAccess()/setEditAccessonNpcDefinition. Changes the access forbids throwIllegalStateException. - AddedAPI:
AllianceRegistry.setMatchAllPlayersandsetFriendlyFire. - FixedNPCs stuck chasing an unreachable target ignored players hitting them.
- FixedMelee attacks landed through walls.
- FixedMelee NPCs got stuck below their target or walked back into pits instead of going around.
- FixedWide NPCs needed gaps one block wider than their body.
- FixedNPCs knocked off their route kept pushing at the missed ledge instead of finding a new way.
- FixedNPCs that gave up on a target never chased it again while it stayed in view.
- FixedLeft-clicking an item slot in a menu cleared it instead of opening it.
- FixedNPCs routing around a wall kept turning back and edged slowly along it.
- FixedNPCs hopped constantly when brushing past walls, slowing them to a crawl.
- FixedFast NPCs circled around corners and obstacles instead of rounding them.
- FixedAlliance changes made through the API didn't reach connected players right away.
- ChangedLunaNPC now runs on Minecraft 26.3.
- ChangedAttack speeds faster than 0.5s now really hit that fast, and a new speed applies on the next attack.
- ChangedCrossbows stay raised and reload right after each shot like a pillager, and bows stay drawn for the whole fight.
- ChangedNPCs no longer flip weapons at the boundary between close and long range.
- ChangedArmed illagers use their vanilla weapon poses.
- ChangedThe Main Hand and Ranged Weapon slots now explain what they do.
- ChangedItems that don't fit in a full inventory drop at the player's feet instead of toward the NPC.
- ChangedAPI:
NpcRegistry.createandgetOrCreatenow take anNpcEditAccess; passNpcEditAccess.SHAREDto keep the old behavior. - ChangedAPI:
NpcJobSettings,NpcTraitsSettingsandNpcProjectileSettingshave new components, so direct constructor calls need updating. Thewith…methods are unaffected.
5f61e304d7423f10c47ef02cbd8ac36c7021101e0d6a56911c2b15a99ea6d985- Minecraft26.2
- Fabric Loader0.19.3or newer
- Fabric API0.154.2+26.2or newer
- AddedFirst public release of LunaNPC
d5b203803bd6cbde20f1c4aa3676e8d5ec47cdcfac021043ebe1ca03636e7fadDeveloper API
package net.lunacore.lunanpc.examples; import net.minecraft.server.MinecraftServer; import net.lunacore.luna_npc.api.LunaNpcApi; import net.lunacore.luna_npc.api.NpcAttackSettings; import net.lunacore.luna_npc.api.NpcDefinition; import net.lunacore.luna_npc.api.NpcEditAccess; import net.lunacore.luna_npc.api.NpcReactionSettings; import net.lunacore.luna_npc.api.NpcRegistry; import net.lunacore.luna_npc.api.SurfaceMode; // A heavy melee construct: a metallic iron golem that natural-spawns at villages, hits hard but slowly, // and drops scrap. Shows structure spawning, a weaponless melee attack, and per-item chance drops. public final class SteelGolemExample { private SteelGolemExample() { } public static void build(MinecraftServer server) { NpcRegistry npcs = LunaNpcApi.npcs(server); NpcDefinition golem = npcs.getOrCreate("SteelGolem", NpcEditAccess.API_ONLY); golem.setHealth(200); golem.setCategory("golems"); golem.setAllianceId("golems"); golem.setModelSettings(golem.modelSettings().withModel("iron_golem", "iron_golem_movie")); golem.setHurtSoundId("minecraft:entity.iron_golem.hurt"); golem.setDieSoundId("minecraft:entity.iron_golem.death"); golem.setCombatCloseSoundId("minecraft:entity.iron_golem.attack"); // 20 damage, 3-block reach, one swing every 5 seconds — no weapon item, it strikes bare-handed. golem.setCombat(golem.combat().withMelee(NpcAttackSettings.melee(20.0F, 3.0F).withInterval(5.0F))); // Walks and charges at the same 2 blocks/second. golem.setMovement(golem.movement().withWalkSpeed(2.0F).withRunSpeed(2.0F)); golem.setTraits(golem.traits().asCreature() .withReaction(NpcReactionSettings.defaults().withMode(NpcReactionSettings.AGGRESSIVE))); // Spawns at any village, at the surface, with a 5% spawn chance. golem.setSpawn(golem.spawn() .withStructureChance("minecraft:village_plains", 5) .withStructureChance("minecraft:village_desert", 5) .withStructureChance("minecraft:village_savanna", 5) .withStructureChance("minecraft:village_snowy", 5) .withStructureChance("minecraft:village_taiga", 5) .withSurfaceMode(SurfaceMode.SURFACE_ONLY)); // Each drop rolls its own chance on death. Drops are one item per slot (no stack counts), so // repeated slots stand in for "a few" stone / iron. golem.setDropSettings(golem.dropSettings() .withDrop(0, "minecraft:dandelion", 8.0F) .withDrop(1, "minecraft:poppy", 5.0F) .withDrop(2, "minecraft:cornflower", 3.0F) .withDrop(3, "minecraft:oxeye_daisy", 2.0F) .withDrop(4, "minecraft:flint_and_steel", 5.0F) .withDrop(5, "minecraft:stone", 10.0F) .withDrop(6, "minecraft:stone", 10.0F) .withDrop(7, "minecraft:stone", 10.0F) .withDrop(8, "minecraft:iron_ingot", 5.0F) .withDrop(9, "minecraft:iron_ingot", 5.0F) .withDrop(10, "minecraft:iron_ingot", 5.0F) .withDrop(11, "minecraft:iron_ingot", 5.0F)); } }
package net.lunacore.lunanpc.examples; import net.minecraft.server.MinecraftServer; import net.lunacore.luna_npc.api.LunaNpcApi; import net.lunacore.luna_npc.api.NpcDefinition; import net.lunacore.luna_npc.api.NpcEditAccess; import net.lunacore.luna_npc.api.NpcReactionSettings; import net.lunacore.luna_npc.api.NpcRegistry; import net.lunacore.luna_npc.api.SurfaceMode; // A fast melee zombie: cave-spawning, lightly armored, and far quicker at a charge than a walk. Shows // heavy model stretching, armor plus a weapon, cave/structure spawning, and a wide loot table. public final class ChargerZombieExample { private ChargerZombieExample() { } public static void build(MinecraftServer server) { NpcRegistry npcs = LunaNpcApi.npcs(server); NpcDefinition zombie = npcs.getOrCreate("ChargerZombie", NpcEditAccess.API_ONLY); zombie.setHealth(40); zombie.setCategory("creatures"); zombie.setAllianceId("undead"); zombie.setHurtSoundId("minecraft:entity.zombie.hurt"); zombie.setDieSoundId("minecraft:entity.zombie.death"); zombie.setAmbientSoundId("minecraft:entity.zombie.ambient"); // The lopsided stretch of the very first test zombie: big head, one huge right arm, stubby legs. zombie.setModelSettings(zombie.modelSettings() .withModel("zombie", "zombie") .withSize(0.89F) .withHeadScale(2.0F).withBodyScale(1.0F) .withLeftArmScale(0.5F).withRightArmScale(2.0F) .withLeftLegScale(0.5F).withRightLegScale(0.5F)); zombie.giveMeleeWeapon("minecraft:diamond_sword", 7.0F); zombie.setEquipment(zombie.equipment().withHelmet("minecraft:iron_helmet")); // Slow walk, fast charge. zombie.setMovement(zombie.movement().withWalkSpeed(2.0F).withRunSpeed(4.0F)); zombie.setTraits(zombie.traits().asCreature() .withReaction(NpcReactionSettings.defaults().withMode(NpcReactionSettings.AGGRESSIVE))); // Caves everywhere at 2%, plus 4% inside mineshafts. zombie.setSpawn(zombie.spawn() .withUseGlobalBiome(true).withGlobalBiomeChance(2) .withStructureChance("minecraft:mineshaft", 4) .withSurfaceMode(SurfaceMode.CAVES_ONLY)); zombie.setDropSettings(zombie.dropSettings() .withXp(10, 40) .withDrop(0, "minecraft:rotten_flesh", 80.0F) .withDrop(1, "minecraft:iron_ingot", 12.0F) .withDrop(2, "minecraft:carrot", 3.0F) .withDrop(3, "minecraft:potato", 3.0F) .withDrop(4, "minecraft:iron_helmet", 5.0F) .withDrop(5, "minecraft:diamond_sword", 3.0F)); } }
package net.lunacore.lunanpc.examples; import java.util.List; import net.minecraft.server.MinecraftServer; import net.lunacore.luna_npc.api.LunaNpcApi; import net.lunacore.luna_npc.api.NpcAccuracyAnchor; import net.lunacore.luna_npc.api.NpcAdvancedCombat; import net.lunacore.luna_npc.api.NpcAirSpawn; import net.lunacore.luna_npc.api.NpcAttackEffect; import net.lunacore.luna_npc.api.NpcCombatAction; import net.lunacore.luna_npc.api.NpcCombatActionType; import net.lunacore.luna_npc.api.NpcCombatAttack; import net.lunacore.luna_npc.api.NpcCombatAttackKind; import net.lunacore.luna_npc.api.NpcCombatComparator; import net.lunacore.luna_npc.api.NpcCombatCondition; import net.lunacore.luna_npc.api.NpcCombatPlacement; import net.lunacore.luna_npc.api.NpcCombatRule; import net.lunacore.luna_npc.api.NpcCombatTrigger; import net.lunacore.luna_npc.api.NpcCombatTriggerType; import net.lunacore.luna_npc.api.NpcDefinition; import net.lunacore.luna_npc.api.NpcEditAccess; import net.lunacore.luna_npc.api.NpcReactionSettings; import net.lunacore.luna_npc.api.NpcRegistry; import net.lunacore.luna_npc.api.NpcTargetSelector; import net.lunacore.luna_npc.api.NpcValueSource; import net.lunacore.luna_npc.api.NpcValueSourceType; // A flying nether blaze that shoots reddish fireballs on a timed rhythm, built with the node-based // advanced-combat system. The rhythm loops: a 2s-cadence volley for 5 seconds, a 5-second pause, then // a 0.5s-cadence burst for 1.5 seconds. Two boolean flags ("resting"/"bursting") plus timers drive the // phases; both clear = steady volley. Advanced combat suppresses default movement, so it holds flight // with an explicit SWITCH_NAV_MODE at combat start. public final class CrimsonBlazeExample { private static final String RESTING = "resting"; private static final String BURSTING = "bursting"; private static final int FLIGHT_HEIGHT = 10; private CrimsonBlazeExample() { } public static void build(MinecraftServer server) { NpcRegistry npcs = LunaNpcApi.npcs(server); NpcDefinition blaze = npcs.getOrCreate("CrimsonBlaze", NpcEditAccess.API_ONLY); blaze.setHealth(10); blaze.setAllianceId("creatures"); blaze.setModelSettings(blaze.modelSettings().withModel("blaze", "blaze")); blaze.setHurtSoundId("minecraft:entity.blaze.hurt"); blaze.setDieSoundId("minecraft:entity.blaze.death"); blaze.setAmbientSoundId("minecraft:entity.blaze.ambient"); blaze.setMovement(blaze.movement().withMovementType("air").withFlightHeight(FLIGHT_HEIGHT)); blaze.setTraits(blaze.traits().asCreature() .withFireImmune(true) .withReaction(NpcReactionSettings.defaults().withMode(NpcReactionSettings.AGGRESSIVE))); // 40-block aggro, prefers 15 blocks. The shot does not home, so its distance accuracy curve // applies: ~80% within 10 blocks, falling to ~40% past 25. blaze.setCombat(blaze.combat() .withAggroRange(40.0F) .withDistancePreference(15.0F) .withProjectile(blaze.combat().projectile() .withItem("luna_npc:red_fire") .withHoming(false) .withDynamicAccuracy(true) .withAccuracyAnchors(List.of( new NpcAccuracyAnchor(0, 80), new NpcAccuracyAnchor(10, 80), new NpcAccuracyAnchor(25, 40))))); blaze.setSpawn(blaze.spawn() .withBiomeChance("minecraft:nether_wastes", 10) .withBiomeChance("minecraft:crimson_forest", 10) .withBiomeChance("minecraft:warped_forest", 10) .withBiomeChance("minecraft:soul_sand_valley", 10) .withBiomeChance("minecraft:basalt_deltas", 10) .withAirSpawn(NpcAirSpawn.defaults().withWorldRange(128, 256).withAboveGround(34, 64))); blaze.setDropSettings(blaze.dropSettings() .withXp(10, 10) .withDrop(0, "minecraft:blaze_powder", 50.0F) .withDrop(1, "minecraft:blaze_rod", 8.0F)); NpcCombatAttack bolt = new NpcCombatAttack("red_bolt", NpcCombatAttackKind.PROJECTILE, 5.0F, 15.0F, 8.0F, "luna_npc:red_fire", false, false, 1, 0.0F, List.of(new NpcAttackEffect("flame", 0.0F, 1)), NpcCombatPlacement.atSelf()); // Rule graph positions are spread out so the editor shows six distinct nodes, not one stack. List<NpcCombatRule> rules = List.of( // Combat starts: hold flight, clear the flags, open the 5s steady window (100 ticks). rule(trigger(NpcCombatTriggerType.ON_COMBAT_START), List.of(), List.of(holdFlight(), clearFlag(RESTING), clearFlag(BURSTING), startTimer("toRest", 100)), -240, -120), // Steady volley: a shot every 2s while neither resting nor bursting. rule(interval(2.0F), List.of(flagClear(RESTING), flagClear(BURSTING)), List.of(shootSound(), shoot()), -240, 40), // 5s up -> rest for 5s. rule(onTimer("toRest"), List.of(), List.of(setFlag(RESTING), startTimer("toBurst", 100)), 0, -120), // rest up -> burst for 1.5s (30 ticks). rule(onTimer("toBurst"), List.of(), List.of(clearFlag(RESTING), setFlag(BURSTING), startTimer("toSteady", 30)), 0, 40), // Burst: a shot every 0.5s while bursting (~3 shots). rule(interval(0.5F), List.of(flagSet(BURSTING)), List.of(shootSound(), shoot()), 240, 40), // burst up -> back to the steady window. rule(onTimer("toSteady"), List.of(), List.of(clearFlag(BURSTING), startTimer("toRest", 100)), 240, -120)); blaze.setAdvancedCombat(NpcAdvancedCombat.defaults() .withEnabled(true) .withEngagementRadius(40.0F) .withAttackLibrary(List.of(bolt)) .withRules(rules)); } // Holds air navigation for the whole fight (5 min ceiling) so advanced combat keeps it flying. The // fixed flag holds the flight height even up close, instead of giving way down to the target. private static NpcCombatAction holdFlight() { return new NpcCombatAction(NpcCombatActionType.SWITCH_NAV_MODE, NpcTargetSelector.SELF, NpcCombatPlacement.atSelf(), NpcValueSource.constant(FLIGHT_HEIGHT), 6000, 0, "fly", List.of(), true); } private static NpcCombatAction shoot() { return new NpcCombatAction(NpcCombatActionType.USE_ATTACK, NpcTargetSelector.PRIMARY, NpcCombatPlacement.atTarget(), NpcValueSource.constant(0.0F), 0, 0, "red_bolt", List.of(), false); } private static NpcCombatAction shootSound() { return new NpcCombatAction(NpcCombatActionType.SOUND, NpcTargetSelector.SELF, NpcCombatPlacement.atSelf(), NpcValueSource.constant(0.0F), 0, 0, "minecraft:entity.blaze.shoot", List.of(), false); } private static NpcCombatAction setFlag(String key) { return flagAction(NpcCombatActionType.SET_FLAG, key, 1.0F); } private static NpcCombatAction clearFlag(String key) { return flagAction(NpcCombatActionType.CLEAR_FLAG, key, 0.0F); } private static NpcCombatAction flagAction(NpcCombatActionType type, String key, float value) { return new NpcCombatAction(type, NpcTargetSelector.SELF, NpcCombatPlacement.atSelf(), NpcValueSource.constant(value), 0, 0, key, List.of(), false); } // durationTicks is the timer length in ticks (20 per second). private static NpcCombatAction startTimer(String key, int ticks) { return new NpcCombatAction(NpcCombatActionType.START_TIMER, NpcTargetSelector.SELF, NpcCombatPlacement.atSelf(), NpcValueSource.constant(0.0F), ticks, 0, key, List.of(), false); } private static NpcCombatCondition flagSet(String key) { return flagCondition(key, NpcCombatComparator.GREATER_EQUAL); } private static NpcCombatCondition flagClear(String key) { return flagCondition(key, NpcCombatComparator.LESS); } private static NpcCombatCondition flagCondition(String key, NpcCombatComparator cmp) { return new NpcCombatCondition(new NpcValueSource(NpcValueSourceType.FLAG_VALUE, 0.0F, 0.0F, 0, key), cmp, NpcValueSource.constant(1.0F), false); } private static NpcCombatTrigger trigger(NpcCombatTriggerType type) { return new NpcCombatTrigger(type, 0.0F, 0.0F, ""); } private static NpcCombatTrigger interval(float seconds) { return new NpcCombatTrigger(NpcCombatTriggerType.INTERVAL, seconds, 0.0F, ""); } private static NpcCombatTrigger onTimer(String key) { return new NpcCombatTrigger(NpcCombatTriggerType.ON_TIMER_ELAPSED, 0.0F, 0.0F, key); } private static NpcCombatRule rule(NpcCombatTrigger trigger, List<NpcCombatCondition> conditions, List<NpcCombatAction> actions, int graphX, int graphY) { return new NpcCombatRule(trigger, conditions, actions, 0.0F, false, graphX, graphY); } }
Full type reference
Every type, function and enum, browsable at api.lunacore.dev/LunaNPC.






