Why
A custom mob is a vanilla entity with a PDC tag, tuned attributes and listeners. The problem: the tag must be set on every spawn, and finding "all my mobs" means walking every entity in every world.
EffectiveEntity sets the key itself, caches live instances as chunks load and gives click and hit handlers per type. Spawn eggs and composite entities are built on top of it.
Minimal example
object Guard : EffectiveEntityWithSpawnEgg() {
override fun getEntityType() = EntityType.ZOMBIE
override fun getNamespacedData() = MyPlugin.instance to "guard"
override fun editEntity(entity: Entity) {
entity.customName(Component.text("Guard"))
entity.isCustomNameVisible = true
(entity as Zombie).isBaby = false
}
fun init() {
addInteractHandler(Click.RIGHT) { e ->
e.player.sendMessage("Hello")
Result.CANCEL_EVENT
}
doEntityNearLookable(lookDistance = 6f)
}
}EffectiveEntityWithSpawnEgg is EffectiveEntity plus an automatic guard_spawn_egg egg. If you do not need the egg, extend EffectiveEntity.
Guard.spawnEntity(location)
Guard.spawnEntity(location, listOf("5")) // с AdditionalArgs
val all = Guard.getEntities()
val here = Guard.getEntitiesInBlock(block)
if (EffectiveEntity.getNamespacedKeyByEntity(e) == Guard.getNamespacedKey()) { … }Interfaces and the egg
Clicks and looking are Effective* interfaces, like for items; the egg is a separate wrapper item.
addInteractHandler(Click.RIGHT, { e ->
e.player.sendMessage("hi from " + e.clickedEntity.name)
Result.CANCEL_EVENT
})
addInteractHandler(Click.LEFT, { e -> … Result.ALLOW_EVENT }, cooldownData = CooldownData(20))
EffectiveEntityInteractable.addInteractHandler(cowEntity, Click.RIGHT, { e -> … })RIGHT — right-click on the entity, LEFT — attack; sneak variants as for items. In e: player, clickedEntity, hand, click. Via EffectiveEntityInteractable.addInteractHandler with a vanilla entity (no key) the handler hooks its whole EntityType.
doEntityNearLookable()
doEntityNearLookable(lookDistance = 10f)
doEntityNearLookable(whoToLook = Look.TO_NEAR_ENTITY)
doEntityNearLookable(whoToLook = { it is Player && it.isSneaking }, lookDistance = 4f)The entity turns its head towards the nearest target within lookDistance blocks. Look.TO_NEAR_PLAYER (default) and TO_NEAR_ENTITY, or your own (Entity) -> Boolean predicate.
object Guard : EffectiveEntityWithSpawnEgg() {
…
override fun getSpawnEggMaterial() = Material.ZOMBIE_SPAWN_EGG
override fun getSpawnPlacement() = SpawnPlacement.TOP
override fun editSpawnEggMeta(meta: ItemMeta) {
meta.displayName(Component.text("Guard egg"))
}
}
player.inventory.addItem(Guard.getSpawnEggItem(3))
object GuardEgg : SummoningEggItem() {
override fun getSpawnEffectiveEntity() = Guard
override fun getMaterial() = Material.EGG
override fun getNamespacedData() = MyPlugin.instance to "guard_egg"
override fun editMeta(meta: ItemMeta) {}
}EffectiveEntityWithSpawnEgg makes the egg for you (key <id>_spawn_egg); SummoningEggItem — when the egg should be a separate item. SpawnPlacement: VANILLA — like a vanilla egg (into a passable block or against the clicked face), TOP/BOTTOM — above/below the block, CENTER — block centre, EXACT — the click point. The entity AdditionalArgs travel through the egg.
override fun getAdditionalArgs() = AdditionalArgs(
MyPlugin.instance,
listOf("power" to PersistentDataType.INTEGER),
)
override fun editEntity(entity: Entity) {
val power = EffectiveDataContainerUtils.getContainerValue<Int>(entity, additionalKey("power")) ?: 1
(entity as Zombie).getAttribute(Attribute.ATTACK_DAMAGE)?.baseValue = power.toDouble()
}
Guard.spawnEntity(location, listOf("5"))Same as for items: values in declaration order, in the entity PDC, available already in editEntity. Command: /emob guard 5. With EffectiveEntityWithSpawnEgg the parameters travel in the egg and apply on spawn.
What to override
- getEntityType() *Base vanilla type
- getNamespacedData() *Plugin and id
- editEntity(entity) *Attributes, name, equipment — called on every spawn
- getAdditionalArgs()Default: nullPer-instance parameters in PDC
- getSpawnEggMaterial()Default: PIG_SPAWN_EGGEffectiveEntityWithSpawnEgg only
- getSpawnPlacement()Default: VANILLATOP / BOTTOM / VANILLA / CENTER / EXACT
- editSpawnEggMeta(meta)Default: —Egg name and lore
* required
Pitfalls
- addInteractHandler on a non-custom entity (say a plain cow) fires for every cow — matching falls back to EntityType.
- doEntityNearLookable sends a rotation packet, not a teleport — on purpose, so the client is not jerked around.
- Egg methods are called from the constructor — constants only, not subclass fields.