diff --git a/src/main/kotlin/net/hareworks/hcu/items/App.kt b/src/main/kotlin/net/hareworks/hcu/items/App.kt index 19b0968..39c9069 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/App.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/App.kt @@ -18,6 +18,7 @@ import net.hareworks.hcu.items.content.components.AreaTillerComponent import org.bukkit.permissions.PermissionDefault import org.bukkit.plugin.java.JavaPlugin +import net.hareworks.hcu.items.util.ItemActionUtil public class App : JavaPlugin() { @@ -53,4 +54,9 @@ public class App : JavaPlugin() { server.pluginManager.registerEvents(net.hareworks.hcu.items.listeners.EventListener(this), this) } + + override fun onDisable() { + ItemActionUtil.clearAllScrollStates() + logger.info("Items plugin disabled!") + } } diff --git a/src/main/kotlin/net/hareworks/hcu/items/content/components/AreaTillerComponent.kt b/src/main/kotlin/net/hareworks/hcu/items/content/components/AreaTillerComponent.kt index 36ac30c..40128b1 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/components/AreaTillerComponent.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/components/AreaTillerComponent.kt @@ -37,6 +37,7 @@ import java.util.UUID import kotlin.math.max import kotlin.math.min import kotlin.reflect.KClass +import net.hareworks.hcu.items.util.ItemActionUtil /** * AreaTillerComponent - 広範囲一括耕地化コンポーネント @@ -302,39 +303,27 @@ class AreaTillerComponent(private val plugin: JavaPlugin) : ToolComponent { */ private fun handleItemHeld(event: PlayerItemHeldEvent, item: ItemStack) { val player = event.player - - // スニーク中のみ調整 + val session = editSessions.getOrPut(player.uniqueId) { EditSession() } + if (!player.isSneaking) return - val session = editSessions.getOrPut(player.uniqueId) { EditSession() } - val tier = getTier(item) - val maxRange = getMaxRangeForTier(tier) + ItemActionUtil.handleScrollAdjustment(event) { delta -> + val tier = getTier(item) + val maxRange = getMaxRangeForTier(tier) - val now = System.currentTimeMillis() - if (now - session.lastScrollTime < 50) return + // プレイヤーの向きから、どの方角の範囲を伸長するか決定 + val facing = getDirectionFacing(player.location.yaw) - session.lastScrollTime = now + when (facing) { + BlockFace.NORTH -> session.north = (session.north + delta).coerceIn(0, maxRange) + BlockFace.SOUTH -> session.south = (session.south + delta).coerceIn(0, maxRange) + BlockFace.EAST -> session.east = (session.east + delta).coerceIn(0, maxRange) + BlockFace.WEST -> session.west = (session.west + delta).coerceIn(0, maxRange) + else -> {} + } - // スクロール方向を判定 - val diff = event.newSlot - event.previousSlot - val scrollUp = (diff == -1 || diff == 8) - val delta = if (scrollUp) 1 else -1 - - // プレイヤーの向きから、どの方角の範囲を伸長するか決定 - val facing = getDirectionFacing(player.location.yaw) - - when (facing) { - BlockFace.NORTH -> session.north = (session.north + delta).coerceIn(0, maxRange) - BlockFace.SOUTH -> session.south = (session.south + delta).coerceIn(0, maxRange) - BlockFace.EAST -> session.east = (session.east + delta).coerceIn(0, maxRange) - BlockFace.WEST -> session.west = (session.west + delta).coerceIn(0, maxRange) - else -> {} + player.playSound(player.location, Sound.UI_BUTTON_CLICK, 0.3f, 1.2f) } - - player.playSound(player.location, Sound.UI_BUTTON_CLICK, 0.3f, 1.2f) - - // イベントをキャンセルしてスロット切り替えを防ぐ - event.isCancelled = true } /** 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 index 45a2adf..2f2ca34 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/content/items/MagnetItem.kt @@ -13,6 +13,7 @@ import org.bukkit.entity.Item import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.persistence.PersistentDataType +import net.hareworks.hcu.items.util.ItemActionUtil class MagnetItem : AbstractItem("magnet_item") { @@ -109,42 +110,37 @@ class MagnetItem : AbstractItem("magnet_item") { 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))) + ItemActionUtil.handleScrollAdjustment(event) { delta -> + val configInfo = Config.magnet + val tier = getTier(item) + val maxRadius = configInfo.radiusBase + (tier.level * configInfo.radiusPerTier) - // 視覚的なフィードバック (BlockDisplayによるリング表示) - updateRadiusRing(player, currentRadius, tier) + // Get current radius + var currentRadius = item.itemMeta?.persistentDataContainer?.get(KEY_RADIUS, PersistentDataType.DOUBLE) ?: maxRadius + + currentRadius += delta.toDouble() + 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 + player.playSound(player.location, Sound.UI_BUTTON_CLICK, 0.3f, 1.2f) + } } private val activeRings = java.util.concurrent.ConcurrentHashMap() 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 698c57c..5730a48 100644 --- a/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt +++ b/src/main/kotlin/net/hareworks/hcu/items/listeners/EventListener.kt @@ -15,7 +15,9 @@ import org.bukkit.event.entity.ProjectileHitEvent import org.bukkit.event.entity.EntityExplodeEvent import org.bukkit.event.entity.EntityInteractEvent import org.bukkit.event.block.* +import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.inventory.ItemStack +import net.hareworks.hcu.items.util.ItemActionUtil import org.bukkit.plugin.EventExecutor import org.bukkit.plugin.Plugin import org.bukkit.event.EventPriority @@ -120,6 +122,11 @@ class EventListener(private val plugin: Plugin) : Listener { dispatchGlobalToComponents(event) } + @EventHandler + fun onPlayerQuit(event: PlayerQuitEvent) { + ItemActionUtil.clearScrollState(event.player) + } + private fun dispatchGlobalToComponents(event: T) { val eventClass = event.javaClass.kotlin ComponentRegistry.getAll().forEach { component -> diff --git a/src/main/kotlin/net/hareworks/hcu/items/util/ItemActionUtil.kt b/src/main/kotlin/net/hareworks/hcu/items/util/ItemActionUtil.kt new file mode 100644 index 0000000..622f6cf --- /dev/null +++ b/src/main/kotlin/net/hareworks/hcu/items/util/ItemActionUtil.kt @@ -0,0 +1,107 @@ +package net.hareworks.hcu.items.util + +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerItemHeldEvent +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +/** + * アイテム操作に関する共通ユーティリティ + */ +object ItemActionUtil { + // プレイヤーごとのスクロール状態を管理 + private data class ScrollState( + var lastScrollTime: Long = 0L, + var accumulatedDelta: Double = 0.0, + var lastProcessedTime: Long = 0L + ) + + private val scrollStates = ConcurrentHashMap() + + // 設定値 + private const val SCROLL_COOLDOWN_MS = 50L // スクロール間隔(ミリ秒) + private const val ACCELERATION_THRESHOLD = 150L // 加速判定の時間閾値 + private const val DELTA_THRESHOLD = 0.8 // 累積値の閾値 + private const val MAX_ACCUMULATED_DELTA = 5.0 // 最大累積値 + + /** + * マウスホイール(スロット変更)による設定値の調整ロジックを処理します。 + * スクロールの速度に応じて滑らかに、かつ連続スクロール時には加速して調整します。 + * + * @param event スロット変更イベント + * @param onAdjust 調整が有効な場合に呼び出されるコールバック (delta: Int) + */ + fun handleScrollAdjustment(event: PlayerItemHeldEvent, onAdjust: (Int) -> Unit) { + val player = event.player + + // 2. プレイヤーがほぼ静止状態のみ有効 + if (player.velocity.length() > 0.08) return + + // 3. スクロール方向を判定 + val diff = event.newSlot - event.previousSlot + val scrollUp = (diff == -1 || diff == 8) + val rawDelta = if (scrollUp) 1.0 else -1.0 + + // 4. 滑らかなスクロール処理 + val state = scrollStates.computeIfAbsent(player.uniqueId) { ScrollState() } + val currentTime = System.currentTimeMillis() + val timeSinceLastScroll = currentTime - state.lastScrollTime + + // クールダウンチェック(連続スクロールの検出) + if (timeSinceLastScroll < SCROLL_COOLDOWN_MS) { + event.isCancelled = true + return + } + + // 加速度の計算(素早くスクロールすると累積値が増える) + val accelerationFactor = if (timeSinceLastScroll < ACCELERATION_THRESHOLD) { + 1.5 // 素早いスクロールで加速 + } else { + 1.0 + } + + // 累積デルタ値の更新 + state.accumulatedDelta += rawDelta * accelerationFactor + state.lastScrollTime = currentTime + + // 累積値の制限 + state.accumulatedDelta = state.accumulatedDelta.coerceIn( + -MAX_ACCUMULATED_DELTA, + MAX_ACCUMULATED_DELTA + ) + + // 閾値を超えたら実際に値を変更 + val timeSinceLastProcess = currentTime - state.lastProcessedTime + if (kotlin.math.abs(state.accumulatedDelta) >= DELTA_THRESHOLD || + timeSinceLastProcess > 200L) { // または一定時間経過 + + val deltaToApply = state.accumulatedDelta.toInt() + if (deltaToApply != 0) { + onAdjust(deltaToApply) + state.lastProcessedTime = currentTime + state.accumulatedDelta = 0.0 + } + } + + // 5. イベントをキャンセルしてスロット切り替えを防ぐ + event.isCancelled = true + } + + /** + * プレイヤーのスクロール状態をクリアします。 + * プレイヤーがログアウトした際などに呼び出してください。 + * + * @param player 対象プレイヤー + */ + fun clearScrollState(player: Player) { + scrollStates.remove(player.uniqueId) + } + + /** + * 全てのスクロール状態をクリアします。 + * プラグインの無効化時などに呼び出してください。 + */ + fun clearAllScrollStates() { + scrollStates.clear() + } +}