feat: Add AutoFeeder item with automatic food consumption and configurable hunger threshold

This commit is contained in:
Kariya 2025-12-18 16:02:09 +00:00
parent fb7ae54875
commit 6bc2fb2be2
6 changed files with 267 additions and 9 deletions

View File

@ -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.TestItem
import net.hareworks.hcu.items.content.items.GrapplingItem import net.hareworks.hcu.items.content.items.GrapplingItem
import net.hareworks.hcu.items.content.items.MagnetItem 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.GliderComponent
import net.hareworks.hcu.items.content.components.DoubleJumpComponent import net.hareworks.hcu.items.content.components.DoubleJumpComponent
import net.hareworks.hcu.items.content.components.BlinkComponent import net.hareworks.hcu.items.content.components.BlinkComponent
@ -41,6 +42,7 @@ public class App : JavaPlugin() {
ItemRegistry.register(TestItem()) ItemRegistry.register(TestItem())
ItemRegistry.register(GrapplingItem()) ItemRegistry.register(GrapplingItem())
ItemRegistry.register(MagnetItem()) ItemRegistry.register(MagnetItem())
ItemRegistry.register(AutoFeederItem())
// Register Components // Register Components
ComponentRegistry.register(GliderComponent(this)) ComponentRegistry.register(GliderComponent(this))

View File

@ -17,5 +17,6 @@ interface CustomItem {
fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {} fun onEntityDamage(event: org.bukkit.event.entity.EntityDamageEvent) {}
fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {} fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {}
fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {} 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) {} fun onTick(player: org.bukkit.entity.Player, item: ItemStack) {}
} }

View File

@ -21,6 +21,7 @@ object Config {
var doubleJump = DoubleJumpSettings() var doubleJump = DoubleJumpSettings()
var veinMiner = VeinMinerSettings() var veinMiner = VeinMinerSettings()
var magnet = MagnetSettings() var magnet = MagnetSettings()
var autoFeeder = AutoFeederSettings()
/** /**
* グローバル設定 * グローバル設定
@ -54,6 +55,13 @@ object Config {
var allowScrollChange: Boolean = true var allowScrollChange: Boolean = true
) )
/**
* AutoFeederアイテム設定
*/
data class AutoFeederSettings(
var hungerThreshold: Int = 10
)
/** /**
* VeinMinerコンポーネント設定 * VeinMinerコンポーネント設定
*/ */
@ -100,6 +108,7 @@ object Config {
loadDoubleJumpSettings(config) loadDoubleJumpSettings(config)
loadVeinMinerSettings(plugin, config) loadVeinMinerSettings(plugin, config)
loadMagnetSettings(config) loadMagnetSettings(config)
loadAutoFeederSettings(config)
saveDefaults(plugin, 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_base", 5.0)
config.addDefault("components.magnet.radius_per_tier", 5.0) config.addDefault("components.magnet.radius_per_tier", 5.0)
config.addDefault("components.magnet.allow_scroll_change", true) config.addDefault("components.magnet.allow_scroll_change", true)
config.addDefault("items.auto_feeder.hunger_threshold", 10)
config.options().copyDefaults(true) config.options().copyDefaults(true)
plugin.saveConfig() plugin.saveConfig()
@ -196,7 +215,7 @@ object Config {
compatibleMaterials: MutableMap<Material, Set<Material>> compatibleMaterials: MutableMap<Material, Set<Material>>
) { ) {
compatibleMaterials.clear() 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) { for (groupObj in groups) {
val group = when (groupObj) { val group = when (groupObj) {

View File

@ -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: インベントリにあればOKonTickが呼ばれている時点でインベントリ内にあると仮定
// 食事処理を実行
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
}
}
}

View File

@ -146,6 +146,18 @@ class EventListener(private val plugin: Plugin) : Listener {
dispatchToComponents(item) { it.onItemHeld(player, item, event) } 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() { private fun tickComponents() {
for (player in plugin.server.onlinePlayers) { for (player in plugin.server.onlinePlayers) {
val uuid = player.uniqueId val uuid = player.uniqueId
@ -175,17 +187,25 @@ class EventListener(private val plugin: Plugin) : Listener {
} }
} }
// Update Cache
// Update Cache // Update Cache
lastHeldItems[uuid] = Pair(currentMainHand, currentOffHand) lastHeldItems[uuid] = Pair(currentMainHand, currentOffHand)
// --- Regular Tick Processing --- // --- Regular Tick Processing ---
// Armor Tick // 1. Dispatch onTick to ALL CustomItems in inventory
val armorItems = player.inventory.armorContents.filterNotNull() // inventory.contents typically covers storage, hotbar, armor, and offhand depending on implementation,
for (item in armorItems) { // but we iterate it to ensure we catch items anywhere in the inventory.
if (item.type.isAir) continue for (item in player.inventory.contents) {
if (item == null || item.type.isAir) continue
dispatchToItem(item) { it.onTick(player, item) } 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 -> dispatchToComponents(item) { component ->
if (component is EquippableComponent) { if (component is EquippableComponent) {
component.onTick(player, item) component.onTick(player, item)
@ -195,7 +215,6 @@ class EventListener(private val plugin: Plugin) : Listener {
// ToolComponents in Main Hand // ToolComponents in Main Hand
if (!currentMainHand.type.isAir) { if (!currentMainHand.type.isAir) {
dispatchToItem(currentMainHand) { it.onTick(player, currentMainHand) }
dispatchToComponents(currentMainHand) { component -> dispatchToComponents(currentMainHand) { component ->
if (component is ToolComponent) { if (component is ToolComponent) {
component.onHoldTick(player, currentMainHand) component.onHoldTick(player, currentMainHand)
@ -208,7 +227,6 @@ class EventListener(private val plugin: Plugin) : Listener {
// ToolComponents in Off Hand // ToolComponents in Off Hand
if (!currentOffHand.type.isAir) { if (!currentOffHand.type.isAir) {
dispatchToItem(currentOffHand) { it.onTick(player, currentOffHand) }
dispatchToComponents(currentOffHand) { component -> dispatchToComponents(currentOffHand) { component ->
if (component is ToolComponent) { if (component is ToolComponent) {
component.onHoldTick(player, currentOffHand) component.onHoldTick(player, currentOffHand)

View File

@ -71,3 +71,6 @@ components:
- "minecraft:hay_block" - "minecraft:hay_block"
- "#minecraft:wart_blocks" - "#minecraft:wart_blocks"
items:
auto_feeder:
hunger_threshold: 10 # 満腹度がこの値以下になると自動で食事