feat: Implement a grappling hook special item with dedicated event handling for its mechanics.

This commit is contained in:
Kariya 2025-12-08 08:07:01 +00:00
parent 553f4589a6
commit a47930ac87
4 changed files with 245 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import net.hareworks.permits_lib.PermitsLib
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 org.bukkit.permissions.PermissionDefault
import org.bukkit.plugin.java.JavaPlugin
@ -30,5 +31,6 @@ public class App : JavaPlugin() {
// Register items
ItemRegistry.register(TestItem())
ItemRegistry.register(GrapplingItem())
}
}

View File

@ -41,6 +41,26 @@ abstract class SpecialItem(val id: String) {
*/
open fun onInteract(event: org.bukkit.event.player.PlayerInteractEvent) {}
/**
* Called when a player uses a fishing rod (casts, reels in, catches, etc).
*/
open fun onFish(event: org.bukkit.event.player.PlayerFishEvent) {}
/**
* Called when a projectile from this item hits something.
*/
open fun onProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) {}
/**
* Called when a projectile from this item is launched.
*/
open fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) {}
/**
* Called when the player holding this item takes damage.
*/
open fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {}
companion object {
val KEY_HCU_ITEM_ID = NamespacedKey("hcu_items", "id")
val KEY_HCU_ITEM_TIER = NamespacedKey("hcu_items", "tier")

View File

@ -0,0 +1,167 @@
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.Location
import org.bukkit.Material
import org.bukkit.event.player.PlayerFishEvent
import org.bukkit.inventory.ItemStack
class GrapplingItem : SpecialItem("grappling_hook") {
override fun buildItem(tier: Tier): ItemStack {
val item = ItemStack(Material.FISHING_ROD)
val meta = item.itemMeta ?: return item
meta.displayName(Component.text("Grappling Hook", tier.color))
meta.lore(listOf(
Component.text("Cast and reel in to pull yourself!", NamedTextColor.GRAY),
Component.text("Tier: ${tier.level}", tier.color)
))
// Optional: Make it unbreakable or have durability based on tier?
// meta.isUnbreakable = true
item.itemMeta = meta
return item
}
override fun onFish(event: PlayerFishEvent) {
val hook = event.hook
// Check if stuck via our custom PDC tag (meaning it hit a block and we anchored it)
val isStuck = hook.persistentDataContainer.has(KEY_HOOK_STUCK, org.bukkit.persistence.PersistentDataType.BYTE)
// If reeling in and it's stuck (anchored), pull the player
if (event.state == PlayerFishEvent.State.REEL_IN && isStuck) {
val player = event.player
val playerLoc = player.location
val hookLoc = hook.location
val vector = hookLoc.toVector().subtract(playerLoc.toVector())
val item = player.inventory.itemInMainHand
val tier = SpecialItem.getTier(item)
val speed = 1.0 + (tier.level * 0.4)
val velocity = vector.normalize().multiply(speed)
player.velocity = velocity
// "飛ぶ速度に比例して空腹になる"
// Cost based on speed (magnitude of velocity)
// Base cost multiplier
val hungerCostBase = 2.0
// Tier reduces cost? Or just proportional to speed?
// Let's make higher tiers slightly more efficient per unit of speed.
val efficiency = 1.0 + (tier.level * 0.1)
val hungerCost = (velocity.length() * hungerCostBase / efficiency).toInt().coerceAtLeast(1)
player.foodLevel = (player.foodLevel - hungerCost).coerceAtLeast(0)
}
// Cleanup anchor when reeling in or if the hook is removed
if (event.state == PlayerFishEvent.State.REEL_IN ||
event.state == PlayerFishEvent.State.CAUGHT_ENTITY ||
event.state == PlayerFishEvent.State.BITE) { // BITE might be too early? usually REEL_IN is the end
val vehicle = hook.vehicle
if (vehicle is org.bukkit.entity.ArmorStand && vehicle.persistentDataContainer.has(KEY_ANCHOR_ID, org.bukkit.persistence.PersistentDataType.STRING)) {
vehicle.remove()
}
}
}
override fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) {
val projectile = event.entity
val shooter = projectile.shooter
if (shooter is org.bukkit.entity.Player) {
// "浮を投げるとき投げたプレイヤーの速度が慣性に乗るようにしてください"
// Add player's velocity to the projectile
projectile.velocity = projectile.velocity.add(shooter.velocity)
}
}
override fun onProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) {
if ((event.hitBlock != null || event.hitEntity != null) && event.hitBlock?.isCollidable() == true) {
val hook = event.entity
if (hook is org.bukkit.entity.FishHook) {
// Determine spawn location slightly adjusted to avoid clipping into the wall too much?
// Or just exactly at the hook. Hook collision box is small.
val location = hook.location
// Spawn anchor
val anchor = location.world.spawn(location, org.bukkit.entity.ArmorStand::class.java) { stand ->
stand.isVisible = false
stand.isMarker = true
stand.setGravity(false)
stand.isSmall = true
stand.isInvulnerable = true
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 {
anchor.remove()
}
}
}
}
override fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {
// "手に持っているときは落下ダメージ半減"
// "受けたダメージ分だけ空腹になる"
// "もし空腹で受けきれなかった場合は普通にダメージを受ける"
// "Tireによって空腹度合いが変わる"
if (event.cause == org.bukkit.event.entity.EntityDamageEvent.DamageCause.FALL) {
val player = event.entity
if (player is org.bukkit.entity.Player) {
val item = player.inventory.itemInMainHand
val tier = SpecialItem.getTier(item)
val originalDamage = event.damage
val reducedDamage = originalDamage / 2.0
val damageToAbsorb = originalDamage - reducedDamage
// Hunger cost:
// Base cost is equal to damage absorbed? Or some factor?
// "Tireによって空腹度合いが変わる" -> Higher tier = cheaper
// Example: Cost = Damage * (2.5 - (0.4 * Tier))
// Tier 1: 2.1x damage, Tier 5: 0.5x damage
val costFactor = (3.0 - (0.5 * tier.level)).coerceAtLeast(0.5)
val hungerCostPerDamage = costFactor
val totalHungerCost = (damageToAbsorb * hungerCostPerDamage).toInt()
if (player.foodLevel >= totalHungerCost) {
// "受けたダメージ分だけ空腹になる" (interpreted as paying the cost)
player.foodLevel = (player.foodLevel - totalHungerCost).coerceAtLeast(0)
event.damage = reducedDamage
} else {
// "空腹で受けきれなかった場合はそのままダメージを受けるのではなく空腹で受けられる分のダメージは受けて余ったダメージを直接受けるようにしてください"
// Calculate how much damage we can absorb with available food
val availableFood = player.foodLevel
val damageWeCanAbsorb = availableFood / hungerCostPerDamage
val damageWeCannotAbsorb = damageToAbsorb - damageWeCanAbsorb
// Consume all available food
player.foodLevel = 0
// Take reduced damage for what we could absorb, plus full damage for what we couldn't
event.damage = reducedDamage + damageWeCannotAbsorb
}
}
}
}
companion object {
val KEY_HOOK_STUCK = org.bukkit.NamespacedKey("hcu_items", "hook_stuck")
val KEY_ANCHOR_ID = org.bukkit.NamespacedKey("hcu_items", "anchor_id")
}
}

