feat: Introduce Glider item with its associated recipes, models, textures, and mechanics.
This commit is contained in:
parent
0a8011d65b
commit
7cc8ccc395
1
sample/Gliders
Submodule
1
sample/Gliders
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit a69cba4406090a0f8a5883b463dc8083c9802630
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user