diff --git a/src/main/kotlin/net/hareworks/hcu/items/App.kt b/src/main/kotlin/net/hareworks/hcu/items/App.kt index 05deba8..894c1ae 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/App.kt @@ -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()) } } 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 f69c618..773a755 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/domain/SpecialItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/domain/SpecialItem.kt @@ -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") 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 new file mode 100644 index 0000000..5beec94 --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/domain/impl/GrapplingItem.kt @@ -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") + } +} 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 3237c81..8a7d8b2 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -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) + } + } + } }