feat: Introduce Glider item with its associated recipes, models, textures, and mechanics.

This commit is contained in:
Kariya 2025-12-08 12:51:39 +00:00
parent 0a8011d65b
commit 7cc8ccc395
6 changed files with 511 additions and 3 deletions

1
sample/Gliders Submodule

@ -0,0 +1 @@
Subproject commit a69cba4406090a0f8a5883b463dc8083c9802630

View File

@ -7,6 +7,7 @@ import net.hareworks.permits_lib.domain.NodeRegistration
import net.hareworks.hcu.items.domain.ItemRegistry import net.hareworks.hcu.items.domain.ItemRegistry
import net.hareworks.hcu.items.domain.impl.TestItem import net.hareworks.hcu.items.domain.impl.TestItem
import net.hareworks.hcu.items.domain.impl.GrapplingItem import net.hareworks.hcu.items.domain.impl.GrapplingItem
import net.hareworks.hcu.items.domain.impl.GliderItem
import org.bukkit.permissions.PermissionDefault import org.bukkit.permissions.PermissionDefault
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -32,5 +33,6 @@ public class App : JavaPlugin() {
// Register items // Register items
ItemRegistry.register(TestItem()) ItemRegistry.register(TestItem())
ItemRegistry.register(GrapplingItem()) ItemRegistry.register(GrapplingItem())
ItemRegistry.register(GliderItem())
} }
} }

View File

@ -30,6 +30,8 @@ abstract class SpecialItem(val id: String) {
open fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {} open fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {}
open fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {}
companion object { companion object {
val KEY_HCU_ITEM_ID = NamespacedKey("hcu_items", "id") val KEY_HCU_ITEM_ID = NamespacedKey("hcu_items", "id")
val KEY_HCU_ITEM_TIER = NamespacedKey("hcu_items", "tier") val KEY_HCU_ITEM_TIER = NamespacedKey("hcu_items", "tier")

View File

@ -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<UUID, GliderState>()
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
}
}
}
}

View File

@ -91,7 +91,7 @@ class GrapplingItem : SpecialItem("grappling_hook") {
stand.persistentDataContainer.set(KEY_ANCHOR_ID, org.bukkit.persistence.PersistentDataType.STRING, "grapple") stand.persistentDataContainer.set(KEY_ANCHOR_ID, org.bukkit.persistence.PersistentDataType.STRING, "grapple")
} }
// Mount hook to anchor
if (anchor.addPassenger(hook)) { if (anchor.addPassenger(hook)) {
hook.persistentDataContainer.set(KEY_HOOK_STUCK, org.bukkit.persistence.PersistentDataType.BYTE, 1) hook.persistentDataContainer.set(KEY_HOOK_STUCK, org.bukkit.persistence.PersistentDataType.BYTE, 1)
} else { } else {

View File

@ -8,9 +8,25 @@ import org.bukkit.inventory.EquipmentSlot
import net.hareworks.hcu.items.App import net.hareworks.hcu.items.App
import net.hareworks.hcu.items.domain.ItemRegistry import net.hareworks.hcu.items.domain.ItemRegistry
import net.hareworks.hcu.items.domain.SpecialItem import net.hareworks.hcu.items.domain.SpecialItem
import net.hareworks.hcu.items.domain.impl.GliderItem
class EventListener(private val plugin: App) : Listener { 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 @EventHandler
fun onInteract(event: PlayerInteractEvent) { fun onInteract(event: PlayerInteractEvent) {
if (event.hand == EquipmentSlot.OFF_HAND) return if (event.hand == EquipmentSlot.OFF_HAND) return
@ -28,7 +44,7 @@ class EventListener(private val plugin: App) : Listener {
@EventHandler @EventHandler
fun onFish(event: org.bukkit.event.player.PlayerFishEvent) { fun onFish(event: org.bukkit.event.player.PlayerFishEvent) {
val player = event.player val player = event.player
val item = player.inventory.itemInMainHand // Assuming main hand usage primarily val item = player.inventory.itemInMainHand
if (SpecialItem.isSpecialItem(item)) { if (SpecialItem.isSpecialItem(item)) {
val id = SpecialItem.getId(item) ?: return val id = SpecialItem.getId(item) ?: return
@ -44,7 +60,7 @@ class EventListener(private val plugin: App) : Listener {
val shooter = projectile.shooter val shooter = projectile.shooter
if (shooter is org.bukkit.entity.Player) { if (shooter is org.bukkit.entity.Player) {
val item = shooter.inventory.itemInMainHand // Check main hand val item = shooter.inventory.itemInMainHand
if (SpecialItem.isSpecialItem(item)) { if (SpecialItem.isSpecialItem(item)) {
val id = SpecialItem.getId(item) ?: return val id = SpecialItem.getId(item) ?: return
val specialItem = ItemRegistry.get(id) 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)
}
}
} }