From 7cc8ccc39566084b35d81fa7e0b6f17506635ec1 Mon Sep 17 00:00:00 2001 From: Kariya Date: Mon, 8 Dec 2025 12:51:39 +0000 Subject: [PATCH] feat: Introduce Glider item with its associated recipes, models, textures, and mechanics. --- sample/Gliders | 1 + .../kotlin/net/hareworks/hcu/items/App.kt | 2 + .../hareworks/hcu/items/domain/SpecialItem.kt | 2 + .../hcu/items/domain/impl/GliderItem.kt | 475 ++++++++++++++++++ .../hcu/items/domain/impl/GrapplingItem.kt | 2 +- .../hcu/items/listeners/EventListener.kt | 32 +- 6 files changed, 511 insertions(+), 3 deletions(-) create mode 160000 sample/Gliders create mode 100644 src/main/kotlin/net/hareworks/hcu/items/domain/impl/GliderItem.kt diff --git a/sample/Gliders b/sample/Gliders new file mode 160000 index 0000000..a69cba4 --- /dev/null +++ b/sample/Gliders @@ -0,0 +1 @@ +Subproject commit a69cba4406090a0f8a5883b463dc8083c9802630 diff --git a/src/main/kotlin/net/hareworks/hcu/items/App.kt b/src/main/kotlin/net/hareworks/hcu/items/App.kt index 894c1ae..94b62ce 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/App.kt @@ -7,6 +7,7 @@ import net.hareworks.permits_lib.domain.NodeRegistration import net.hareworks.hcu.items.domain.ItemRegistry import net.hareworks.hcu.items.domain.impl.TestItem import net.hareworks.hcu.items.domain.impl.GrapplingItem +import net.hareworks.hcu.items.domain.impl.GliderItem import org.bukkit.permissions.PermissionDefault import org.bukkit.plugin.java.JavaPlugin @@ -32,5 +33,6 @@ public class App : JavaPlugin() { // Register items ItemRegistry.register(TestItem()) ItemRegistry.register(GrapplingItem()) + ItemRegistry.register(GliderItem()) } } diff --git a/src/main/kotlin/net/hareworks/hcu/items/domain/SpecialItem.kt b/src/main/kotlin/net/hareworks/hcu/items/domain/SpecialItem.kt index f207068..8a0ed79 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/domain/SpecialItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/domain/SpecialItem.kt @@ -30,6 +30,8 @@ abstract class SpecialItem(val id: String) { open fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {} + open fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {} + companion object { val KEY_HCU_ITEM_ID = NamespacedKey("hcu_items", "id") val KEY_HCU_ITEM_TIER = NamespacedKey("hcu_items", "tier") diff --git a/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GliderItem.kt b/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GliderItem.kt new file mode 100644 index 0000000..9a497ff --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GliderItem.kt @@ -0,0 +1,475 @@ +package net.hareworks.hcu.items.domain.impl + +import net.hareworks.hcu.items.domain.SpecialItem +import net.hareworks.hcu.items.domain.Tier +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.block.data.Lightable +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.Damageable +import org.bukkit.persistence.PersistentDataType +import org.bukkit.util.Vector +import org.bukkit.Particle +import org.bukkit.Sound +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.* + +/** + * Glider Item - グライダーアイテム + * + * サンプルのGlidersプロジェクトを参考にした、Paper/Bukkit向けのグライダー実装。 + * + * 特徴: + * - 空中での落下速度を軽減し、滑空が可能 + * - ティアによって滑空性能と耐久性が変化 + * - アップドラフト(上昇気流)のサポート + * - 使用時に耐久値が減少 + */ +class GliderItem : SpecialItem("glider") { + + companion object { + + val activeGliders = ConcurrentHashMap() + + + val KEY_GLIDER_ENABLED = NamespacedKey("hcu_items", "glider_enabled") + val KEY_GLIDER_COPPER = NamespacedKey("hcu_items", "glider_copper_upgrade") + val KEY_GLIDER_NETHER = NamespacedKey("hcu_items", "glider_nether_upgrade") + + + private const val MIN_DEPLOY_HEIGHT = 2.0 + private const val DURABILITY_TICK_INTERVAL = 100 + private const val UPDRAFT_BOOST = 0.5 + + + private val TIER_MAX_DURABILITY = mapOf( + Tier.ONE to 64, + Tier.TWO to 128, + Tier.THREE to 256, + Tier.FOUR to 512, + Tier.FIVE to 1024 + ) + + + private val TIER_FALL_SPEED = mapOf( + Tier.ONE to -0.08, + Tier.TWO to -0.065, + Tier.THREE to -0.05, + Tier.FOUR to -0.04, + Tier.FIVE to -0.03 + ) + + + private val TIER_HUNGER_INTERVAL = mapOf( + Tier.ONE to 40, + Tier.TWO to 60, + Tier.THREE to 80, + Tier.FOUR to 120, + Tier.FIVE to 200 + ) + + + private const val HUNGER_EXHAUSTION = 0.3f + + private val UPDRAFT_BLOCKS = setOf( + Material.FIRE, + Material.SOUL_FIRE, + Material.CAMPFIRE, + Material.SOUL_CAMPFIRE, + Material.MAGMA_BLOCK, + Material.LAVA + ) + } + + data class GliderState( + var ticksGliding: Int = 0, + var lastTickTime: Long = System.currentTimeMillis() + ) + + override fun buildItem(tier: Tier): ItemStack { + val item = ItemStack(Material.PHANTOM_MEMBRANE) + val meta = item.itemMeta ?: return item + + val maxDurability = TIER_MAX_DURABILITY[tier] ?: 64 + val fallSpeed = TIER_FALL_SPEED[tier] ?: -0.05 + val hungerInterval = TIER_HUNGER_INTERVAL[tier] ?: 80 + + + val glideStars = "★".repeat(tier.level) + "☆".repeat(5 - tier.level) + + val efficiencyStars = "★".repeat(tier.level) + "☆".repeat(5 - tier.level) + + meta.displayName(Component.text("グライダー", tier.color)) + meta.lore(listOf( + Component.empty(), + Component.text("空中で右クリックで展開", NamedTextColor.GRAY), + Component.text("地上や水中で自動収納", NamedTextColor.GRAY), + Component.empty(), + Component.text("【性能】", NamedTextColor.WHITE), + Component.text("ティア: ${tier.name}", tier.color), + Component.text("滞空力: $glideStars", NamedTextColor.AQUA), + Component.text("燃費: $efficiencyStars", NamedTextColor.GREEN), + Component.text("耐久値: $maxDurability", NamedTextColor.DARK_GRAY) + )) + + + meta.persistentDataContainer.set(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN, false) + + item.itemMeta = meta + return item + } + + override fun onInteract(event: PlayerInteractEvent) { + val player = event.player + val item = event.item ?: return + val tier = SpecialItem.getTier(item) + + + if (!canDeploy(player)) { + player.sendMessage(Component.text("グライダーを展開するには空中にいる必要があります!", NamedTextColor.RED)) + return + } + + + if (isBroken(item)) { + player.sendMessage(Component.text("グライダーが壊れています!修理が必要です。", NamedTextColor.RED)) + return + } + + val meta = item.itemMeta ?: return + val isEnabled = meta.persistentDataContainer.get(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN) ?: false + + if (isEnabled) { + + disableGlider(player, item) + player.sendMessage(Component.text("グライダーを収納しました", NamedTextColor.YELLOW)) + } else { + + enableGlider(player, item) + player.sendMessage(Component.text("グライダーを展開しました!", NamedTextColor.GREEN)) + player.playSound(player.location, Sound.ITEM_ARMOR_EQUIP_ELYTRA, 1.0f, 1.0f) + } + + event.isCancelled = true + } + + override fun onPlayerMove(event: PlayerMoveEvent) { + val player = event.player + val item = player.inventory.itemInMainHand + + if (!isGliderEnabled(item)) return + + + if (isFlyingBlocked(player)) { + disableGlider(player, item) + return + } + } + + fun tickGlider(player: Player) { + val item = player.inventory.itemInMainHand + if (!SpecialItem.isSpecialItem(item) || SpecialItem.getId(item) != "glider") return + if (!isGliderEnabled(item)) return + + val tier = SpecialItem.getTier(item) + val state = activeGliders.getOrPut(player.uniqueId) { GliderState() } + + + if (isFlyingBlocked(player)) { + disableGlider(player, item) + return + } + + state.ticksGliding++ + + + player.fallDistance = 0f + + + if (checkAndApplyUpdraft(player)) { + spawnUpdraftParticles(player) + return + } + + + applyGlidingPhysics(player, tier) + + + if (state.ticksGliding % 5 == 0) { + spawnGlidingParticles(player) + } + + + val hungerInterval = TIER_HUNGER_INTERVAL[tier] ?: 80 + if (state.ticksGliding % hungerInterval == 0) { + consumeHunger(player) + } + + + if (state.ticksGliding % DURABILITY_TICK_INTERVAL == 0) { + consumeDurability(player, item, tier) + } + } + + private fun applyGlidingPhysics(player: Player, tier: Tier) { + val velocity = player.velocity + val fallSpeed = TIER_FALL_SPEED[tier] ?: -0.05 + + + val direction = player.location.direction + + + val horizontalDir = Vector(direction.x, 0.0, direction.z) + if (horizontalDir.lengthSquared() > 0) { + horizontalDir.normalize() + } + + + val currentHorizontalSpeed = sqrt(velocity.x * velocity.x + velocity.z * velocity.z) + + + val baseGlideSpeed = 0.4 + + val tierBonus = (tier.level - 1) * 0.05 + val targetSpeed = baseGlideSpeed + tierBonus + + + val newHorizontalSpeed = if (currentHorizontalSpeed < targetSpeed) { + + minOf(currentHorizontalSpeed + 0.1, targetSpeed) + } else { + + maxOf(currentHorizontalSpeed * 0.95, targetSpeed) + } + + + val newVelocity = Vector( + horizontalDir.x * newHorizontalSpeed, + maxOf(velocity.y, fallSpeed), + horizontalDir.z * newHorizontalSpeed + ) + player.sendActionBar(Component.text("Gliding at ${newHorizontalSpeed} m/s", NamedTextColor.GREEN)) + + player.velocity = newVelocity + } + + /** + * 空腹を消費 + * + * 滑空中は少しずつ空腹になる。 + */ + private fun consumeHunger(player: Player) { + + player.exhaustion = player.exhaustion + HUNGER_EXHAUSTION + } + + /** + * 上昇気流をチェックし、検出された場合はブーストを適用 + */ + private fun checkAndApplyUpdraft(player: Player): Boolean { + val location = player.location + + + for (y in 0 until 20) { + 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) + return true + } + } + + + for (x in -2..2) { + for (z in -2..2) { + for (y in -3..0) { + val checkLoc = location.clone().add(x.toDouble(), y.toDouble(), z.toDouble()) + 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) + return true + } + } + } + } + + return false + } + + /** + * 耐久値を消費 + */ + private fun consumeDurability(player: Player, item: ItemStack, tier: Tier) { + val meta = item.itemMeta ?: return + if (meta is Damageable) { + val maxDamage = TIER_MAX_DURABILITY[tier] ?: 64 + val currentDamage = meta.damage + + if (currentDamage >= maxDamage - 1) { + + meta.damage = maxDamage + item.itemMeta = meta + disableGlider(player, item) + player.playSound(player.location, Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f) + player.sendMessage(Component.text("グライダーが壊れました!", NamedTextColor.RED)) + } else { + meta.damage = currentDamage + 1 + item.itemMeta = meta + } + } + } + + /** + * グライダーを展開 + */ + private fun enableGlider(player: Player, item: ItemStack) { + val meta = item.itemMeta ?: return + meta.persistentDataContainer.set(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN, true) + item.itemMeta = meta + + activeGliders[player.uniqueId] = GliderState() + } + + /** + * グライダーを収納 + */ + private fun disableGlider(player: Player, item: ItemStack) { + val meta = item.itemMeta ?: return + meta.persistentDataContainer.set(KEY_GLIDER_ENABLED, PersistentDataType.BOOLEAN, false) + item.itemMeta = meta + + activeGliders.remove(player.uniqueId) + } + + /** + * グライダーが展開されているかチェック + */ + private fun isGliderEnabled(item: ItemStack?): Boolean { + if (item == null || 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 ?: return true + if (meta is Damageable) { + val tier = SpecialItem.getTier(item) + val maxDamage = TIER_MAX_DURABILITY[tier] ?: 64 + return meta.damage >= maxDamage + } + return false + } + + /** + * グライダーを展開できるかチェック + */ + private fun canDeploy(player: Player): Boolean { + + if (player.isInsideVehicle) return false + + + val loc = player.location + val belowOne = loc.clone().subtract(0.0, 1.0, 0.0).block + val belowTwo = loc.clone().subtract(0.0, 2.0, 0.0).block + + @Suppress("DEPRECATION") + val isInAir = !player.isOnGround && belowOne.type.isAir && belowTwo.type.isAir + val hasUpdraft = checkNearUpdraft(player) + val isFalling = player.fallDistance > 2 + + return isInAir || hasUpdraft || isFalling || isGliderEnabled(player.inventory.itemInMainHand) + } + + /** + * 近くに上昇気流源があるかチェック + */ + private fun checkNearUpdraft(player: Player): Boolean { + val location = player.location + for (x in -2..2) { + for (z in -2..2) { + for (y in -3..0) { + val block = location.clone().add(x.toDouble(), y.toDouble(), z.toDouble()).block + if (UPDRAFT_BLOCKS.contains(block.type)) { + return true + } + } + } + } + return false + } + + /** + * 飛行がブロックされているかチェック + */ + @Suppress("DEPRECATION") + private fun isFlyingBlocked(player: Player): Boolean { + return player.isOnGround || + player.isInWater || + player.isSwimming || + player.isGliding + } + + /** + * 滑空中のパーティクル効果 + */ + private fun spawnGlidingParticles(player: Player) { + val loc = player.location.add(0.0, 2.0, 0.0) + player.world.spawnParticle( + Particle.CLOUD, + loc, + 1, + 0.3, 0.0, 0.3, + 0.01 + ) + } + + /** + * 上昇気流のパーティクル効果 + */ + private fun spawnUpdraftParticles(player: Player) { + val loc = player.location.add(0.0, 1.0, 0.0) + player.world.spawnParticle( + Particle.FLAME, + loc, + 5, + 0.3, 0.5, 0.3, + 0.02 + ) + } + + /** + * ダメージイベントをオーバーライドして落下ダメージを無効化 + */ + override fun onEntityDamage(event: EntityDamageEvent) { + if (event.cause == EntityDamageEvent.DamageCause.FALL) { + val entity = event.entity + if (entity is Player && activeGliders.containsKey(entity.uniqueId)) { + + event.isCancelled = true + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GrapplingItem.kt b/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GrapplingItem.kt index eda5adb..4959a30 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GrapplingItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GrapplingItem.kt @@ -91,7 +91,7 @@ class GrapplingItem : SpecialItem("grappling_hook") { stand.persistentDataContainer.set(KEY_ANCHOR_ID, org.bukkit.persistence.PersistentDataType.STRING, "grapple") } - // Mount hook to anchor + if (anchor.addPassenger(hook)) { hook.persistentDataContainer.set(KEY_HOOK_STUCK, org.bukkit.persistence.PersistentDataType.BYTE, 1) } else { 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 a2a8458..87be8a7 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -8,9 +8,25 @@ import org.bukkit.inventory.EquipmentSlot import net.hareworks.hcu.items.App import net.hareworks.hcu.items.domain.ItemRegistry import net.hareworks.hcu.items.domain.SpecialItem +import net.hareworks.hcu.items.domain.impl.GliderItem class EventListener(private val plugin: App) : Listener { + init { + plugin.server.scheduler.runTaskTimer(plugin, Runnable { + tickAllGliders() + }, 1L, 1L) + } + + private fun tickAllGliders() { + val gliderItem = ItemRegistry.get("glider") as? GliderItem ?: return + + for ((uuid, _) in GliderItem.activeGliders) { + val player = plugin.server.getPlayer(uuid) ?: continue + gliderItem.tickGlider(player) + } + } + @EventHandler fun onInteract(event: PlayerInteractEvent) { if (event.hand == EquipmentSlot.OFF_HAND) return @@ -28,7 +44,7 @@ class EventListener(private val plugin: App) : Listener { @EventHandler fun onFish(event: org.bukkit.event.player.PlayerFishEvent) { val player = event.player - val item = player.inventory.itemInMainHand // Assuming main hand usage primarily + val item = player.inventory.itemInMainHand if (SpecialItem.isSpecialItem(item)) { val id = SpecialItem.getId(item) ?: return @@ -44,7 +60,7 @@ class EventListener(private val plugin: App) : Listener { val shooter = projectile.shooter if (shooter is org.bukkit.entity.Player) { - val item = shooter.inventory.itemInMainHand // Check main hand + val item = shooter.inventory.itemInMainHand if (SpecialItem.isSpecialItem(item)) { val id = SpecialItem.getId(item) ?: return val specialItem = ItemRegistry.get(id) @@ -80,4 +96,16 @@ class EventListener(private val plugin: App) : Listener { } } } + + @EventHandler + fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) { + val player = event.player + val item = player.inventory.itemInMainHand + + if (SpecialItem.isSpecialItem(item)) { + val id = SpecialItem.getId(item) ?: return + val specialItem = ItemRegistry.get(id) + specialItem?.onPlayerMove(event) + } + } }