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.
Community reports
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.
Dependencies
- Fabric Loader0.19.3+
- Fabric API0.154.2+26.2+
Feature overview
- 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
latest v0.1.0Developer API
Java · Fabricpackage 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.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"); 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.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"); 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.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"); 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.







