From fb7ae548755e0638abd17bbcbabba431fbe27494 Mon Sep 17 00:00:00 2001 From: Kariya Date: Wed, 17 Dec 2025 11:19:50 +0000 Subject: [PATCH] feat: Add Magnet Item with configurable attraction radius and scroll adjustment. --- .../kotlin/net/hareworks/hcu/items/App.kt | 2 + .../items/api/component/CustomComponent.kt | 1 + .../hcu/items/api/item/CustomItem.kt | 2 + .../net/hareworks/hcu/items/config/Config.kt | 25 ++ .../hcu/items/content/items/MagnetItem.kt | 283 ++++++++++++++++++ .../hcu/items/listeners/EventListener.kt | 13 + src/main/resources/config.yml | 4 + 7 files changed, 330 insertions(+) create mode 100644 src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt diff --git a/src/main/kotlin/net/hareworks/hcu/items/App.kt b/src/main/kotlin/net/hareworks/hcu/items/App.kt index 067cf7c..10b2147 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/App.kt @@ -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)) 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 8754539..4d43783 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 @@ -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) {} } diff --git a/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt b/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt index d847a51..0a14a52 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/api/item/CustomItem.kt @@ -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) {} } 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 64abc74..9eea871 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/config/Config.kt @@ -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() diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt b/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt new file mode 100644 index 0000000..09452e5 --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt @@ -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() + + private data class RingSession( + val entities: List, + 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 { + val center = player.location + val segments = 64 // 分割数を増やして滑らかに + val entities = mutableListOf() + + 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) { + 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 + } + } +} 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 70ab5de..e77147a 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -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) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 345125b..faf8146 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -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