feat: Add Magnet Item with configurable attraction radius and scroll adjustment.

This commit is contained in:
Kariya 2025-12-17 11:19:50 +00:00
parent 0e72f24164
commit fb7ae54875
7 changed files with 330 additions and 0 deletions

View File

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

View File

@ -29,4 +29,5 @@ interface CustomComponent {
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) {}
fun onItemHeld(player: org.bukkit.entity.Player, item: org.bukkit.inventory.ItemStack, event: org.bukkit.event.player.PlayerItemHeldEvent) {}
}

View File

@ -16,4 +16,6 @@ interface CustomItem {
fun onProjectileLaunch(event: org.bukkit.event.entity.ProjectileLaunchEvent) {}
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 onTick(player: org.bukkit.entity.Player, item: ItemStack) {}
}

View File

@ -20,6 +20,7 @@ object Config {
var blink = BlinkSettings()
var doubleJump = DoubleJumpSettings()
var veinMiner = VeinMinerSettings()
var magnet = MagnetSettings()
/**
* グローバル設定
@ -44,6 +45,15 @@ object Config {
var powerForward: Double = 0.3
)
/**
* Magnetコンポーネント設定
*/
data class MagnetSettings(
var radiusBase: Double = 5.0,
var radiusPerTier: Double = 5.0,
var allowScrollChange: Boolean = true
)
/**
* VeinMinerコンポーネント設定
*/
@ -89,6 +99,7 @@ object Config {
loadBlinkSettings(config)
loadDoubleJumpSettings(config)
loadVeinMinerSettings(plugin, config)
loadMagnetSettings(config)
saveDefaults(plugin, config)
}
@ -147,6 +158,17 @@ object Config {
loadToolCategories(config, veinMiner.toolCategories)
}
/**
* Magnet設定を読み込む
*/
private fun loadMagnetSettings(config: FileConfiguration) {
magnet = MagnetSettings(
radiusBase = config.getDouble("components.magnet.radius_base", 5.0),
radiusPerTier = config.getDouble("components.magnet.radius_per_tier", 5.0),
allowScrollChange = config.getBoolean("components.magnet.allow_scroll_change", true)
)
}
/**
* デフォルト設定を保存する
*/
@ -158,6 +180,9 @@ object Config {
config.addDefault("components.double_jump.power.forward", 0.3)
config.addDefault("components.vein_miner.max_blocks", 64)
config.addDefault("components.vein_miner.activation_mode", "SNEAK")
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.options().copyDefaults(true)
plugin.saveConfig()

View File

@ -0,0 +1,283 @@
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.NamespacedKey
import org.bukkit.Sound
import org.bukkit.entity.ExperienceOrb
import org.bukkit.entity.Item
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType
class MagnetItem : AbstractItem("magnet_item") {
override val maxTier: Int = 3
companion object {
val KEY_RADIUS = NamespacedKey("hcu_items", "magnet_radius")
}
override fun buildItem(tier: Tier): ItemStack {
return ItemStack(Material.IRON_NUGGET).apply {
val meta = itemMeta ?: return@apply
meta.displayName(Component.text("マグネットグローブ", tier.color))
val configInfo = Config.magnet
val maxRadius = configInfo.radiusBase + (tier.level * configInfo.radiusPerTier)
// 星(強さの表現)
val powerStars = "".repeat(tier.level) + "".repeat(3 - tier.level) // Tier上限3なので3つ
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("吸引力: $powerStars", NamedTextColor.AQUA),
Component.text("最大範囲: ${maxRadius}m", NamedTextColor.GREEN),
Component.empty(),
Component.text("メインハンドで[Shift]+[Scroll]", NamedTextColor.YELLOW),
Component.text("回収範囲を調整", NamedTextColor.YELLOW)
))
itemMeta = meta
}
}
override fun onTick(player: Player, item: ItemStack) {
// オフハンドにある時のみ機能する
if (!item.isSimilar(player.inventory.itemInOffHand)) return
val tier = getTier(item)
val configInfo = Config.magnet
// Calculate max radius based on Tier
val maxRadius = configInfo.radiusBase + (tier.level * configInfo.radiusPerTier)
// Get current set radius from PDC, default to max
val currentRadius = item.itemMeta?.persistentDataContainer?.get(KEY_RADIUS, PersistentDataType.DOUBLE) ?: maxRadius
val actualRadius = currentRadius.coerceAtMost(maxRadius)
// Pull implementation
val location = player.location.add(0.0, 1.0, 0.0) // Check from player center
val entities = location.world.getNearbyEntities(location, actualRadius, actualRadius, actualRadius)
var pulledAny = false
for (entity in entities) {
if (entity is Item || entity is ExperienceOrb) {
// Ignore items with pickup delay > 20 (newly dropped)
if (entity is Item && entity.pickupDelay > 20) continue
// Direction to player
val direction = location.clone().subtract(entity.location).toVector()
val distance = direction.length()
val force = direction.normalize().multiply(0.3)
entity.velocity = entity.velocity.add(force)
pulledAny = true
// 吸引エフェクト
player.spawnParticle(
org.bukkit.Particle.END_ROD,
entity.location,
1,
0.0, 0.0, 0.0,
0.5,
null
)
}
}
}
override fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {
val player = event.player
val item = player.inventory.getItem(event.previousSlot) ?: return
if (!Config.magnet.allowScrollChange) return
if (!player.isSneaking) return
// 動いていない時のみ調整可能
if (player.velocity.length() > 0.08) return // 完全な0は難しいため閾値を設定
val configInfo = Config.magnet
val tier = getTier(item)
val maxRadius = configInfo.radiusBase + (tier.level * configInfo.radiusPerTier)
val diff = event.newSlot - event.previousSlot
val scrollUp = (diff == -1 || diff == 8)
val change = if (scrollUp) 1.0 else -1.0
// Get current radius
var currentRadius = item.itemMeta?.persistentDataContainer?.get(KEY_RADIUS, PersistentDataType.DOUBLE) ?: maxRadius
currentRadius += change
currentRadius = currentRadius.coerceIn(1.0, maxRadius)
// Save to PDC
val meta = item.itemMeta
meta.persistentDataContainer.set(KEY_RADIUS, PersistentDataType.DOUBLE, currentRadius)
item.itemMeta = meta
// Feedback
player.sendActionBar(Component.text("回収範囲: ", NamedTextColor.AQUA)
.append(Component.text(String.format("%.1f", currentRadius) + "m", NamedTextColor.YELLOW))
.append(Component.text(" | ", NamedTextColor.DARK_GRAY))
.append(Component.text("最大: ", NamedTextColor.GRAY))
.append(Component.text(String.format("%.1f", maxRadius) + "m", NamedTextColor.WHITE)))
// 視覚的なフィードバック (BlockDisplayによるリング表示)
updateRadiusRing(player, currentRadius, tier)
// Cancel event to prevent slot switch
event.isCancelled = true
}
private val activeRings = java.util.concurrent.ConcurrentHashMap<java.util.UUID, RingSession>()
private data class RingSession(
val entities: List<org.bukkit.entity.BlockDisplay>,
var task: org.bukkit.scheduler.BukkitTask,
var radius: Double,
var tierMaterial: Material,
var lastKeepAlive: Long
)
private fun updateRadiusRing(player: Player, radius: Double, tier: Tier) {
val tierMaterial = getTierGlassMaterial(tier)
val session = activeRings[player.uniqueId]
if (session != null && session.entities.all { it.isValid }) {
// セッション更新
session.radius = radius
session.tierMaterial = tierMaterial
session.lastKeepAlive = System.currentTimeMillis()
// 色が変わった場合はブロック更新
// ※Tierが変わるとMaterialが変わるため
session.entities.forEach {
it.block = org.bukkit.Bukkit.createBlockData(tierMaterial)
}
// 即座に位置更新(スクロールの反応性を良くするため)
updateRingTransformations(player, radius, session.entities)
} else {
// クリーンアップ(念のため)
session?.entities?.forEach { it.remove() }
session?.task?.cancel()
// 新規作成
val entities = createRingEntities(player, radius, tierMaterial)
val task = startRingTask(player)
activeRings[player.uniqueId] = RingSession(entities, task, radius, tierMaterial, System.currentTimeMillis())
}
}
private fun startRingTask(player: Player): org.bukkit.scheduler.BukkitTask {
return org.bukkit.Bukkit.getScheduler().runTaskTimer(net.hareworks.hcu.items.App.instance, Runnable {
val session = activeRings[player.uniqueId]
if (session == null || !player.isOnline || session.entities.any { !it.isValid }) {
session?.entities?.forEach { it.remove() }
activeRings.remove(player.uniqueId)
return@Runnable
}
// タイムアウトチェック (3秒)
if (System.currentTimeMillis() - session.lastKeepAlive > 3000) {
session.entities.forEach { it.remove() }
session.task.cancel()
activeRings.remove(player.uniqueId)
return@Runnable
}
// プレイヤーの位置に合わせて追従
updateRingTransformations(player, session.radius, session.entities)
}, 0L, 1L) // 毎実行
}
private fun createRingEntities(player: Player, radius: Double, material: Material): List<org.bukkit.entity.BlockDisplay> {
val center = player.location
val segments = 64 // 分割数を増やして滑らかに
val entities = mutableListOf<org.bukkit.entity.BlockDisplay>()
for (i in 0 until segments) {
val display = player.world.spawn(center, org.bukkit.entity.BlockDisplay::class.java) { e ->
e.block = org.bukkit.Bukkit.createBlockData(material)
e.brightness = org.bukkit.entity.Display.Brightness(15, 15)
e.isPersistent = false // セーブしない
e.setGravity(false)
e.isVisibleByDefault = false // デフォルトでは見えない
}
player.showEntity(net.hareworks.hcu.items.App.instance, display) // プレイヤーにだけ見せる
entities.add(display)
}
// 初期配置
updateRingTransformations(player, radius, entities)
return entities
}
private fun updateRingTransformations(player: Player, radius: Double, entities: List<org.bukkit.entity.BlockDisplay>) {
val center = player.location.clone().add(0.0, 0.1, 0.0) // 足元少し上
val segments = entities.size
val angleStep = 2.0 * Math.PI / segments
// 滑らかにつなぐための長さ計算(弦の長さ)
val segmentLength = 2.0 * radius * Math.sin(Math.PI / segments)
// 線の太さ
val thickness = 0.05f
for (i in 0 until segments) {
val entity = entities[i]
val angle = i * angleStep
// 配置位置
val x = radius * Math.cos(angle)
val z = radius * Math.sin(angle)
val loc = center.clone().add(x, 0.0, z)
// 向きの設定
val nextAngle = angle + angleStep
val nextX = radius * Math.cos(nextAngle)
val nextZ = radius * Math.sin(nextAngle)
val dir = org.bukkit.util.Vector(nextX - x, 0.0, nextZ - z)
loc.direction = dir
entity.teleport(loc)
// Transformation設定: Z軸方向視線方向に伸ばしてつなげる
val scaleX = thickness
val scaleY = thickness
val scaleZ = segmentLength.toFloat() * 1.05f // 隙間防止のため少し長めに
// X, Yの中心を合わせて、Z方向前方へ伸ばす
val transformation = org.joml.Matrix4f()
.translate(-thickness / 2, -thickness / 2, 0f)
.scale(scaleX, scaleY, scaleZ)
entity.setTransformationMatrix(transformation)
}
}
private fun getTierGlassMaterial(tier: Tier): Material {
return when ((tier.level - 1) % 5 + 1) {
1 -> Material.WHITE_STAINED_GLASS
2 -> Material.LIME_STAINED_GLASS
3 -> Material.LIGHT_BLUE_STAINED_GLASS
4 -> Material.MAGENTA_STAINED_GLASS
5 -> Material.YELLOW_STAINED_GLASS
else -> Material.WHITE_STAINED_GLASS
}
}
}

