From d4e84fea1b000ecfa872e310bf14ddba61bddc9c Mon Sep 17 00:00:00 2001 From: Kariya Date: Wed, 10 Dec 2025 12:42:53 +0000 Subject: [PATCH] feat: Add `onToggleSneak` event to `CustomComponent` and refactor `GliderComponent` activation to use it with a global ticker. --- .../items/api/component/CustomComponent.kt | 1 + .../content/components/GliderComponent.kt | 369 ++++++++++-------- .../hcu/items/listeners/EventListener.kt | 20 + 3 files changed, 217 insertions(+), 173 deletions(-) diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt index a8e5ebe..4f1c470 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt @@ -17,4 +17,5 @@ interface CustomComponent { fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) {} fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {} fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {} + fun onToggleSneak(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.player.PlayerToggleSneakEvent) {} } diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt index 0745992..241c011 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/GliderComponent.kt @@ -13,17 +13,16 @@ import org.bukkit.block.data.Lightable import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.Damageable -import org.bukkit.util.Vector import org.bukkit.persistence.PersistentDataType +import org.bukkit.util.Vector import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.sqrt import kotlin.math.pow -class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component"), EquippableComponent { +class GliderComponent(private val plugin: App) : AbstractComponent(plugin, "glider_component"), EquippableComponent { override val displayName: String = "Glider" - // Custom maxTier for Glider override val maxTier: Int = 10 companion object { @@ -39,138 +38,134 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component ) } - // Dynamic Tier attributes - private fun getMaxDurability(tier: Tier): Int = (64 * 2.0.pow(tier.level - 1)).toInt() - - private fun getFallSpeed(tier: Tier): Double { - // -0.08 base, getting closer to 0 by 0.015~ per level, capped at -0.01 - // Simulating the map: 1->-0.08, 2->-0.065, 3->-0.05, 4->-0.04, 5->-0.03 - return when (tier.level) { - 1 -> -0.08 - 2 -> -0.065 - 3 -> -0.05 - 4 -> -0.04 - 5 -> -0.03 - else -> (-0.03 + (tier.level - 5) * 0.005).coerceAtMost(-0.005) - } - } - - private fun getHungerInterval(tier: Tier): Int = 40 + (tier.level - 1) * 20 // 1->40, 5->120... close approximation or simplier formula - data class GliderState( var ticksGliding: Int = 0, var lastTickTime: Long = System.currentTimeMillis() ) - override fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) { - val player = event.player - val item = event.item ?: return - - // Only handle Right Click - if (event.action != org.bukkit.event.block.Action.RIGHT_CLICK_AIR && event.action != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK) return - - val tier = getTier(item) ?: Tier.ONE - - if (!canDeploy(player)) { - player.sendActionBar(Component.text("✗ ", NamedTextColor.RED).append(Component.text("空中にいる必要があります!", NamedTextColor.YELLOW))) - player.playSound(player.location, Sound.ENTITY_VILLAGER_NO, 0.5f, 1.0f) - return - } - - if (isBroken(item)) { - player.sendActionBar(Component.text("⚠ ", NamedTextColor.RED).append(Component.text("グライダーが壊れています!", NamedTextColor.YELLOW))) - player.playSound(player.location, Sound.ENTITY_ITEM_BREAK, 0.5f, 0.8f) - return - } - - val isEnabled = isGliderEnabled(item) - - if (isEnabled) { - disableGlider(player, item) - player.sendActionBar(Component.text("⬇ ", NamedTextColor.GRAY).append(Component.text("グライダー収納", NamedTextColor.YELLOW))) - player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 0.8f, 0.8f) - } else { - enableGlider(player, item) - player.sendActionBar(Component.text("⬆ ", NamedTextColor.GREEN).append(Component.text("グライダー展開!", NamedTextColor.AQUA)).append(Component.text(" [Tier ${tier.level}]", tier.color))) - player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 1.0f, 1.2f) - } - // event.isCancelled = true // Maybe? GliderItem did it. - } - - override fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) { - val player = event.player - val item = player.inventory.itemInMainHand - - // Safety check if method called correctly - if (!has(item)) return - if (!isGliderEnabled(item)) return - - if (isFlyingBlocked(player)) { - disableGlider(player, item, silent = true) - return - } - } - - override fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) { - if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) { - val entity = event.entity - if (entity is Player && activeGliders.containsKey(entity.uniqueId)) { - event.isCancelled = true - } - } - } - - override fun onTick(player: Player, item: ItemStack) { - if (activeGliders.containsKey(player.uniqueId)) { - val state = activeGliders[player.uniqueId]!! - // Try to get Tier from Component, fallback to Tier.ONE - val tier = getTier(item) ?: Tier.ONE - - if (isFlyingBlocked(player)) { - disableGlider(player, item, silent = true) - return - } - - state.ticksGliding++ - player.fallDistance = 0f - - if (checkAndApplyUpdraft(player)) { - spawnUpdraftParticles(player) - return - } - - applyGlidingPhysics(player, tier) - - if (state.ticksGliding % 5 == 0) spawnGlidingParticles(player) - if (state.ticksGliding % 10 == 0) updateGliderActionBar(player, item, tier, state) + init { + // Global Ticker for active gliders + plugin.server.scheduler.runTaskTimer(plugin, Runnable { + if (activeGliders.isEmpty()) return@Runnable - val hungerInterval = getHungerInterval(tier) - if (state.ticksGliding % hungerInterval == 0) consumeHunger(player) + // Validate and Tick + val iterator = activeGliders.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val uuid = entry.key + val state = entry.value + val player = plugin.server.getPlayer(uuid) - if (state.ticksGliding % DURABILITY_TICK_INTERVAL == 0) consumeDurability(player, item, tier) - } else { - // Activation Logic for Equippable (Sneak + Jump/Drop) - if (player.isSneaking && canDeploy(player)) { - enableGlider(player, item) + if (player == null) { + iterator.remove() + continue + } + + val item = findGliderItem(player) + + // Validation: Must have item equipped, not be grounded/blocked + if (item == null) { + disableGliderMemoryOnly(player) // Removed without item reference + iterator.remove() // Remove from map + continue + } + + if (isFlyingBlocked(player)) { + disableGlider(player, item, silent = true) + continue + } + + // Physics and Logic + tickGliderPhysics(player, item, state) + } + }, 1L, 1L) + } + + override fun apply(item: ItemStack, tier: Tier?) { + if (isEquipment(item.type)) { + super.apply(item, tier) + } + } + + override fun onToggleSneak(player: Player, item: ItemStack, event: org.bukkit.event.player.PlayerToggleSneakEvent) { + // Only trigger when STARTING to sneak + if (event.isSneaking) { + if (activeGliders.containsKey(player.uniqueId)) { + // If already gliding, stow it + disableGlider(player, item) + } else { + // If not gliding, verify conditions to deploy + if (canDeploy(player) && !isBroken(item)) { + enableGlider(player, item) + } else if (isBroken(item)) { + player.sendActionBar(Component.text("⚠ ", NamedTextColor.RED).append(Component.text("グライダーが壊れています!", NamedTextColor.YELLOW))) + player.playSound(player.location, Sound.ENTITY_ITEM_BREAK, 0.5f, 0.8f) + } } } } - private fun enableGlider(player: Player, item: ItemStack, silent: Boolean = false) { + // Logic handled in global ticker now, but required by interface + override fun onTick(player: Player, item: ItemStack) {} + + // Only allow attaching to equipment types + private fun isEquipment(material: Material): Boolean { + val name = material.name + return name.endsWith("_HELMET") || name.endsWith("_CHESTPLATE") || + name.endsWith("_LEGGINGS") || name.endsWith("_BOOTS") || + material == Material.ELYTRA || material == Material.SHIELD || + material == Material.CARVED_PUMPKIN || material == Material.TURTLE_HELMET + } + + // Find the itemStack that has this component in user's equipment + private fun findGliderItem(player: Player): ItemStack? { + val equipment = player.equipment ?: return null + val candidates = mutableListOf() + candidates.addAll(equipment.armorContents.filterNotNull()) + candidates.add(equipment.itemInMainHand) + candidates.add(equipment.itemInOffHand) + + return candidates.find { has(it) && isGliderEnabled(it) } + } + + private fun tickGliderPhysics(player: Player, item: ItemStack, state: GliderState) { + val tier = getTier(item) ?: Tier.ONE + + state.ticksGliding++ + player.fallDistance = 0f + + // Updraft + if (checkAndApplyUpdraft(player)) { + spawnUpdraftParticles(player) + return + } + + // Physics + applyGlidingPhysics(player, tier) + + // Effects + if (state.ticksGliding % 5 == 0) spawnGlidingParticles(player) + if (state.ticksGliding % 10 == 0) updateGliderActionBar(player, item, tier) + + // Hunger + val hungerInterval = getHungerInterval(tier) + if (state.ticksGliding % hungerInterval == 0) consumeHunger(player) + + // Durability + if (state.ticksGliding % DURABILITY_TICK_INTERVAL == 0) consumeDurability(player, item) + } + + private fun enableGlider(player: Player, item: ItemStack) { val meta = item.itemMeta ?: return meta.persistentDataContainer.set(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN, true) item.itemMeta = meta - // Memory update if (!activeGliders.containsKey(player.uniqueId)) { val tier = getTier(item) ?: Tier.ONE activeGliders[player.uniqueId] = GliderState() - // Visuals - if (!silent) { - player.sendActionBar(Component.text("⬆ ", NamedTextColor.GREEN).append(Component.text("グライダー展開!", NamedTextColor.AQUA)).append(Component.text(" [Tier ${tier.level}]", tier.color))) - player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 1.0f, 1.2f) - } + player.sendActionBar(Component.text("⬆ ", NamedTextColor.GREEN).append(Component.text("グライダー展開!", NamedTextColor.AQUA)).append(Component.text(" [Tier ${tier.level}]", tier.color))) + player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 1.0f, 1.2f) } } @@ -179,11 +174,9 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component meta.persistentDataContainer.set(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN, false) item.itemMeta = meta - // Memory update if (activeGliders.containsKey(player.uniqueId)) { activeGliders.remove(player.uniqueId) - // Visuals if (!silent) { player.sendActionBar(Component.text("⬇ ", NamedTextColor.GRAY).append(Component.text("グライダー収納", NamedTextColor.YELLOW))) player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 0.8f, 0.8f) @@ -191,13 +184,14 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component } } - // Check if item has 'enabled' state in PDC - private fun isGliderEnabled(item: ItemStack): Boolean { - if (item.type.isAir) return false - val meta = item.itemMeta ?: return false - return meta.persistentDataContainer.get(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN) ?: false + private fun disableGliderMemoryOnly(player: Player) { + if (activeGliders.containsKey(player.uniqueId)) { + player.sendActionBar(Component.text("⬇ ", NamedTextColor.GRAY).append(Component.text("グライダー収納 (装備解除)", NamedTextColor.YELLOW))) + player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 0.8f, 0.8f) + // Removal from map happens in caller + } } - + private fun canDeploy(player: Player): Boolean { if (player.isInsideVehicle) return false val loc = player.location @@ -206,41 +200,17 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component @Suppress("DEPRECATION") val isInAir = !player.isOnGround && belowOne.type.isAir && belowTwo.type.isAir - val isFalling = player.fallDistance > 1.5 + val isFalling = player.fallDistance > 1.0 return isInAir || isFalling } - private fun isBroken(item: ItemStack): Boolean { - // Simple check: is it broken based on our custom max durability? - // Actually for now false is safe default if we don't strictly track custom durability item destruction - // Users might want custom durability handling -> consuming durability updates the item damage. - // If item is fully damaged (vanilla), it might break. - return false - } - @Suppress("DEPRECATION") private fun isFlyingBlocked(player: Player): Boolean { return player.isOnGround || player.isInWater || player.isSwimming || (player.isGliding && !activeGliders.containsKey(player.uniqueId)) } - - private fun checkAndApplyUpdraft(player: Player): Boolean { - val location = player.location - for (y in 0 until 10) { - val checkLoc = location.clone().subtract(0.0, y.toDouble(), 0.0) - val block = checkLoc.block - if (UPDRAFT_BLOCKS.contains(block.type)) { - val blockData = block.blockData - if (blockData is Lightable && !blockData.isLit) continue - - player.velocity = Vector(player.velocity.x, UPDRAFT_BOOST, player.velocity.z) - player.playSound(player.location, Sound.BLOCK_FIRE_AMBIENT, 0.3f, 1.5f) - return true - } - } - return false - } - + + // Physics Helpers private fun applyGlidingPhysics(player: Player, tier: Tier) { val velocity = player.velocity val fallSpeed = getFallSpeed(tier) @@ -266,7 +236,6 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component horizontalDir.z * newHorizontalSpeed ) } - private fun spawnGlidingParticles(player: Player) { player.world.spawnParticle(Particle.CLOUD, player.location.add(0.0, 2.0, 0.0), 1, 0.3, 0.0, 0.3, 0.01) } @@ -275,16 +244,55 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component player.world.spawnParticle(Particle.FLAME, player.location.add(0.0, 1.0, 0.0), 5, 0.3, 0.5, 0.3, 0.02) } - private fun updateGliderActionBar(player: Player, item: ItemStack, tier: Tier, state: GliderState) { + private fun checkAndApplyUpdraft(player: Player): Boolean { + val location = player.location + for (y in 0 until 10) { + val checkLoc = location.clone().subtract(0.0, y.toDouble(), 0.0) + val block = checkLoc.block + if (UPDRAFT_BLOCKS.contains(block.type)) { + val blockData = block.blockData + if (blockData is Lightable && !blockData.isLit) continue + + player.velocity = Vector(player.velocity.x, UPDRAFT_BOOST, player.velocity.z) + player.playSound(player.location, Sound.BLOCK_FIRE_AMBIENT, 0.3f, 1.5f) + return true + } + } + return false + } + + private fun consumeHunger(player: Player) { + player.exhaustion += HUNGER_EXHAUSTION + } + + private fun consumeDurability(player: Player, item: ItemStack) { + val meta = item.itemMeta ?: return + if (meta is Damageable) { + val maxDurability = item.type.maxDurability + if (maxDurability <= 0) return + + val currentDamage = meta.damage + if (currentDamage >= maxDurability - 1) { + meta.damage = maxDurability.toInt() + disableGlider(player, item) + player.playSound(player.location, Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f) + } else { + meta.damage = currentDamage + 1 + } + item.itemMeta = meta + } + } + + private fun updateGliderActionBar(player: Player, item: ItemStack, tier: Tier) { val speed = player.velocity.length() * 20 val altitude = player.location.y val meta = item.itemMeta val durabilityText = if (meta is Damageable) { - val maxDurability = getMaxDurability(tier) + val maxDurability = item.type.maxDurability val currentDamage = meta.damage - val remaining = maxDurability - currentDamage - "$remaining/$maxDurability" + val remaining = (maxDurability - currentDamage).coerceAtLeast(0) + if (maxDurability > 0) "$remaining/$maxDurability" else "∞" } else { "∞" } @@ -299,23 +307,38 @@ class GliderComponent(plugin: App) : AbstractComponent(plugin, "glider_component ) } - private fun consumeHunger(player: Player) { - player.exhaustion += HUNGER_EXHAUSTION - } - - private fun consumeDurability(player: Player, item: ItemStack, tier: Tier) { - val meta = item.itemMeta ?: return - if (meta is Damageable) { - val maxDamage = getMaxDurability(tier) - val currentDamage = meta.damage - if (currentDamage >= maxDamage - 1) { - meta.damage = maxDamage - disableGlider(player, item) // Use item aware disable - player.playSound(player.location, Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f) - } else { - meta.damage = currentDamage + 1 - } - item.itemMeta = meta + private fun getFallSpeed(tier: Tier): Double { + return when (tier.level) { + 1 -> -0.08 + 2 -> -0.065 + 3 -> -0.05 + 4 -> -0.04 + 5 -> -0.03 + else -> (-0.03 + (tier.level - 5) * 0.005).coerceAtMost(-0.005) } } + + private fun getHungerInterval(tier: Tier): Int = 40 + (tier.level - 1) * 20 + + override fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) { + if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) { + val entity = event.entity + if (entity is Player && activeGliders.containsKey(entity.uniqueId)) { + event.isCancelled = true + } + } + } + + private fun isGliderEnabled(item: ItemStack): Boolean { + if (item.type.isAir) return false + val meta = item.itemMeta ?: return false + return meta.persistentDataContainer.get(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN) ?: false + } + + private fun isBroken(item: ItemStack): Boolean { + val meta = item.itemMeta as? Damageable ?: return false + val maxDurability = item.type.maxDurability + if (maxDurability <= 0) return false + return meta.damage >= maxDurability + } } diff --git a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt index 99108d1..25b607a 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -119,4 +119,24 @@ class EventListener(private val plugin: App) : Listener { } } } + + @EventHandler + fun onToggleSneak(event: org.bukkit.event.player.PlayerToggleSneakEvent) { + val player = event.player + val equipment = player.equipment ?: return + + val itemsToCheck = mutableListOf() + itemsToCheck.addAll(equipment.armorContents.filterNotNull()) + itemsToCheck.add(equipment.itemInMainHand) + itemsToCheck.add(equipment.itemInOffHand) + + for (item in itemsToCheck) { + if (item.type.isAir) continue + for (component in net.hareworks.hcu.items.registry.ComponentRegistry.getAll()) { + if (component.has(item)) { + component.onToggleSneak(player, item, event) + } + } + } + } }