View File

@ -25,4 +25,60 @@ class EventListener(private val plugin: App) : Listener {
specialItem?.onInteract(event)
}
}
@EventHandler
fun onFish(event: org.bukkit.event.player.PlayerFishEvent) {
val player = event.player
val item = player.inventory.itemInMainHand // Assuming main hand usage primarily
if (SpecialItem.isSpecialItem(item)) {
val id = SpecialItem.getId(item) ?: return
val specialItem = ItemRegistry.get(id)
specialItem?.onFish(event)
}
}
@EventHandler
fun onProjectileHit(event: org.bukkit.event.entity.ProjectileHitEvent) {
val projectile = event.entity
val shooter = projectile.shooter
if (shooter is org.bukkit.entity.Player) {
val item = shooter.inventory.itemInMainHand // Check main hand
if (SpecialItem.isSpecialItem(item)) {
val id = SpecialItem.getId(item) ?: return
val specialItem = ItemRegistry.get(id)
specialItem?.onProjectileHit(event)
}
}
}
@EventHandler
fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) {
val projectile = event.entity
val shooter = projectile.shooter
if (shooter is org.bukkit.entity.Player) {
val item = shooter.inventory.itemInMainHand
if (SpecialItem.isSpecialItem(item)) {
val id = SpecialItem.getId(item) ?: return
val specialItem = ItemRegistry.get(id)
specialItem?.onProjectileLaunch(event)
}
}
}
@EventHandler
fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {
val entity = event.entity
if (entity is org.bukkit.entity.Player) {
val item = entity.inventory.itemInMainHand
if (SpecialItem.isSpecialItem(item)) {
val id = SpecialItem.getId(item) ?: return
val specialItem = ItemRegistry.get(id)
specialItem?.onEntityDamage(event)
}
}
}
}