View File

@ -137,6 +137,15 @@ class EventListener(private val plugin: Plugin) : Listener {
dispatchToComponents(item) { it.onBlockBreak(event) }
}
@EventHandler
fun onItemHeld(event: org.bukkit.event.player.PlayerItemHeldEvent) {
val player = event.player
val item = player.inventory.getItem(event.previousSlot) ?: return
dispatchToItem(item) { it.onItemHeld(event) }
dispatchToComponents(item) { it.onItemHeld(player, item, event) }
}
private fun tickComponents() {
for (player in plugin.server.onlinePlayers) {
val uuid = player.uniqueId
@ -166,6 +175,7 @@ class EventListener(private val plugin: Plugin) : Listener {
}
}
// Update Cache
// Update Cache
lastHeldItems[uuid] = Pair(currentMainHand, currentOffHand)
@ -175,6 +185,7 @@ class EventListener(private val plugin: Plugin) : Listener {
val armorItems = player.inventory.armorContents.filterNotNull()
for (item in armorItems) {
if (item.type.isAir) continue
dispatchToItem(item) { it.onTick(player, item) }
dispatchToComponents(item) { component ->
if (component is EquippableComponent) {
component.onTick(player, item)
@ -184,6 +195,7 @@ 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)
@ -196,6 +208,7 @@ 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)

View File

@ -6,6 +6,10 @@ components:
power:
vertical: 0.5
forward: 0.3
magnet:
radius_base: 5.0
radius_per_tier: 5.0
allow_scroll_change: true
vein_miner:
max_blocks: 64
activation_mode: "SNEAK" # SNEAK, ALWAYS, STAND