feat: Implement Vein Miner component with configurable multi-block breaking, compatible material groups, tool categories, and visual highlighting.

This commit is contained in:
Kariya 2025-12-16 11:57:26 +00:00
parent f1787b9b62
commit f45947b9e9
6 changed files with 416 additions and 0 deletions

View File

@ -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.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
import net.hareworks.hcu.items.content.components.VeinMinerComponent
import org.bukkit.permissions.PermissionDefault import org.bukkit.permissions.PermissionDefault
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -43,6 +44,7 @@ public class App : JavaPlugin() {
ComponentRegistry.register(GliderComponent(this)) ComponentRegistry.register(GliderComponent(this))
ComponentRegistry.register(DoubleJumpComponent(this)) ComponentRegistry.register(DoubleJumpComponent(this))
ComponentRegistry.register(BlinkComponent(this)) ComponentRegistry.register(BlinkComponent(this))
ComponentRegistry.register(VeinMinerComponent(this))
} }
} }

View File

@ -28,4 +28,5 @@ interface CustomComponent {
fun onPlayerMove(event: org.bukkit.event.player.PlayerMoveEvent) {} 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 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 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) {}
} }

View File

@ -10,6 +10,13 @@ object Config {
var doubleJumpCooldown: Long = 1500 var doubleJumpCooldown: Long = 1500
var doubleJumpPowerVertical: Double = 0.5 var doubleJumpPowerVertical: Double = 0.5
var doubleJumpPowerForward: Double = 0.3 var doubleJumpPowerForward: Double = 0.3
// Vein Miner
data class ToolCategory(val toolPattern: String, val allowedBlockPatterns: List<String>)
var veinMinerMaxBlocks: Int = 64
var veinMinerActivationMode: String = "SNEAK" // "SNEAK", "ALWAYS", "STAND"
val veinMinerCompatibleMaterials: MutableMap<org.bukkit.Material, Set<org.bukkit.Material>> = mutableMapOf()
val veinMinerToolCategories: MutableList<ToolCategory> = mutableListOf()
fun load(plugin: JavaPlugin) { fun load(plugin: JavaPlugin) {
plugin.reloadConfig() plugin.reloadConfig()
@ -23,6 +30,12 @@ object Config {
doubleJumpPowerVertical = config.getDouble("components.double_jump.power.vertical", 0.5) doubleJumpPowerVertical = config.getDouble("components.double_jump.power.vertical", 0.5)
doubleJumpPowerForward = config.getDouble("components.double_jump.power.forward", 0.3) 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 // Save back to ensure defaults are written if missing
saveDefaults(plugin, config) saveDefaults(plugin, config)
} }
@ -33,7 +46,45 @@ object Config {
config.addDefault("components.double_jump.power.vertical", 0.5) config.addDefault("components.double_jump.power.vertical", 0.5)
config.addDefault("components.double_jump.power.forward", 0.3) 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) config.options().copyDefaults(true)
plugin.saveConfig() 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<String>() ?: continue
veinMinerToolCategories.add(ToolCategory(toolPattern, allowedBlocks))
}
}
} }

View File

@ -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<UUID, HighlightSession>()
data class HighlightSession(
val centerBlock: Block,
val entities: List<BlockDisplay>,
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<BlockDisplay>()
// クライアントのパフォーマンスを考慮し、最大ハイライト数を制限しても良いが、
// ユーザー要望通り全てハイライトする
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<Block> {
val visited = mutableSetOf<Block>()
// ターゲット適正チェック
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<Block> = LinkedList()
visited.add(startBlock)
queue.add(startBlock)
val max = Config.veinMinerMaxBlocks
// 見つかったブロックの実リスト
val foundBlocks = mutableSetOf<Block>()
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
}
}
}

View File

@ -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() { private fun tickComponents() {
for (player in plugin.server.onlinePlayers) { for (player in plugin.server.onlinePlayers) {
val items = getEquipmentItems(player) val items = getEquipmentItems(player)

View File

@ -6,3 +6,45 @@ components:
power: power:
vertical: 0.5 vertical: 0.5
forward: 0.3 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"