From f45947b9e97a2249f10da7b30ccd4255db3e3634 Mon Sep 17 00:00:00 2001 From: Kariya Date: Tue, 16 Dec 2025 11:57:26 +0000 Subject: [PATCH] feat: Implement Vein Miner component with configurable multi-block breaking, compatible material groups, tool categories, and visual highlighting. --- .../kotlin/net/hareworks/hcu/items/App.kt | 2 + .../items/api/component/CustomComponent.kt | 1 + .../net/hareworks/hcu/items/config/Config.kt | 51 +++ .../content/components/VeinMinerComponent.kt | 311 ++++++++++++++++++ .../hcu/items/listeners/EventListener.kt | 9 + src/main/resources/config.yml | 42 +++ 6 files changed, 416 insertions(+) create mode 100644 src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt diff --git a/src/main/kotlin/net/hareworks/hcu/items/App.kt b/src/main/kotlin/net/hareworks/hcu/items/App.kt index 5d248d5..143fce3 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/App.kt @@ -11,6 +11,7 @@ import net.hareworks.hcu.items.content.items.GrapplingItem import net.hareworks.hcu.items.content.components.GliderComponent import net.hareworks.hcu.items.content.components.DoubleJumpComponent import net.hareworks.hcu.items.content.components.BlinkComponent +import net.hareworks.hcu.items.content.components.VeinMinerComponent import org.bukkit.permissions.PermissionDefault import org.bukkit.plugin.java.JavaPlugin @@ -43,6 +44,7 @@ public class App : JavaPlugin() { ComponentRegistry.register(GliderComponent(this)) ComponentRegistry.register(DoubleJumpComponent(this)) ComponentRegistry.register(BlinkComponent(this)) + ComponentRegistry.register(VeinMinerComponent(this)) } } diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt index 5ee02cc..8754539 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/component/CustomComponent.kt @@ -28,4 +28,5 @@ interface CustomComponent { fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {} fun onToggleSneak(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.player.PlayerToggleSneakEvent) {} fun onToggleGlide(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.entity.EntityToggleGlideEvent) {} + fun onBlockBreak(event: org.bukkit.event.block.BlockBreakEvent) {} } 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 678e880..58bfaec 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt @@ -10,6 +10,13 @@ object Config { var doubleJumpCooldown: Long = 1500 var doubleJumpPowerVertical: Double = 0.5 var doubleJumpPowerForward: Double = 0.3 + + // Vein Miner + data class ToolCategory(val toolPattern: String, val allowedBlockPatterns: List) + var veinMinerMaxBlocks: Int = 64 + var veinMinerActivationMode: String = "SNEAK" // "SNEAK", "ALWAYS", "STAND" + val veinMinerCompatibleMaterials: MutableMap> = mutableMapOf() + val veinMinerToolCategories: MutableList = mutableListOf() fun load(plugin: JavaPlugin) { plugin.reloadConfig() @@ -23,6 +30,12 @@ object Config { doubleJumpPowerVertical = config.getDouble("components.double_jump.power.vertical", 0.5) doubleJumpPowerForward = config.getDouble("components.double_jump.power.forward", 0.3) + // Vein Miner + veinMinerMaxBlocks = config.getInt("components.vein_miner.max_blocks", 64) + veinMinerActivationMode = config.getString("components.vein_miner.activation_mode", "SNEAK") ?: "SNEAK" + loadCompatibleGroups(config) + loadToolCategories(config) + // Save back to ensure defaults are written if missing saveDefaults(plugin, config) } @@ -33,7 +46,45 @@ object Config { config.addDefault("components.double_jump.power.vertical", 0.5) config.addDefault("components.double_jump.power.forward", 0.3) + config.addDefault("components.vein_miner.max_blocks", 64) + // Groups default is complex to add here, relying on config.yml resource or existing file + + // Tool categories default is handled by config.yml resource + config.options().copyDefaults(true) plugin.saveConfig() } + + private fun loadCompatibleGroups(config: FileConfiguration) { + veinMinerCompatibleMaterials.clear() + val groups = config.getList("components.vein_miner.compatible_groups") as? List<*> ?: return + + for (groupObj in groups) { + val group = groupObj as? List<*> ?: continue + val materials = group.mapNotNull { + val name = (it as? String) ?: return@mapNotNull null + try { + org.bukkit.Material.valueOf(name.uppercase().removePrefix("MINECRAFT:")) + } catch (e: IllegalArgumentException) { + null + } + }.toSet() + + for (mat in materials) { + val existing = veinMinerCompatibleMaterials.getOrDefault(mat, emptySet()) + veinMinerCompatibleMaterials[mat] = existing + materials + } + } + } + + private fun loadToolCategories(config: FileConfiguration) { + veinMinerToolCategories.clear() + val list = config.getMapList("components.vein_miner.tool_categories") + + for (map in list) { + val toolPattern = map["tool_pattern"] as? String ?: continue + val allowedBlocks = (map["allowed_blocks"] as? List<*>)?.filterIsInstance() ?: continue + veinMinerToolCategories.add(ToolCategory(toolPattern, allowedBlocks)) + } + } } diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt new file mode 100644 index 0000000..d692ae1 --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/VeinMinerComponent.kt @@ -0,0 +1,311 @@ +package net.hareworks.hcu.items.content.components + +import net.hareworks.hcu.items.api.component.EquippableComponent +import net.hareworks.hcu.items.api.Tier +import net.hareworks.hcu.items.config.Config +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.block.Block +import org.bukkit.entity.Player +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType +import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.inventory.meta.Damageable +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.ExperienceOrb +import org.bukkit.entity.BlockDisplay +import org.bukkit.entity.Display +import org.bukkit.util.Transformation +import org.joml.Vector3f +import org.bukkit.Color +import org.bukkit.Bukkit +import java.util.LinkedList +import java.util.Queue +import java.util.Random +import java.util.UUID + +class VeinMinerComponent(private val plugin: JavaPlugin) : EquippableComponent { + + override val key: NamespacedKey = NamespacedKey(plugin, "vein_miner") + override val displayName: String = "Vein Miner" + override val maxTier: Int = 1 + override val minTier: Int = 1 + + private val activeHighlights = mutableMapOf() + + data class HighlightSession( + val centerBlock: Block, + val entities: List, + val lastUpdate: Long + ) + + override fun apply(item: ItemStack, tier: Tier?) { + val meta = item.itemMeta ?: return + meta.persistentDataContainer.set(key, PersistentDataType.BYTE, 1.toByte()) + item.itemMeta = meta + } + + override fun has(item: ItemStack): Boolean { + if (item.type.isAir) return false + return item.itemMeta?.persistentDataContainer?.has(key, PersistentDataType.BYTE) == true + } + + override fun remove(item: ItemStack) { + val meta = item.itemMeta ?: return + meta.persistentDataContainer.remove(key) + item.itemMeta = meta + } + + override fun onTick(player: Player, item: ItemStack) { + // ハイライト条件チェック (Sneakしながら) + if (!player.isSneaking) { + clearHighlight(player) + return + } + + // ターゲットブロック取得 + val targetBlock = player.getTargetBlockExact(5) + if (targetBlock == null || targetBlock.type == Material.AIR) { + clearHighlight(player) + return + } + + // 既存ハイライトの更新不要チェック + val session = activeHighlights[player.uniqueId] + if (session != null && session.centerBlock == targetBlock) { + return + } + + // ハイライト再生成 + clearHighlight(player) + + // 破壊対象計算 + val blocksToBreak = calculateBreakList(player, targetBlock, item) + if (blocksToBreak.isEmpty()) return + + // ハイライトエンティティ生成 + val entities = mutableListOf() + + // クライアントのパフォーマンスを考慮し、最大ハイライト数を制限しても良いが、 + // ユーザー要望通り全てハイライトする + for (block in blocksToBreak) { + val loc = block.location.add(0.5, 0.5, 0.5) + try { + // BlockDisplay生成 + val display = block.world.spawn(loc, BlockDisplay::class.java) { e -> + e.block = block.blockData + // 少し大きくして元のブロックを覆う + e.transformation = Transformation( + Vector3f(0f, 0f, 0f), + org.joml.AxisAngle4f(0f, 0f, 0f, 0f), + Vector3f(1.01f, 1.01f, 1.01f), + org.joml.AxisAngle4f(0f, 0f, 0f, 0f) + ) + e.isGlowing = true + e.glowColorOverride = Color.AQUA // 鮮やかな色 + e.brightness = Display.Brightness(15, 15) // 最大輝度 + e.isVisibleByDefault = false + } + player.showEntity(plugin, display) + entities.add(display) + } catch (e: Exception) { + // 1.19.4未満などのケース + } + } + + activeHighlights[player.uniqueId] = HighlightSession(targetBlock, entities, System.currentTimeMillis()) + } + + // アイテム持ち替えやスニーク解除時にも消えるように + override fun onUnequip(player: Player, item: ItemStack) { + clearHighlight(player) + } + + private fun clearHighlight(player: Player) { + val session = activeHighlights.remove(player.uniqueId) ?: return + for (e in session.entities) { + e.remove() + } + } + + override fun onBlockBreak(event: BlockBreakEvent) { + if (event.isCancelled) return + val player = event.player + val block = event.block + val item = player.inventory.itemInMainHand + + // 発動条件チェック + if (!shouldActivate(player)) return + + // 破壊対象リスト + val blocksToBreak = calculateBreakList(player, block, item) + + // 単一破壊(連鎖なし)なら何もしない(通常のイベントに任せる) + if (blocksToBreak.size <= 1) return + + // ハイライト消去 + clearHighlight(player) + + val dropLocation = block.location.add(0.5, 0.5, 0.5) + val hasSilkTouch = item.containsEnchantment(Enchantment.SILK_TOUCH) + + var soundPlayed = false + + for (target in blocksToBreak) { + if (target == block) continue // 起点ブロックはイベントフローで破壊される + + // 保護プラグインチェック (擬似イベント) + val checkEvent = BlockBreakEvent(target, player) + Bukkit.getPluginManager().callEvent(checkEvent) + if (checkEvent.isCancelled) continue + + // 演出 (最初の1回だけ、または確率で) + if (!soundPlayed) { + target.world.playSound(target.location, target.blockSoundGroup.breakSound, 1f, 1f) + target.world.spawnParticle(org.bukkit.Particle.BLOCK, target.location.add(0.5,0.5,0.5), 10, 0.3, 0.3, 0.3, target.blockData) + soundPlayed = true + } + + // ドロップ処理 + val drops = target.getDrops(item) + for (drop in drops) { + target.world.dropItem(dropLocation, drop) + } + + // 経験値 + if (!hasSilkTouch) { + val xp = getExpFromBlock(target.type) + if (xp > 0) { + val orb = target.world.spawn(dropLocation, ExperienceOrb::class.java) + orb.experience = xp + } + } + + // ブロック消去 + target.type = Material.AIR + + // 耐久消費 + damageItem(player, item) + } + } + + private fun shouldActivate(player: Player): Boolean { + return when (Config.veinMinerActivationMode) { + "SNEAK" -> player.isSneaking + "STAND" -> !player.isSneaking + "ALWAYS" -> true + else -> player.isSneaking + } + } + + private fun calculateBreakList(player: Player, startBlock: Block, item: ItemStack): Set { + val visited = mutableSetOf() + + // ターゲット適正チェック + if (!isValidTarget(startBlock, item)) return emptySet() + + // 互換ブロックタイプ抽出 + val startType = startBlock.type + val targetMaterials = Config.veinMinerCompatibleMaterials[startType] ?: setOf() + val efficientTargets = if (targetMaterials.isNotEmpty()) targetMaterials else setOf(startType) + + val queue: Queue = LinkedList() + visited.add(startBlock) + queue.add(startBlock) + + val max = Config.veinMinerMaxBlocks + // 見つかったブロックの実リスト + val foundBlocks = mutableSetOf() + foundBlocks.add(startBlock) + + while (queue.isNotEmpty() && foundBlocks.size < max) { + val current = queue.poll() + + // 隣接チェック + for (x in -1..1) { + for (y in -1..1) { + for (z in -1..1) { + if (x == 0 && y == 0 && z == 0) continue + val neighbor = current.getRelative(x, y, z) + + if (visited.contains(neighbor)) continue + + if (efficientTargets.contains(neighbor.type)) { + visited.add(neighbor) + + // まだ破壊できるならQueueに追加 + if (foundBlocks.size < max) { + foundBlocks.add(neighbor) + queue.add(neighbor) + } + } + } + } + } + } + + return foundBlocks + } + + private fun isValidTarget(block: Block, item: ItemStack): Boolean { + if (block.getDrops(item).isEmpty()) return false + + val itemName = item.type.key.toString() + val blockName = block.type.key.toString() + + var isAllowed = false + for (category in Config.veinMinerToolCategories) { + if (itemName.contains(category.toolPattern)) { + for (pattern in category.allowedBlockPatterns) { + if (blockName.contains(pattern)) { + isAllowed = true + break + } + } + } + if (isAllowed) break + } + return isAllowed + } + + private fun damageItem(player: Player, item: ItemStack) { + val meta = item.itemMeta as? Damageable ?: return + val unbreakingLevel = item.getEnchantmentLevel(Enchantment.UNBREAKING) + + if (unbreakingLevel > 0) { + val random = Random() + if (random.nextInt(unbreakingLevel + 1) > 0) { + return + } + } + + val maxDamage = item.type.maxDurability + if (maxDamage > 0) { + val newDamage = meta.damage + 1 + if (newDamage >= maxDamage) { + item.amount = 0 + player.world.playSound(player.location, org.bukkit.Sound.ENTITY_ITEM_BREAK, 1f, 1f) + } else { + meta.damage = newDamage + item.itemMeta = meta + } + } + } + + private fun getExpFromBlock(type: Material): Int { + val random = Random() + return when (type) { + Material.COAL_ORE, Material.DEEPSLATE_COAL_ORE -> random.nextInt(3) + Material.DIAMOND_ORE, Material.DEEPSLATE_DIAMOND_ORE -> random.nextInt(5) + 3 + Material.EMERALD_ORE, Material.DEEPSLATE_EMERALD_ORE -> random.nextInt(5) + 3 + Material.LAPIS_ORE, Material.DEEPSLATE_LAPIS_ORE -> random.nextInt(4) + 2 + Material.NETHER_QUARTZ_ORE -> random.nextInt(4) + 2 + Material.REDSTONE_ORE, Material.DEEPSLATE_REDSTONE_ORE -> random.nextInt(5) + 1 + Material.NETHER_GOLD_ORE -> random.nextInt(2) + Material.SCULK -> 1 + Material.SPAWNER -> random.nextInt(29) + 15 + else -> 0 + } + } +} 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 59fcb69..3edccf8 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -121,6 +121,15 @@ class EventListener(private val plugin: Plugin) : Listener { } } + @EventHandler + fun onBlockBreak(event: org.bukkit.event.block.BlockBreakEvent) { + val player = event.player + val item = player.inventory.itemInMainHand + + // Dispatch to components on the main hand item + dispatchToComponents(item) { it.onBlockBreak(event) } + } + private fun tickComponents() { for (player in plugin.server.onlinePlayers) { val items = getEquipmentItems(player) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 042e9e7..74337b8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -6,3 +6,45 @@ components: power: vertical: 0.5 forward: 0.3 + vein_miner: + max_blocks: 64 + activation_mode: "SNEAK" # SNEAK, ALWAYS, STAND + compatible_groups: + - [ "minecraft:diamond_ore", "minecraft:deepslate_diamond_ore" ] + - [ "minecraft:iron_ore", "minecraft:deepslate_iron_ore" ] + - [ "minecraft:gold_ore", "minecraft:deepslate_gold_ore" ] + - [ "minecraft:copper_ore", "minecraft:deepslate_copper_ore" ] + - [ "minecraft:coal_ore", "minecraft:deepslate_coal_ore" ] + - [ "minecraft:redstone_ore", "minecraft:deepslate_redstone_ore" ] + - [ "minecraft:lapis_ore", "minecraft:deepslate_lapis_ore" ] + - [ "minecraft:emerald_ore", "minecraft:deepslate_emerald_ore" ] + tool_categories: + - tool_pattern: "_pickaxe" + allowed_blocks: + - "_ore" + - "minecraft:ancient_debris" + - "minecraft:amethyst_block" + - "minecraft:budding_amethyst" + - "minecraft:obsidian" + - tool_pattern: "_axe" + allowed_blocks: + - "_log" + - "_stem" + - "_hyphae" + - "minecraft:mangrove_roots" + - "minecraft:bamboo_block" + - tool_pattern: "_shovel" + allowed_blocks: + - "minecraft:clay" + - "minecraft:gravel" + - "minecraft:soul_sand" + - "minecraft:soul_soil" + - "minecraft:mud" + - "minecraft:snow" + - tool_pattern: "_hoe" + allowed_blocks: + - "_leaves" + - "minecraft:nether_wart_block" + - "minecraft:shroomlight" + - "minecraft:hay_block" +