diff --git a/src/main/kotlin/net/hareworks/hcu/items/App.kt b/src/main/kotlin/net/hareworks/hcu/items/App.kt index 10b2147..a667a99 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/App.kt @@ -9,6 +9,7 @@ import net.hareworks.hcu.items.registry.ComponentRegistry import net.hareworks.hcu.items.content.items.TestItem import net.hareworks.hcu.items.content.items.GrapplingItem import net.hareworks.hcu.items.content.items.MagnetItem +import net.hareworks.hcu.items.content.items.AutoFeederItem import net.hareworks.hcu.items.content.components.GliderComponent import net.hareworks.hcu.items.content.components.DoubleJumpComponent import net.hareworks.hcu.items.content.components.BlinkComponent @@ -41,6 +42,7 @@ public class App : JavaPlugin() { ItemRegistry.register(TestItem()) ItemRegistry.register(GrapplingItem()) ItemRegistry.register(MagnetItem()) + ItemRegistry.register(AutoFeederItem()) // Register Components ComponentRegistry.register(GliderComponent(this)) diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt b/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt index 0a14a52..15917d6 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt @@ -17,5 +17,6 @@ interface CustomItem { fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {} fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {} fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {} + fun onFoodLevelChange(event: org.bukkit.event.entity.FoodLevelChangeEvent) {} fun onTick(player: org.bukkit.entity.Player, item: ItemStack) {} } diff --git a/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt b/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt index 9eea871..9695d5d 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt @@ -21,6 +21,7 @@ object Config { var doubleJump = DoubleJumpSettings() var veinMiner = VeinMinerSettings() var magnet = MagnetSettings() + var autoFeeder = AutoFeederSettings() /** * グローバル設定 @@ -54,6 +55,13 @@ object Config { var allowScrollChange: Boolean = true ) + /** + * AutoFeederアイテム設定 + */ + data class AutoFeederSettings( + var hungerThreshold: Int = 10 + ) + /** * VeinMinerコンポーネント設定 */ @@ -100,6 +108,7 @@ object Config { loadDoubleJumpSettings(config) loadVeinMinerSettings(plugin, config) loadMagnetSettings(config) + loadAutoFeederSettings(config) saveDefaults(plugin, config) } @@ -169,6 +178,15 @@ object Config { ) } + /** + * AutoFeeder設定を読み込む + */ + private fun loadAutoFeederSettings(config: FileConfiguration) { + autoFeeder = AutoFeederSettings( + hungerThreshold = config.getInt("items.auto_feeder.hunger_threshold", 10) + ) + } + /** * デフォルト設定を保存する */ @@ -183,6 +201,7 @@ object Config { config.addDefault("components.magnet.radius_base", 5.0) config.addDefault("components.magnet.radius_per_tier", 5.0) config.addDefault("components.magnet.allow_scroll_change", true) + config.addDefault("items.auto_feeder.hunger_threshold", 10) config.options().copyDefaults(true) plugin.saveConfig() @@ -196,7 +215,7 @@ object Config { compatibleMaterials: MutableMap> ) { compatibleMaterials.clear() - val groups = config.getList("components.vein_miner.compatible_groups") as? List<*> ?: return + val groups = config.getList("components.vein_miner.compatible_groups") ?: return for (groupObj in groups) { val group = when (groupObj) { diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/items/AutoFeederItem.kt b/src/main/kotlin/net/hareworks/hcu/items/content/items/AutoFeederItem.kt new file mode 100644 index 0000000..f05060a --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/content/items/AutoFeederItem.kt @@ -0,0 +1,215 @@ +package net.hareworks.hcu.items.content.items + +import net.hareworks.hcu.items.api.Tier +import net.hareworks.hcu.items.api.item.AbstractItem +import net.hareworks.hcu.items.config.Config +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Material +import org.bukkit.Particle +import org.bukkit.Sound +import org.bukkit.entity.Player +import org.bukkit.event.entity.FoodLevelChangeEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.BundleMeta +import io.papermc.paper.datacomponent.DataComponentTypes +import io.papermc.paper.datacomponent.item.consumable.ConsumeEffect +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Auto Feeder (オートフィーダー) + * バンドルベースのアイテムで、満腹度が一定以下になると自動で中の食料を食べる + */ +class AutoFeederItem : AbstractItem("auto_feeder") { + + override val maxTier: Int = 2 + + override fun buildItem(tier: Tier): ItemStack { + return ItemStack(Material.BUNDLE).apply { + val meta = itemMeta as? BundleMeta ?: return@apply + + val configInfo = Config.autoFeeder + val threshold = configInfo.hungerThreshold + + // ティアによる説明の違い + val activationDesc = when (tier.level) { + 1 -> Component.text("左手に持っている時のみ発動", NamedTextColor.YELLOW) + 2 -> Component.text("インベントリにあるだけで発動", NamedTextColor.GOLD) + else -> Component.text("左手に持っている時のみ発動", NamedTextColor.YELLOW) + } + + 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("発動閾値: ${threshold}以下", NamedTextColor.GREEN), + activationDesc, + Component.empty(), + Component.text("バンドルに食料を入れて使用", NamedTextColor.AQUA) + )) + + itemMeta = meta + } + } + + override fun onTick(player: Player, item: ItemStack) { + + // 満腹度チェック + val configInfo = Config.autoFeeder + val threshold = configInfo.hungerThreshold + + if (player.foodLevel > threshold) return + + // Tierによる発動条件チェック + val tier = getTier(item) + if (tier.level == 1) { + // Tier 1: 左手(オフハンド)に持っている時のみ + // まったく同じアイテムかチェック(参照または内容の一致) + if (item != player.inventory.itemInOffHand) return + } + // Tier 2: インベントリにあればOK(onTickが呼ばれている時点でインベントリ内にあると仮定) + + // 食事処理を実行 + feed(player, item) + } + + private fun feed(player: Player, item: ItemStack) { + // バンドルから食料を取得して食べる + val bundleMeta = item.itemMeta as? BundleMeta ?: return + val contents = bundleMeta.items.toMutableList() + + // 食料アイテムを探す + val foodItem = contents.firstOrNull { it.type.isEdible } ?: return + + // Food情報を取得 + var nutrition = 0 + var saturation = 0.0f + + if (foodItem.hasData(DataComponentTypes.FOOD)) { + val food = foodItem.getData(DataComponentTypes.FOOD) + if (food != null) { + nutrition = food.nutrition() + saturation = food.saturation() + } + } + + // 食料を消費 + if (foodItem.amount > 1) { + foodItem.amount -= 1 + } else { + contents.remove(foodItem) + } + bundleMeta.setItems(contents) + item.itemMeta = bundleMeta + + // 満腹度を回復 + val newFood = (player.foodLevel + nutrition).coerceAtMost(20) + val newSaturation = (player.saturation + saturation).coerceAtMost(newFood.toFloat()) + + player.foodLevel = newFood + player.saturation = newSaturation + + // 特殊効果を適用 + applyFoodEffects(player, foodItem) + + // フィードバック + player.sendActionBar( + Component.text("🍖 ", NamedTextColor.GOLD) + .append(Component.text(foodItem.type.name.lowercase().replace("_", " "), NamedTextColor.YELLOW)) + .append(Component.text(" を自動で食べました!", NamedTextColor.GRAY)) + ) + + // パーティクルとサウンド + player.world.spawnParticle( + Particle.ITEM, + player.location.add(0.0, 1.5, 0.0), + 10, + 0.3, 0.3, 0.3, + 0.1, + foodItem + ) + + player.playSound( + player.location, + Sound.ENTITY_GENERIC_EAT, + 1.0f, + 1.0f + ) + } + + /** + * DataComponentTypes.CONSUMABLEを使用して効果を適用する + */ + private fun applyFoodEffects(player: Player, item: ItemStack) { + // DataComponentが存在するかチェック + if (!item.hasData(DataComponentTypes.CONSUMABLE)) { + return + } + + val consumable = item.getData(DataComponentTypes.CONSUMABLE) ?: return + + // consumeEffects (APIによっては effects などの可能性あり) + for (consumeEffect in consumable.consumeEffects()) { + if (consumeEffect is ConsumeEffect.ApplyStatusEffects) { + if (Math.random() < consumeEffect.probability()) { + for (effect in consumeEffect.effects()) { + player.addPotionEffect(effect) + } + } + } + } + } + + data class FoodInfo(val nutrition: Int, val saturation: Float) + + private fun getFoodInfo(material: Material): FoodInfo? { + return when (material) { + Material.APPLE -> FoodInfo(4, 2.4f) + Material.BAKED_POTATO -> FoodInfo(5, 6.0f) + Material.BREAD -> FoodInfo(5, 6.0f) + Material.CARROT -> FoodInfo(3, 3.6f) + Material.COOKED_BEEF -> FoodInfo(8, 12.8f) + Material.COOKED_CHICKEN -> FoodInfo(6, 7.2f) + Material.COOKED_COD -> FoodInfo(5, 6.0f) + Material.COOKED_MUTTON -> FoodInfo(6, 9.6f) + Material.COOKED_PORKCHOP -> FoodInfo(8, 12.8f) + Material.COOKED_RABBIT -> FoodInfo(5, 6.0f) + Material.COOKED_SALMON -> FoodInfo(6, 9.6f) + Material.COOKIE -> FoodInfo(2, 0.4f) + Material.DRIED_KELP -> FoodInfo(1, 0.6f) + Material.ENCHANTED_GOLDEN_APPLE -> FoodInfo(4, 9.6f) + Material.GOLDEN_APPLE -> FoodInfo(4, 9.6f) + Material.GOLDEN_CARROT -> FoodInfo(6, 14.4f) + Material.HONEY_BOTTLE -> FoodInfo(6, 1.2f) + Material.MELON_SLICE -> FoodInfo(2, 1.2f) + Material.MUSHROOM_STEW -> FoodInfo(6, 7.2f) + Material.POISONOUS_POTATO -> FoodInfo(2, 1.2f) + Material.POTATO -> FoodInfo(1, 0.6f) + Material.PUFFERFISH -> FoodInfo(1, 0.2f) + Material.PUMPKIN_PIE -> FoodInfo(8, 4.8f) + Material.RABBIT_STEW -> FoodInfo(10, 12.0f) + Material.BEEF -> FoodInfo(3, 1.8f) + Material.CHICKEN -> FoodInfo(2, 1.2f) + Material.COD -> FoodInfo(2, 0.4f) + Material.MUTTON -> FoodInfo(2, 1.2f) + Material.PORKCHOP -> FoodInfo(3, 1.8f) + Material.RABBIT -> FoodInfo(3, 1.8f) + Material.SALMON -> FoodInfo(2, 0.4f) + Material.ROTTEN_FLESH -> FoodInfo(4, 0.8f) + Material.SPIDER_EYE -> FoodInfo(2, 3.2f) + Material.SUSPICIOUS_STEW -> FoodInfo(6, 7.2f) + Material.SWEET_BERRIES -> FoodInfo(2, 0.4f) + Material.GLOW_BERRIES -> FoodInfo(2, 0.4f) + Material.BEETROOT -> FoodInfo(1, 1.2f) + Material.BEETROOT_SOUP -> FoodInfo(6, 7.2f) + Material.CHORUS_FRUIT -> FoodInfo(4, 2.4f) + else -> null + } + } +} 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 e77147a..bd29f51 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -146,6 +146,18 @@ class EventListener(private val plugin: Plugin) : Listener { dispatchToComponents(item) { it.onItemHeld(player, item, event) } } + @EventHandler + fun onFoodLevelChange(event: org.bukkit.event.entity.FoodLevelChangeEvent) { + val entity = event.entity + if (entity is Player) { + // インベントリ内の全アイテムをチェック + val items = entity.inventory.contents.filterNotNull() + for (item in items) { + dispatchToItem(item) { it.onFoodLevelChange(event) } + } + } + } + private fun tickComponents() { for (player in plugin.server.onlinePlayers) { val uuid = player.uniqueId @@ -175,17 +187,25 @@ class EventListener(private val plugin: Plugin) : Listener { } } - // Update Cache // Update Cache lastHeldItems[uuid] = Pair(currentMainHand, currentOffHand) // --- Regular Tick Processing --- - // Armor Tick - val armorItems = player.inventory.armorContents.filterNotNull() - for (item in armorItems) { - if (item.type.isAir) continue + // 1. Dispatch onTick to ALL CustomItems in inventory + // inventory.contents typically covers storage, hotbar, armor, and offhand depending on implementation, + // but we iterate it to ensure we catch items anywhere in the inventory. + for (item in player.inventory.contents) { + if (item == null || item.type.isAir) continue dispatchToItem(item) { it.onTick(player, item) } + } + + // 2. Dispatch to Components for specific slots (Armor, MainHand, OffHand) + // Note: dispatchToItem is skipped here because it was already done above for all items. + + // Armor Tick + for (item in player.inventory.armorContents) { + if (item == null || item.type.isAir) continue dispatchToComponents(item) { component -> if (component is EquippableComponent) { component.onTick(player, item) @@ -195,7 +215,6 @@ class EventListener(private val plugin: Plugin) : Listener { // ToolComponents in Main Hand if (!currentMainHand.type.isAir) { - dispatchToItem(currentMainHand) { it.onTick(player, currentMainHand) } dispatchToComponents(currentMainHand) { component -> if (component is ToolComponent) { component.onHoldTick(player, currentMainHand) @@ -208,7 +227,6 @@ class EventListener(private val plugin: Plugin) : Listener { // ToolComponents in Off Hand if (!currentOffHand.type.isAir) { - dispatchToItem(currentOffHand) { it.onTick(player, currentOffHand) } dispatchToComponents(currentOffHand) { component -> if (component is ToolComponent) { component.onHoldTick(player, currentOffHand) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index faf8146..cfdf48b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -19,7 +19,7 @@ components: "#minecraft:gold_ores": "#FCEE4B" "#minecraft:copper_ores": "#966756" "#minecraft:coal_ores": "#363636" - "#minecraft:redstone_ores": "#FF0000" + "#minecraft:redstone_ores": "#FF0000" "#minecraft:lapis_ores": "#1F61AE" "#minecraft:emerald_ores": "#00D93A" "minecraft:ancient_debr ": "#623A32" @@ -71,3 +71,6 @@ components: - "minecraft:hay_block" - "#minecraft:wart_blocks" +items: + auto_feeder: + hunger_threshold: 10 # 満腹度がこの値以下になると自動で食事