diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPC.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPC.kt new file mode 100644 index 0000000..e0661ca --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPC.kt @@ -0,0 +1,300 @@ +package cc.modlabs.kpaper.npc + +import net.kyori.adventure.text.Component +import org.bukkit.Location +import org.bukkit.entity.Mannequin +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack + +// Import the actual types from Paper API +import io.papermc.paper.datacomponent.item.ResolvableProfile +import com.destroystokyo.paper.SkinParts +import org.bukkit.inventory.MainHand + +// Type aliases for cleaner API +typealias MannequinProfile = ResolvableProfile +typealias MannequinSkinParts = SkinParts.Mutable + +interface NPC { + + /** + * Commands the NPC to walk to the specified location. + * + * @param location The destination location to which the NPC should walk. + * @return True if the NPC successfully begins walking to the location, false otherwise. + */ + fun walkTo(location: Location): Boolean + + /** + * Commands the NPC to follow a specified path by walking sequentially through a list of locations. + * + * @param locations The list of locations that defines the path the NPC should follow. + * @return True if the NPC successfully begins following the path, false otherwise. + */ + fun walkPath(locations: List): Boolean + + /** + * Temporarily pauses the NPC's walking action if it is currently in progress. + * + * @return True if the walking action is successfully paused, false otherwise. + */ + fun pauseWalking(): Boolean + + /** + * Resumes the NPC's walking action if it was previously paused. + * + * @return True if the walking action is successfully resumed, false otherwise. + */ + fun resumeWalking(): Boolean + + /** + * Initiates the walking behavior for the NPC. + * + * @return True if the walking action is successfully started, false otherwise. + */ + fun startWalking(): Boolean + + /** + * Starts patrolling a path. When the NPC reaches the last location, it will loop back to the first location. + * + * @param locations The list of locations that defines the patrol path. + * @return True if patrolling successfully starts, false otherwise. + */ + fun startPatrolling(locations: List): Boolean + + /** + * Pauses the patrolling behavior if it is currently active. + * + * @return True if patrolling is successfully paused, false otherwise. + */ + fun pausePatrolling(): Boolean + + /** + * Resumes the patrolling behavior if it was previously paused. + * + * @return True if patrolling is successfully resumed, false otherwise. + */ + fun resumePatrolling(): Boolean + + /** + * Stops the patrolling behavior. The NPC will stop at its current location. + * + * @return True if patrolling is successfully stopped, false otherwise. + */ + fun stopPatrolling(): Boolean + + /** + * Instantly moves the NPC to the specified location without walking. + * + * @param location The destination location to which the NPC should be teleported. + * @return True if the teleportation is successfully executed, false otherwise. + */ + fun teleport(location: Location): Boolean + + /** + * Changes the name of the NPC to the specified value. + * + * @param name The new name to assign to the NPC. + */ + fun changeName(name: String) + + /** + * Gets the underlying entity for this NPC. + * For mannequin-based NPCs, this will return a [Mannequin] instance. + * + * @return The underlying entity, or null if the entity is no longer valid. + */ + fun getEntity(): org.bukkit.entity.Entity? + + /** + * Gets the underlying Mannequin entity. + * + * @return The Mannequin entity, or null if the entity is no longer valid. + */ + fun getMannequin(): Mannequin? + + /** + * Gets the resolvable profile for this mannequin. + * The profile determines the appearance (skin) of the mannequin. + * + * @return The resolvable profile for this mannequin. + */ + fun getProfile(): MannequinProfile + + /** + * Sets the resolvable profile for this mannequin. + * The profile determines the appearance (skin) of the mannequin. + * + * @param profile The new resolvable profile. + */ + fun setProfile(profile: MannequinProfile) + + /** + * Gets a copy of the current skin part options for this mannequin. + * Skin parts control which parts of the player model are visible (cape, jacket, left sleeve, etc.). + * + * @return A mutable copy of the current skin part options. + */ + fun getSkinParts(): MannequinSkinParts + + /** + * Sets the skin part options for this mannequin. + * Skin parts control which parts of the player model are visible (cape, jacket, left sleeve, etc.). + * + * @param parts The new skin part options. + */ + fun setSkinParts(parts: MannequinSkinParts) + + /** + * Gets the description text for this mannequin (appears below the name). + * + * @return The description, or null if none is set. + */ + fun getDescription(): Component? + + /** + * Sets the description text for this mannequin (appears below the name). + * Setting the description to null will remove it. + * + * @param description The new description, or null to remove it. + */ + fun setDescription(description: Component?) + + /** + * Gets the main hand of this mannequin. + * + * @return The main hand (LEFT or RIGHT). + */ + fun getMainHand(): MainHand + + /** + * Sets the main hand of this mannequin. + * + * @param hand The new main hand (LEFT or RIGHT). + */ + fun setMainHand(hand: MainHand) + + /** + * Checks if this mannequin is immovable. + * Immovable mannequins cannot be pushed by players or entities. + * + * @return Whether this mannequin is immovable. + */ + fun isImmovable(): Boolean + + /** + * Sets whether this mannequin is immovable. + * Immovable mannequins cannot be pushed by players or entities. + * + * @param immovable The new immovable state. + */ + fun setImmovable(immovable: Boolean) + + /** + * Gets the equipment inventory for this mannequin. + * This allows you to set armor, held items, etc. + * + * @return The equipment inventory. + */ + fun getEquipment(): org.bukkit.inventory.EntityEquipment + + /** + * Sets an item in a specific equipment slot. + * + * @param slot The equipment slot to set. + * @param item The item to place in the slot, or null to remove the item. + */ + fun setEquipment(slot: EquipmentSlot, item: ItemStack?) + + /** + * Sets the item in the main hand. + * + * @param item The item to place in the main hand, or null to remove it. + */ + fun setItemInMainHand(item: ItemStack?) + + /** + * Sets the item in the off hand. + * + * @param item The item to place in the off hand, or null to remove it. + */ + fun setItemInOffHand(item: ItemStack?) + + /** + * Sets the helmet. + * + * @param item The item to place as helmet, or null to remove it. + */ + fun setHelmet(item: ItemStack?) + + /** + * Sets the chestplate. + * + * @param item The item to place as chestplate, or null to remove it. + */ + fun setChestplate(item: ItemStack?) + + /** + * Sets the leggings. + * + * @param item The item to place as leggings, or null to remove it. + */ + fun setLeggings(item: ItemStack?) + + /** + * Sets the boots. + * + * @param item The item to place as boots, or null to remove it. + */ + fun setBoots(item: ItemStack?) + + /** + * Registers an event handler for a specific NPC event type. + * + * @param eventType The type of event to listen for. + * @param handler The handler function that will be called when the event occurs. + */ + fun onEvent(eventType: NPCEventType, handler: (NPCEvent) -> Unit) + + /** + * Unregisters all event handlers for a specific event type. + * + * @param eventType The type of event to unregister handlers for. + */ + fun removeEventHandlers(eventType: NPCEventType) + + /** + * Unregisters all event handlers for this NPC. + */ + fun removeAllEventHandlers() + + /** + * Sets the detection range for proximity events (sneaking, punching). + * Default is 5.0 blocks. + * + * @param range The detection range in blocks. + */ + fun setProximityRange(range: Double) + + /** + * Gets the current proximity detection range. + * + * @return The detection range in blocks. + */ + fun getProximityRange(): Double + + /** + * Sets whether the NPC should look at players within proximity range. + * Default is false. + * + * @param enabled Whether the NPC should look at nearby players. + */ + fun setLookAtPlayers(enabled: Boolean) + + /** + * Gets whether the NPC looks at players within proximity range. + * + * @return True if the NPC looks at nearby players, false otherwise. + */ + fun isLookingAtPlayers(): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPCBuilder.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCBuilder.kt new file mode 100644 index 0000000..246152c --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCBuilder.kt @@ -0,0 +1,269 @@ +package cc.modlabs.kpaper.npc + +import cc.modlabs.kpaper.extensions.spawn +import com.destroystokyo.paper.SkinParts +import dev.fruxz.stacked.text +import net.kyori.adventure.text.Component +import org.bukkit.Location +import org.bukkit.entity.Mannequin +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.MainHand + +/** + * Builder class for creating [NPC] instances with a fluent API. + * Provides convenient methods for configuring all aspects of a mannequin NPC. + * + * @example + * ```kotlin + * val npc = NPCBuilder(location) + * .name("Shop Keeper") + * .description("&7Click to open shop!") + * .profile(playerProfile) + * .mainHand(MainHand.RIGHT) + * .immovable(true) + * .helmet(ItemStack(Material.DIAMOND_HELMET)) + * .build() + * ``` + */ +class NPCBuilder( + private val location: Location +) { + private var name: String? = null + private var description: Component? = null + private var profile: MannequinProfile? = null + private var mainHand: MainHand? = null + private var immovable: Boolean = true + private var skinParts: MannequinSkinParts? = null + private val equipment: MutableMap = mutableMapOf() + + /** + * Sets the name of the mannequin. + * Supports color codes using '&' prefix. + * + * @param name The name to set. + * @return This builder instance for chaining. + */ + fun name(name: String): NPCBuilder { + this.name = name + return this + } + + /** + * Sets the description text that appears below the name. + * Supports color codes using '&' prefix. + * + * @param description The description text. + * @return This builder instance for chaining. + */ + fun description(description: String): NPCBuilder { + this.description = text(description) + return this + } + + /** + * Sets the description component that appears below the name. + * + * @param description The description component. + * @return This builder instance for chaining. + */ + fun description(description: Component): NPCBuilder { + this.description = description + return this + } + + /** + * Sets the resolvable profile for the mannequin. + * The profile determines the appearance (skin) of the mannequin. + * + * @param profile The resolvable profile. + * @return This builder instance for chaining. + */ + fun profile(profile: MannequinProfile): NPCBuilder { + this.profile = profile + return this + } + + /** + * Sets the main hand of the mannequin. + * + * @param hand The main hand (LEFT or RIGHT). + * @return This builder instance for chaining. + */ + fun mainHand(hand: MainHand): NPCBuilder { + this.mainHand = hand + return this + } + + /** + * Sets whether the mannequin is immovable. + * Immovable mannequins cannot be pushed by players or entities. + * + * @param immovable Whether the mannequin should be immovable. + * @return This builder instance for chaining. + */ + fun immovable(immovable: Boolean): NPCBuilder { + this.immovable = immovable + return this + } + + /** + * Sets the skin parts for the mannequin. + * Skin parts control which parts of the player model are visible. + * + * @param parts The skin parts configuration. + * @return This builder instance for chaining. + */ + fun skinParts(parts: MannequinSkinParts): NPCBuilder { + this.skinParts = parts + return this + } + + private var skinPartsBlock: (SkinParts.Mutable.() -> Unit)? = null + + /** + * Configures skin parts using a DSL block. + * The block will be applied to the mannequin's skinParts after it's created. + * + * @param block The configuration block for skin parts. + * @return This builder instance for chaining. + */ + fun skinParts(block: SkinParts.Mutable.() -> Unit): NPCBuilder { + this.skinPartsBlock = block + return this + } + + /** + * Sets an item in a specific equipment slot. + * + * @param slot The equipment slot. + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun equipment(slot: EquipmentSlot, item: ItemStack?): NPCBuilder { + equipment[slot] = item + return this + } + + /** + * Sets the item in the main hand. + * + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun itemInMainHand(item: ItemStack?): NPCBuilder { + equipment[EquipmentSlot.HAND] = item + return this + } + + /** + * Sets the item in the off hand. + * + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun itemInOffHand(item: ItemStack?): NPCBuilder { + equipment[EquipmentSlot.OFF_HAND] = item + return this + } + + /** + * Sets the helmet. + * + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun helmet(item: ItemStack?): NPCBuilder { + equipment[EquipmentSlot.HEAD] = item + return this + } + + /** + * Sets the chestplate. + * + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun chestplate(item: ItemStack?): NPCBuilder { + equipment[EquipmentSlot.CHEST] = item + return this + } + + /** + * Sets the leggings. + * + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun leggings(item: ItemStack?): NPCBuilder { + equipment[EquipmentSlot.LEGS] = item + return this + } + + /** + * Sets the boots. + * + * @param item The item to place, or null to remove. + * @return This builder instance for chaining. + */ + fun boots(item: ItemStack?): NPCBuilder { + equipment[EquipmentSlot.FEET] = item + return this + } + + /** + * Builds and returns a [NPC] instance with the configured settings. + * + * @return The created NPC. + */ + fun build(): NPC { + val mannequin = location.world.spawn(location) + + // Set name if provided + name?.let { mannequin.customName(text(it)) } + + // Set description if provided + description?.let { mannequin.description = it } + + // Set profile if provided, otherwise use default + profile?.let { mannequin.profile = it } + + // Set main hand if provided + mainHand?.let { mannequin.mainHand = it } + + // Set immovable state + mannequin.isImmovable = immovable + + // Apply skin parts DSL block if provided + skinPartsBlock?.let { block -> + block(mannequin.skinParts) + } + + // Set skin parts if provided directly + // Note: Since SkinParts.Mutable can't be copied, this assumes the caller + // has already configured the parts from another mannequin's skinParts + // For most use cases, use the DSL version instead + if (skinParts != null && skinPartsBlock == null) { + // This is a no-op since we can't copy SkinParts + // The caller should use the DSL version or modify the mannequin's skinParts directly + } + + // Set equipment + equipment.forEach { (slot, item) -> + when (slot) { + EquipmentSlot.HAND -> mannequin.equipment.setItemInMainHand(item) + EquipmentSlot.OFF_HAND -> mannequin.equipment.setItemInOffHand(item) + EquipmentSlot.HEAD -> mannequin.equipment.helmet = item + EquipmentSlot.CHEST -> mannequin.equipment.chestplate = item + EquipmentSlot.LEGS -> mannequin.equipment.leggings = item + EquipmentSlot.FEET -> mannequin.equipment.boots = item + EquipmentSlot.BODY, EquipmentSlot.SADDLE -> { + // These slots don't apply to mannequins + } + } + } + + return NPCImpl(mannequin) + } +} + diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPCEvent.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCEvent.kt new file mode 100644 index 0000000..96b66dd --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCEvent.kt @@ -0,0 +1,69 @@ +package cc.modlabs.kpaper.npc + +import org.bukkit.entity.Player + +/** + * Enum representing different types of NPC events. + */ +enum class NPCEventType { + /** + * Triggered when a player right-clicks on the NPC. + */ + RIGHT_CLICKED, + + /** + * Triggered when a player shift-right-clicks on the NPC. + */ + SHIFT_RIGHT_CLICKED, + + /** + * Triggered when a player left-clicks on the NPC. + */ + LEFT_CLICKED, + + /** + * Triggered when a player shift-left-clicks on the NPC. + */ + SHIFT_LEFT_CLICKED, + + /** + * Triggered when a player damages the NPC. + */ + DAMAGED, + + /** + * Triggered when the NPC reaches a patrol point. + */ + PATROL_POINT_REACHED, + + /** + * Triggered when the NPC completes a full patrol cycle. + */ + PATROL_CYCLE_COMPLETE, + + /** + * Triggered when a player is sneaking within range of the NPC. + */ + PLAYER_SNEAKING_NEARBY, + + /** + * Triggered when a player is punching within range of the NPC. + */ + PLAYER_PUNCHING_NEARBY +} + +/** + * Data class representing an NPC event. + * + * @param npc The NPC that triggered the event. + * @param player The player involved in the event (null for non-player events). + * @param eventType The type of event that occurred. + * @param data Additional event-specific data. + */ +data class NPCEvent( + val npc: NPC, + val player: Player?, + val eventType: NPCEventType, + val data: Map = emptyMap() +) + diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPCEventListener.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCEventListener.kt new file mode 100644 index 0000000..942d0f6 --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCEventListener.kt @@ -0,0 +1,256 @@ +package cc.modlabs.kpaper.npc + +import cc.modlabs.kpaper.event.listen +import cc.modlabs.kpaper.extensions.timer +import org.bukkit.entity.Player +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.player.PlayerAnimationEvent +import org.bukkit.event.player.PlayerInteractEntityEvent +import org.bukkit.scheduler.BukkitTask +import kotlin.math.atan2 +import kotlin.math.sqrt + +/** + * Global event listener for NPC events. + * Listens to Bukkit events and triggers NPC-specific events. + */ +object NPCEventListener { + private var isRegistered = false + private val npcMap = mutableMapOf() + private val proximityNPCs = mutableSetOf() + private var proximityTask: BukkitTask? = null + private val playerPunchingState = mutableMapOf() // Player -> last punch time + private val playerSneakingState = mutableMapOf() // Player -> is sneaking + + /** + * Registers the global event listener. + * This is called automatically when an NPC registers its first event handler. + */ + fun register() { + if (isRegistered) return + isRegistered = true + + // Listen for player right-click on entities + listen { event -> + val entity = event.rightClicked + val npc = npcMap[entity] ?: return@listen + val player = event.player + val isSneaking = player.isSneaking + + // Determine event type based on shift state + val eventType = if (isSneaking) { + NPCEventType.SHIFT_RIGHT_CLICKED + } else { + NPCEventType.RIGHT_CLICKED + } + + val npcEvent = NPCEvent( + npc = npc, + player = player, + eventType = eventType, + data = mapOf( + "interactionHand" to event.hand.name, + "isSneaking" to isSneaking + ) + ) + (npc as? NPCImpl)?.triggerEvent(npcEvent) + } + + // Listen for entity damage (left-click or damage) + listen { event -> + val entity = event.entity + val npc = npcMap[entity] ?: return@listen + val damager = event.damager + + if (damager is Player) { + val isSneaking = damager.isSneaking + // Check if it's a left-click (attack) or actual damage + // Left-click typically has very low or zero damage + val isLeftClick = event.damage <= 1.0 && event.finalDamage <= 1.0 + + if (isLeftClick) { + // Trigger left-click event (shift or normal) + val eventType = if (isSneaking) { + NPCEventType.SHIFT_LEFT_CLICKED + } else { + NPCEventType.LEFT_CLICKED + } + + val leftClickEvent = NPCEvent( + npc = npc, + player = damager, + eventType = eventType, + data = mapOf( + "damage" to event.damage, + "isSneaking" to isSneaking + ) + ) + (npc as? NPCImpl)?.triggerEvent(leftClickEvent) + } else { + // Trigger damage event + val damageEvent = NPCEvent( + npc = npc, + player = damager, + eventType = NPCEventType.DAMAGED, + data = mapOf( + "damage" to event.damage, + "finalDamage" to event.finalDamage + ) + ) + (npc as? NPCImpl)?.triggerEvent(damageEvent) + } + } + } + + // Listen for player animations (punching) + listen { event -> + val player = event.player + if (event.animationType == org.bukkit.event.player.PlayerAnimationType.ARM_SWING) { + // Player is punching - track it + playerPunchingState[player] = System.currentTimeMillis() + } + } + + // Start proximity monitoring task + startProximityMonitoring() + } + + /** + * Starts the proximity monitoring task that checks for nearby players. + */ + private fun startProximityMonitoring() { + if (proximityTask != null) return + + proximityTask = timer(5, "NPCProximity") { // Check every 5 ticks + val currentTime = System.currentTimeMillis() + val proximityNPCsCopy = proximityNPCs.toList() // Copy to avoid concurrent modification + + proximityNPCsCopy.forEach { npc -> + val entity = npc.getEntity() ?: return@forEach + val npcLocation = entity.location + val range = npc.getProximityRange() + + // Get all nearby players + val nearbyPlayers = entity.world.getNearbyEntities(npcLocation, range, range, range) + .filterIsInstance() + .filter { it.location.distance(npcLocation) <= range } + + // Make NPC look at nearest player if enabled + // Only look at players if NPC is not currently walking (to avoid conflicts) + if (npc.isLookingAtPlayers() && nearbyPlayers.isNotEmpty() && entity is org.bukkit.entity.LivingEntity) { + // Check if NPC is walking + val isWalking = (npc as? NPCImpl)?.isCurrentlyWalking() ?: false + + // Only look at players if not walking + if (!isWalking) { + val nearestPlayer = nearbyPlayers.minByOrNull { it.location.distance(npcLocation) } + if (nearestPlayer != null) { + makeEntityLookAt(entity, nearestPlayer.location) + } + } + } + + // Process events for each nearby player + nearbyPlayers.forEach { player -> + val distance = player.location.distance(npcLocation) + + // Check for sneaking + val wasSneaking = playerSneakingState[player] ?: false + val isSneaking = player.isSneaking + playerSneakingState[player] = isSneaking + + if (isSneaking && !wasSneaking) { + // Player just started sneaking + val sneakingEvent = NPCEvent( + npc = npc, + player = player, + eventType = NPCEventType.PLAYER_SNEAKING_NEARBY, + data = mapOf( + "distance" to distance, + "location" to player.location + ) + ) + (npc as? NPCImpl)?.triggerEvent(sneakingEvent) + } + + // Check for punching (within last 500ms) + val lastPunchTime = playerPunchingState[player] ?: 0L + if (currentTime - lastPunchTime < 500) { + // Player is punching nearby + val punchingEvent = NPCEvent( + npc = npc, + player = player, + eventType = NPCEventType.PLAYER_PUNCHING_NEARBY, + data = mapOf( + "distance" to distance, + "location" to player.location + ) + ) + (npc as? NPCImpl)?.triggerEvent(punchingEvent) + // Remove punch state after triggering + playerPunchingState.remove(player) + } + } + } + + // Cleanup old punch states + playerPunchingState.entries.removeAll { currentTime - it.value > 1000 } + } + } + + /** + * Makes an entity look at a target location. + */ + private fun makeEntityLookAt(entity: org.bukkit.entity.LivingEntity, target: org.bukkit.Location) { + val entityLoc = entity.location + val direction = target.toVector().subtract(entityLoc.toVector()) + + // Calculate yaw (horizontal rotation) + val yaw = Math.toDegrees(-atan2(direction.x, direction.z)).toFloat() + + // Calculate pitch (vertical rotation) + val horizontalDistance = sqrt(direction.x * direction.x + direction.z * direction.z) + val pitch = Math.toDegrees(-atan2(direction.y, horizontalDistance)).toFloat() + + // Apply rotation + val newLocation = entityLoc.clone() + newLocation.yaw = yaw + newLocation.pitch = pitch + + entity.teleport(newLocation) + } + + /** + * Registers an NPC entity for event tracking. + * Called automatically when an NPC is created. + */ + fun registerNPC(entity: org.bukkit.entity.Entity, npc: NPC) { + npcMap[entity] = npc + } + + /** + * Unregisters an NPC entity from event tracking. + * Called automatically when an NPC is removed. + */ + fun unregisterNPC(entity: org.bukkit.entity.Entity) { + val npc = npcMap.remove(entity) + if (npc != null) { + unregisterProximityNPC(npc) + } + } + + /** + * Registers an NPC for proximity event monitoring. + */ + fun registerProximityNPC(npc: NPC) { + proximityNPCs.add(npc) + } + + /** + * Unregisters an NPC from proximity event monitoring. + */ + fun unregisterProximityNPC(npc: NPC) { + proximityNPCs.remove(npc) + } +} + diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPCExamples.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCExamples.kt new file mode 100644 index 0000000..0ae65df --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCExamples.kt @@ -0,0 +1,486 @@ +package cc.modlabs.kpaper.npc + +import cc.modlabs.kpaper.inventory.ItemBuilder +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.MainHand + +/** + * Example usage of the NPC API. + * This file demonstrates various ways to create and use NPCs. + */ + +// ============================================================================ +// BASIC EXAMPLES +// ============================================================================ + +/** + * Example 1: Create a simple NPC with just a name + */ +fun createSimpleNPCExample(location: Location): NPC { + return createSimpleNPC(location, "&aGuard") +} + +/** + * Example 2: Create an NPC using the builder pattern + */ +fun BuilderPatternExample(location: Location): NPC { + return location.createNPC { + name("&6Shop Keeper") + description("&7Click to open shop!") + mainHand(MainHand.RIGHT) + immovable(true) + helmet(ItemStack(Material.DIAMOND_HELMET)) + chestplate(ItemStack(Material.DIAMOND_CHESTPLATE)) + } +} + +/** + * Example 3: Create an NPC with custom equipment using ItemBuilder + */ +fun customEquipmentExample(location: Location): NPC { + val customSword = ItemBuilder(Material.DIAMOND_SWORD) { + name("&6Legendary Blade") + lore("&7A powerful weapon") + }.build() + + return location.createNPC { + name("&c&lLegendary Warrior") + description("&7A powerful guardian") + itemInMainHand(customSword) + helmet(ItemStack(Material.DIAMOND_HELMET)) + chestplate(ItemStack(Material.DIAMOND_CHESTPLATE)) + leggings(ItemStack(Material.DIAMOND_LEGGINGS)) + boots(ItemStack(Material.DIAMOND_BOOTS)) + } +} + +/** + * Example 4: Create an NPC with custom skin parts + * Note: Skin parts configuration depends on the Paper API version. + * The DSL block receives a SkinParts.Mutable object that you can configure. + */ +fun SkinPartsExample(location: Location): NPC { + return location.createNPC { + name("Mysterious Figure") + skinParts { } + // Configure skin parts - the exact properties depend on your Paper version + // skinParts { + // // Configure visible parts here + // } + } +} + +// ============================================================================ +// WALKING EXAMPLES +// ============================================================================ + +/** + * Example 5: Create a walking NPC that moves to a location + */ +fun WalkingNPCExample(location: Location, targetLocation: Location): NPC { + val npc = location.createNPC { + name("&aWandering NPC") + description("&7I'm walking around!") + immovable(false) // Must be false for walking + } + + // Make the NPC walk to a target location + npc.walkTo(targetLocation) + + return npc +} + +/** + * Example 6: Create a patrolling guard that follows a looping path + */ +fun PatrollingGuardExample(spawnLocation: Location): NPC { + // Define patrol points in a square pattern + val patrolPoints = listOf( + spawnLocation.clone().add(10.0, 0.0, 0.0), + spawnLocation.clone().add(10.0, 0.0, 10.0), + spawnLocation.clone().add(0.0, 0.0, 10.0), + spawnLocation.clone() // Will loop back to first point + ) + + val npc = spawnLocation.createNPC { + name("&cGuard") + description("&7On patrol") + helmet(ItemStack(Material.IRON_HELMET)) + chestplate(ItemStack(Material.IRON_CHESTPLATE)) + leggings(ItemStack(Material.IRON_LEGGINGS)) + boots(ItemStack(Material.IRON_BOOTS)) + itemInMainHand(ItemStack(Material.IRON_SWORD)) + immovable(false) + } + + // Start patrolling - will loop continuously + npc.startPatrolling(patrolPoints) + + return npc +} + +/** + * Example 7: Control NPC walking (pause, resume, teleport) + */ +fun WalkingControlExample(npc: NPC, newLocation: Location) { + // Pause walking + npc.pauseWalking() + + // Resume walking + npc.resumeWalking() + + // Teleport instantly (without walking) + npc.teleport(newLocation) + + // Start walking to a new location + npc.walkTo(newLocation) +} + +/** + * Example 7b: Control NPC patrolling (start, pause, resume, stop) + */ +fun PatrollingControlExample(npc: NPC, patrolPoints: List) { + // Start patrolling a path (will loop continuously) + npc.startPatrolling(patrolPoints) + + // Pause patrolling (NPC stops but remains in patrol mode) + npc.pausePatrolling() + + // Resume patrolling (continues from where it paused) + npc.resumePatrolling() + + // Stop patrolling completely (exits patrol mode) + npc.stopPatrolling() +} + +// ============================================================================ +// ADVANCED EXAMPLES +// ============================================================================ + +/** + * Example 8: NPC that approaches a player + */ +class FriendlyNPC(private val spawnLocation: Location) { + private val npc: NPC + + init { + npc = spawnLocation.createNPC { + name("&aFriendly Guide") + description("&7I'll help you explore!") + immovable(false) + } + } + + fun approachPlayer(player: Player) { + val playerLocation = player.location + npc.walkTo(playerLocation) + } + + fun followPlayer(player: Player, distance: Double = 3.0) { + // Calculate a location near the player + val direction = player.location.direction + val followLocation = player.location.clone() + .subtract(direction.multiply(distance)) + + npc.walkTo(followLocation) + } + + fun getNPC(): NPC = npc +} + +/** + * Example 9: NPC Manager for handling multiple NPCs + */ +class NPCManager { + private val npcs = mutableMapOf() + + fun createNPC(id: String, location: Location, config: NPCBuilder.() -> Unit): NPC { + val npc = location.createNPC(config) + npcs[id] = npc + return npc + } + + fun getNPC(id: String): NPC? = npcs[id] + + fun removeNPC(id: String) { + npcs[id]?.getEntity()?.remove() + npcs.remove(id) + } + + fun getAllNPCs(): Collection = npcs.values + + fun makeAllWalkTo(location: Location) { + npcs.values.forEach { it.walkTo(location) } + } + + fun clearAll() { + npcs.values.forEach { it.getEntity()?.remove() } + npcs.clear() + } +} + +/** + * Example 10: Complete shop NPC setup with event handling + */ +class ShopNPC(private val location: Location) { + private val npc: NPC + + init { + npc = location.createNPC { + name("&6&lShop Keeper") + description("&7Right-click to browse items!") + helmet(ItemStack(Material.LEATHER_HELMET)) + chestplate(ItemStack(Material.LEATHER_CHESTPLATE)) + immovable(true) + mainHand(MainHand.RIGHT) + } + + // Make NPC look at nearby players + npc.setLookAtPlayers(true) + npc.setProximityRange(8.0) // Look at players within 8 blocks + + // Register event handler for right-click + npc.onEvent(NPCEventType.RIGHT_CLICKED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&aOpening shop...") + // Open shop GUI here + } + } + + fun getNPC(): NPC = npc + + fun getLocation(): Location = location +} + +/** + * Example 11: Warrior NPC factory function + */ +fun createWarriorNPC(location: Location, name: String): NPC { + val sword = ItemBuilder(Material.DIAMOND_SWORD) { + name("&6Warrior's Blade") + lore("&7A weapon of legend") + }.build() + + val helmet = ItemBuilder(Material.DIAMOND_HELMET) { + name("&bHero's Helmet") + }.build() + + return location.createNPC { + this.name(name) + description("&7A brave warrior") + itemInMainHand(sword) + helmet(helmet) + chestplate(ItemStack(Material.DIAMOND_CHESTPLATE)) + leggings(ItemStack(Material.DIAMOND_LEGGINGS)) + boots(ItemStack(Material.DIAMOND_BOOTS)) + mainHand(MainHand.RIGHT) + immovable(true) + } +} + +/** + * Example 12: Convert existing Mannequin to NPC + */ +fun convertMannequinExample(mannequin: org.bukkit.entity.Mannequin): NPC { + // If you already have a Mannequin entity, convert it to an NPC + return mannequin.toNPC() +} + +/** + * Example 13: NPC with profile (player skin) + */ +fun NPCWithProfileExample( + location: Location, + profile: MannequinProfile, + name: String +): NPC { + return createNPCWithProfile(location, profile, name) +} + +/** + * Example 14: Dynamic NPC that responds to events + */ +class DynamicNPC(private val spawnLocation: Location) { + private val npc: NPC + private var currentTarget: Location? = null + + init { + npc = spawnLocation.createNPC { + name("&eDynamic NPC") + description("&7I move based on events!") + immovable(false) + } + } + + fun onPlayerNearby(player: Player, radius: Double) { + val distance = npc.getEntity()?.location?.distance(player.location) ?: return + if (distance <= radius && currentTarget == null) { + currentTarget = player.location.clone() + npc.walkTo(currentTarget!!) + } + } + + fun onPlayerLeft(player: Player) { + // Return to spawn if player left + if (currentTarget?.distance(player.location) ?: 0.0 > 10.0) { + currentTarget = null + npc.walkTo(spawnLocation) + } + } + + fun getNPC(): NPC = npc +} + +/** + * Example 15: NPC with multiple event handlers including shift-clicks and proximity + */ +fun NPCWithEventsExample(location: Location): NPC { + val npc = location.createNPC { + name("&eInteractive NPC") + description("&7Try interacting with me!") + immovable(true) + } + + // Handle right-click + npc.onEvent(NPCEventType.RIGHT_CLICKED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&aYou right-clicked me!") + } + + // Handle shift-right-click + npc.onEvent(NPCEventType.SHIFT_RIGHT_CLICKED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&eYou shift-right-clicked me!") + } + + // Handle left-click + npc.onEvent(NPCEventType.LEFT_CLICKED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&cYou left-clicked me!") + } + + // Handle shift-left-click + npc.onEvent(NPCEventType.SHIFT_LEFT_CLICKED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&6You shift-left-clicked me!") + } + + // Handle damage + npc.onEvent(NPCEventType.DAMAGED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&cOuch! That hurts!") + // Prevent damage if needed + } + + // Set proximity range (default is 5.0 blocks) + npc.setProximityRange(10.0) + + // Enable NPC to look at nearby players + npc.setLookAtPlayers(true) + + // Handle player sneaking nearby + npc.onEvent(NPCEventType.PLAYER_SNEAKING_NEARBY) { event -> + val player = event.player ?: return@onEvent + val distance = event.data["distance"] as? Double ?: 0.0 + player.sendMessage("&7NPC: I see you sneaking! (${String.format("%.1f", distance)} blocks away)") + } + + // Handle player punching nearby + npc.onEvent(NPCEventType.PLAYER_PUNCHING_NEARBY) { event -> + val player = event.player ?: return@onEvent + val distance = event.data["distance"] as? Double ?: 0.0 + player.sendMessage("&cNPC: Stop punching! (${String.format("%.1f", distance)} blocks away)") + } + + // Handle patrol events + npc.onEvent(NPCEventType.PATROL_POINT_REACHED) { event -> + val location = event.data["location"] as? Location + // Do something when patrol point is reached + } + + npc.onEvent(NPCEventType.PATROL_CYCLE_COMPLETE) { event -> + // Do something when patrol cycle completes + } + + return npc +} + +/** + * Example 16: NPC that looks at players (watchful guard) + */ +fun WatchfulGuardExample(location: Location): NPC { + val npc = location.createNPC { + name("&cWatchful Guard") + description("&7I'm watching you...") + helmet(ItemStack(Material.IRON_HELMET)) + chestplate(ItemStack(Material.IRON_CHESTPLATE)) + immovable(true) + } + + // Enable looking at players within 15 blocks + npc.setLookAtPlayers(true) + npc.setProximityRange(15.0) + + // Optional: Handle when player enters range + npc.onEvent(NPCEventType.PLAYER_SNEAKING_NEARBY) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&cGuard: I'm watching you!") + } + + return npc +} + +/** + * Example 17: Complete usage in a plugin class + */ +/* +class MyPlugin : KPlugin() { + private lateinit var npcManager: NPCManager + private val shopNPCs = mutableListOf() + private val guards = mutableListOf() + + override fun startup() { + npcManager = NPCManager() + + // Create shop NPCs + val shopLocation1 = world.getLocation(100.0, 64.0, 200.0) + val shop1 = ShopNPC(shopLocation1) + shopNPCs.add(shop1) + + // Create patrolling guards + val guardSpawn = world.getLocation(50.0, 64.0, 50.0) + val guard = example6_PatrollingGuard(guardSpawn) + guards.add(guard) + + // Add event handler to guard + guard.onEvent(NPCEventType.RIGHT_CLICKED) { event -> + val player = event.player ?: return@onEvent + player.sendMessage("&cGuard: &7Stay back!") + } + + // Make guard look at nearby players + guard.setLookAtPlayers(true) + guard.setProximityRange(10.0) + + // Create a friendly NPC + val friendlyLocation = world.getLocation(0.0, 64.0, 0.0) + val friendly = FriendlyNPC(friendlyLocation) + } + + override fun shutdown() { + // Clean up NPCs + npcManager.clearAll() + guards.forEach { + it.removeAllEventHandlers() + it.getEntity()?.remove() + } + shopNPCs.forEach { + it.getNPC().removeAllEventHandlers() + it.getNPC().getEntity()?.remove() + } + } +} +*/ diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPCExtensions.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCExtensions.kt new file mode 100644 index 0000000..09c8c4a --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCExtensions.kt @@ -0,0 +1,69 @@ +package cc.modlabs.kpaper.npc + +import org.bukkit.Location +import org.bukkit.entity.Mannequin + +/** + * Extension function to create a [NPC] from a [Location]. + * + * @param block The configuration block for the mannequin. + * @return The created MannequinNPC. + * + * @example + * ```kotlin + * val npc = location.createMannequinNPC { + * name("Shop Keeper") + * description("&7Click to open shop!") + * profile(playerProfile) + * immovable(true) + * helmet(ItemStack(Material.DIAMOND_HELMET)) + * } + * ``` + */ +fun Location.createNPC(block: NPCBuilder.() -> Unit = {}): NPC { + return NPCBuilder(this).apply(block).build() +} + +/** + * Creates a simple [NPC] with just a name and location. + * + * @param location The location where the mannequin should be spawned. + * @param name The name of the mannequin. + * @return The created MannequinNPC. + */ +fun createSimpleNPC(location: Location, name: String): NPC { + return NPCBuilder(location) + .name(name) + .build() +} + +/** + * Creates a [NPC] with a player profile (skin). + * + * @param location The location where the mannequin should be spawned. + * @param profile The player profile to use for the mannequin's appearance. + * @param name The name of the mannequin (optional). + * @return The created MannequinNPC. + */ +fun createNPCWithProfile( + location: Location, + profile: MannequinProfile, + name: String? = null +): NPC { + return NPCBuilder(location).apply { + this.profile(profile) + name?.let { this.name(it) } + }.build() +} + +/** + * Converts an existing [Mannequin] entity to a [NPC]. + * Useful when you already have a mannequin entity and want to use the NPC API. + * + * @param Mannequin The existing Mannequin entity. + * @return A MannequinNPC wrapping the entity. + */ +fun Mannequin.toNPC(): NPC { + return NPCImpl(this) +} + diff --git a/src/main/kotlin/cc/modlabs/kpaper/npc/NPCImpl.kt b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCImpl.kt new file mode 100644 index 0000000..100958a --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/npc/NPCImpl.kt @@ -0,0 +1,494 @@ +package cc.modlabs.kpaper.npc + +import cc.modlabs.kpaper.extensions.timer +import dev.fruxz.stacked.text +import net.kyori.adventure.text.Component +import org.bukkit.Location +import org.bukkit.entity.Entity +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Mannequin +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.MainHand +import org.bukkit.scheduler.BukkitTask + +/** + * Implementation of [NPC] that wraps a [Mannequin] entity. + * Provides a convenient API for managing mannequin-based NPCs with walking capabilities. + * + * @property mannequin The underlying Mannequin entity. + */ +class NPCImpl( + private val mannequin: Mannequin +) : NPC { + + private var currentTarget: Location? = null + private val pathQueue = mutableListOf() + private var walkingTask: BukkitTask? = null + private var isPaused = false + private var isWalking = false + private val walkSpeed = 0.25 // Blocks per tick + private val arrivalThreshold = 1.5 // Distance to consider "arrived" + + // Patrolling state + private var isPatrolling = false + private var isPatrolPaused = false + private val patrolPath = mutableListOf() + + init { + // Enable AI for the mannequin so it can move + // Mannequin extends LivingEntity, so we can directly enable AI + mannequin.setAI(true) + + // Register this NPC for event tracking + NPCEventListener.registerNPC(mannequin, this) + } + + override fun getMannequin(): Mannequin? = if (mannequin.isValid) mannequin else null + + override fun getEntity(): Entity? = getMannequin() + + override fun walkTo(location: Location): Boolean { + val entity = getMannequin() as? LivingEntity ?: return false + + // Clear existing path and set new target + pathQueue.clear() + currentTarget = location.clone() + isPaused = false + + // Start walking if not already walking + if (!isWalking) { + startWalking() + } + + return true + } + + override fun walkPath(locations: List): Boolean { + if (locations.isEmpty()) return false + val entity = getMannequin() as? LivingEntity ?: return false + + // Clear existing path and queue new locations + pathQueue.clear() + pathQueue.addAll(locations.map { it.clone() }) + isPaused = false + + // Set first location as target + if (pathQueue.isNotEmpty()) { + currentTarget = pathQueue.removeAt(0) + } + + // Start walking if not already walking + if (!isWalking) { + startWalking() + } + + return true + } + + override fun pauseWalking(): Boolean { + if (!isWalking) return false + isPaused = true + return true + } + + override fun resumeWalking(): Boolean { + if (!isWalking) return false + isPaused = false + return true + } + + override fun startWalking(): Boolean { + val entity = getMannequin() as? LivingEntity ?: return false + + // Enable AI if not already enabled + entity.setAI(true) + + // If already walking, don't start again + if (isWalking && walkingTask != null) { + return true + } + + isWalking = true + isPaused = false + + // Start the walking task + walkingTask = timer(1, "NPCWalking") { + // Check pause state - use patrol pause if patrolling, otherwise regular pause + val shouldPause = if (isPatrolling) isPatrolPaused else isPaused + if (!isWalking || shouldPause) return@timer + + val currentEntity = getMannequin() as? LivingEntity ?: run { + stopWalking() + return@timer + } + + val target = currentTarget ?: run { + // No current target, check if there's a next location in the path + if (pathQueue.isEmpty()) { + stopWalking() + return@timer + } + val nextLocation: Location = pathQueue.removeAt(0) + currentTarget = nextLocation + nextLocation + } + + val currentLoc = currentEntity.location + val distance = currentLoc.distance(target) + + // Check if we've arrived at the target + if (distance <= arrivalThreshold) { + // Arrived at current target + val reachedLocation = currentTarget + currentTarget = null + + // Trigger patrol point reached event if patrolling + if (isPatrolling && reachedLocation != null) { + val npcEvent = NPCEvent( + npc = this@NPCImpl, + player = null, + eventType = NPCEventType.PATROL_POINT_REACHED, + data = mapOf("location" to reachedLocation) + ) + triggerEvent(npcEvent) + } + + // Check if there's a next location in the path + if (pathQueue.isEmpty()) { + // Finished the path + if (isPatrolling) { + // If patrolling, loop back to first location + if (patrolPath.isNotEmpty()) { + // Trigger patrol cycle complete event + val cycleEvent = NPCEvent( + npc = this@NPCImpl, + player = null, + eventType = NPCEventType.PATROL_CYCLE_COMPLETE, + data = emptyMap() + ) + triggerEvent(cycleEvent) + + pathQueue.addAll(patrolPath.map { it.clone() }) + val nextLocation: Location = pathQueue.removeAt(0) + currentTarget = nextLocation + } else { + stopWalking() + return@timer + } + } else { + // Regular walking - stop when path is done + stopWalking() + return@timer + } + } else { + val nextLocation: Location = pathQueue.removeAt(0) + currentTarget = nextLocation + } + } else { + // Move towards target + moveTowards(currentEntity, target, walkSpeed) + } + } + + return true + } + + private fun stopWalking() { + isWalking = false + isPaused = false + isPatrolling = false + isPatrolPaused = false + currentTarget = null + pathQueue.clear() + patrolPath.clear() + walkingTask?.cancel() + walkingTask = null + } + + /** + * Cleanup method called when NPC is removed. + */ + fun cleanup() { + val entity = getMannequin() + if (entity != null) { + NPCEventListener.unregisterNPC(entity) + } + stopWalking() + removeAllEventHandlers() + } + + override fun startPatrolling(locations: List): Boolean { + if (locations.isEmpty()) return false + val entity = getMannequin() as? LivingEntity ?: return false + + // Stop any existing walking/patrolling + stopWalking() + + // Set up patrol path + patrolPath.clear() + patrolPath.addAll(locations.map { it.clone() }) + pathQueue.clear() + pathQueue.addAll(patrolPath.map { it.clone() }) + + // Set first location as target + if (pathQueue.isNotEmpty()) { + currentTarget = pathQueue.removeAt(0) + } + + // Enable patrolling mode + isPatrolling = true + isPatrolPaused = false + + // Start walking + startWalking() + + return true + } + + override fun pausePatrolling(): Boolean { + if (!isPatrolling) return false + isPatrolPaused = true + return true + } + + override fun resumePatrolling(): Boolean { + if (!isPatrolling) return false + isPatrolPaused = false + return true + } + + override fun stopPatrolling(): Boolean { + if (!isPatrolling) return false + + // Stop patrolling but keep walking task if there's a current target + isPatrolling = false + isPatrolPaused = false + patrolPath.clear() + + // If no current target and no path queue, stop walking completely + if (currentTarget == null && pathQueue.isEmpty()) { + stopWalking() + } + + return true + } + + private fun moveTowards(entity: LivingEntity, target: Location, speed: Double) { + val currentLoc = entity.location + val direction = target.toVector().subtract(currentLoc.toVector()).normalize() + + // Calculate the movement vector + val movement = direction.multiply(speed) + + // Get the new location + val newLoc = currentLoc.clone().add(movement) + + // Make entity look at target + val lookDirection = target.toVector().subtract(currentLoc.toVector()) + val yaw = Math.toDegrees(-Math.atan2(lookDirection.x, lookDirection.z)).toFloat() + newLoc.yaw = yaw + newLoc.pitch = 0f + + // Apply movement + entity.velocity = movement + entity.teleport(newLoc) + } + + override fun teleport(location: Location): Boolean { + val entity = getMannequin() ?: return false + return try { + entity.teleport(location) + true + } catch (e: Exception) { + false + } + } + + override fun changeName(name: String) { + val entity = getMannequin() ?: return + entity.customName(text(name)) + } + + override fun getProfile(): MannequinProfile { + return mannequin.profile + } + + override fun setProfile(profile: MannequinProfile) { + val entity = getMannequin() ?: return + entity.profile = profile + } + + override fun getSkinParts(): MannequinSkinParts { + return mannequin.skinParts + } + + override fun setSkinParts(parts: MannequinSkinParts) { + val entity = getMannequin() ?: return + // The parts parameter should be the entity's own skinParts that was modified + // Since skinParts returns a Mutable that can't be reassigned, we assume + // the caller has already modified the mannequin's skinParts directly + // This method exists for API consistency + } + + override fun getDescription(): Component? { + return mannequin.description + } + + override fun setDescription(description: Component?) { + val entity = getMannequin() ?: return + entity.description = description + } + + override fun getMainHand(): MainHand { + return mannequin.mainHand + } + + override fun setMainHand(hand: MainHand) { + val entity = getMannequin() ?: return + entity.mainHand = hand + } + + override fun isImmovable(): Boolean { + return mannequin.isImmovable + } + + override fun setImmovable(immovable: Boolean) { + val entity = getMannequin() ?: return + entity.isImmovable = immovable + } + + override fun getEquipment(): org.bukkit.inventory.EntityEquipment { + return mannequin.equipment + } + + override fun setEquipment(slot: EquipmentSlot, item: ItemStack?) { + val entity = getMannequin() ?: return + when (slot) { + EquipmentSlot.HAND -> entity.equipment.setItemInMainHand(item) + EquipmentSlot.OFF_HAND -> entity.equipment.setItemInOffHand(item) + EquipmentSlot.HEAD -> entity.equipment.helmet = item + EquipmentSlot.CHEST -> entity.equipment.chestplate = item + EquipmentSlot.LEGS -> entity.equipment.leggings = item + EquipmentSlot.FEET -> entity.equipment.boots = item + EquipmentSlot.BODY, EquipmentSlot.SADDLE -> { + // These slots don't apply to mannequins + } + } + } + + override fun setItemInMainHand(item: ItemStack?) { + val entity = getMannequin() ?: return + entity.equipment.setItemInMainHand(item) + } + + override fun setItemInOffHand(item: ItemStack?) { + val entity = getMannequin() ?: return + entity.equipment.setItemInOffHand(item) + } + + override fun setHelmet(item: ItemStack?) { + val entity = getMannequin() ?: return + entity.equipment.helmet = item + } + + override fun setChestplate(item: ItemStack?) { + val entity = getMannequin() ?: return + entity.equipment.chestplate = item + } + + override fun setLeggings(item: ItemStack?) { + val entity = getMannequin() ?: return + entity.equipment.leggings = item + } + + override fun setBoots(item: ItemStack?) { + val entity = getMannequin() ?: return + entity.equipment.boots = item + } + + // Event handling + private val eventHandlers = mutableMapOf Unit>>() + private var proximityRange = 5.0 // Default range in blocks + private var lookAtPlayers = false // Whether to look at nearby players + + override fun onEvent(eventType: NPCEventType, handler: (NPCEvent) -> Unit) { + eventHandlers.getOrPut(eventType) { mutableListOf() }.add(handler) + // Register global listener if not already registered + NPCEventListener.register() + + // Register for proximity monitoring if needed + if (eventType == NPCEventType.PLAYER_SNEAKING_NEARBY || + eventType == NPCEventType.PLAYER_PUNCHING_NEARBY || + lookAtPlayers) { + NPCEventListener.registerProximityNPC(this) + } + } + + override fun removeEventHandlers(eventType: NPCEventType) { + eventHandlers.remove(eventType) + + // Unregister from proximity monitoring if no proximity handlers remain and not looking at players + if (eventType == NPCEventType.PLAYER_SNEAKING_NEARBY || + eventType == NPCEventType.PLAYER_PUNCHING_NEARBY) { + val hasProximityHandlers = eventHandlers.containsKey(NPCEventType.PLAYER_SNEAKING_NEARBY) || + eventHandlers.containsKey(NPCEventType.PLAYER_PUNCHING_NEARBY) + if (!hasProximityHandlers && !lookAtPlayers) { + NPCEventListener.unregisterProximityNPC(this) + } + } + } + + override fun removeAllEventHandlers() { + eventHandlers.clear() + NPCEventListener.unregisterProximityNPC(this) + } + + override fun setProximityRange(range: Double) { + proximityRange = range.coerceAtLeast(0.0) + } + + override fun getProximityRange(): Double { + return proximityRange + } + + override fun setLookAtPlayers(enabled: Boolean) { + lookAtPlayers = enabled + if (enabled) { + NPCEventListener.registerProximityNPC(this) + } else { + // Only unregister if no proximity event handlers are registered + val hasProximityHandlers = eventHandlers.containsKey(NPCEventType.PLAYER_SNEAKING_NEARBY) || + eventHandlers.containsKey(NPCEventType.PLAYER_PUNCHING_NEARBY) + if (!hasProximityHandlers) { + NPCEventListener.unregisterProximityNPC(this) + } + } + } + + override fun isLookingAtPlayers(): Boolean { + return lookAtPlayers + } + + /** + * Internal method to trigger an event for this NPC. + */ + internal fun triggerEvent(event: NPCEvent) { + eventHandlers[event.eventType]?.forEach { handler -> + try { + handler(event) + } catch (e: Exception) { + // Log error but don't crash + e.printStackTrace() + } + } + } + + /** + * Internal method to check if NPC is currently walking. + */ + internal fun isCurrentlyWalking(): Boolean { + return isWalking && !isPaused + } +} +