feat: Add AutoFeeder item with automatic food consumption and configurable hunger threshold
This commit is contained in:
parent
fb7ae54875
commit
6bc2fb2be2
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Material, Set<Material>>
|
||||
) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -71,3 +71,6 @@ components:
|
|||
- "minecraft:hay_block"
|
||||
- "#minecraft:wart_blocks"
|
||||
|
||||
items:
|
||||
auto_feeder:
|
||||
hunger_threshold: 10 # 満腹度がこの値以下になると自動で食事
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user