feat: Add Area Tiller component with configurable range and global block event handling.
This commit is contained in:
parent
483493bb87
commit
3b62b26273
|
|
@ -14,6 +14,7 @@ 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 net.hareworks.hcu.items.content.components.VeinMinerComponent
|
||||||
|
import net.hareworks.hcu.items.content.components.AreaTillerComponent
|
||||||
|
|
||||||
import org.bukkit.permissions.PermissionDefault
|
import org.bukkit.permissions.PermissionDefault
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
@ -48,6 +49,7 @@ public class App : JavaPlugin() {
|
||||||
ComponentRegistry.register(DoubleJumpComponent(this))
|
ComponentRegistry.register(DoubleJumpComponent(this))
|
||||||
ComponentRegistry.register(BlinkComponent(this))
|
ComponentRegistry.register(BlinkComponent(this))
|
||||||
ComponentRegistry.register(VeinMinerComponent(this))
|
ComponentRegistry.register(VeinMinerComponent(this))
|
||||||
|
ComponentRegistry.register(AreaTillerComponent(this))
|
||||||
|
|
||||||
server.pluginManager.registerEvents(net.hareworks.hcu.items.listeners.EventListener(this), this)
|
server.pluginManager.registerEvents(net.hareworks.hcu.items.listeners.EventListener(this), this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ object Config {
|
||||||
var blink = BlinkSettings()
|
var blink = BlinkSettings()
|
||||||
var doubleJump = DoubleJumpSettings()
|
var doubleJump = DoubleJumpSettings()
|
||||||
var veinMiner = VeinMinerSettings()
|
var veinMiner = VeinMinerSettings()
|
||||||
|
var areaTiller = AreaTillerSettings()
|
||||||
var magnet = MagnetSettings()
|
var magnet = MagnetSettings()
|
||||||
var autoFeeder = AutoFeederSettings()
|
var autoFeeder = AutoFeederSettings()
|
||||||
|
|
||||||
|
|
@ -62,6 +63,14 @@ object Config {
|
||||||
var hungerThreshold: Int = 10
|
var hungerThreshold: Int = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AreaTillerコンポーネント設定
|
||||||
|
*/
|
||||||
|
data class AreaTillerSettings(
|
||||||
|
var maxRangePerTier: Map<Int, Int> = mapOf(1 to 4, 2 to 5, 3 to 4, 4 to 5),
|
||||||
|
var defaultRange: Int = 3
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VeinMinerコンポーネント設定
|
* VeinMinerコンポーネント設定
|
||||||
*/
|
*/
|
||||||
|
|
@ -108,6 +117,7 @@ object Config {
|
||||||
loadDoubleJumpSettings(config)
|
loadDoubleJumpSettings(config)
|
||||||
loadVeinMinerSettings(plugin, config)
|
loadVeinMinerSettings(plugin, config)
|
||||||
loadMagnetSettings(config)
|
loadMagnetSettings(config)
|
||||||
|
loadAreaTillerSettings(config)
|
||||||
loadAutoFeederSettings(config)
|
loadAutoFeederSettings(config)
|
||||||
|
|
||||||
saveDefaults(plugin, config)
|
saveDefaults(plugin, config)
|
||||||
|
|
@ -187,6 +197,33 @@ object Config {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AreaTiller設定を読み込む
|
||||||
|
*/
|
||||||
|
private fun loadAreaTillerSettings(config: FileConfiguration) {
|
||||||
|
val rangeMap = mutableMapOf<Int, Int>()
|
||||||
|
val section = config.getConfigurationSection("components.area_tiller.max_range_per_tier")
|
||||||
|
if (section != null) {
|
||||||
|
for (key in section.getKeys(false)) {
|
||||||
|
key.toIntOrNull()?.let { tier ->
|
||||||
|
rangeMap[tier] = section.getInt(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback defaults if empty
|
||||||
|
if (rangeMap.isEmpty()) {
|
||||||
|
rangeMap[1] = 4
|
||||||
|
rangeMap[2] = 5
|
||||||
|
rangeMap[3] = 4
|
||||||
|
rangeMap[4] = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
areaTiller = AreaTillerSettings(
|
||||||
|
maxRangePerTier = rangeMap,
|
||||||
|
defaultRange = config.getInt("components.area_tiller.default_range", 3)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* デフォルト設定を保存する
|
* デフォルト設定を保存する
|
||||||
*/
|
*/
|
||||||
|
|
@ -201,6 +238,13 @@ object Config {
|
||||||
config.addDefault("components.magnet.radius_base", 5.0)
|
config.addDefault("components.magnet.radius_base", 5.0)
|
||||||
config.addDefault("components.magnet.radius_per_tier", 5.0)
|
config.addDefault("components.magnet.radius_per_tier", 5.0)
|
||||||
config.addDefault("components.magnet.allow_scroll_change", true)
|
config.addDefault("components.magnet.allow_scroll_change", true)
|
||||||
|
|
||||||
|
config.addDefault("components.area_tiller.default_range", 3)
|
||||||
|
config.addDefault("components.area_tiller.max_range_per_tier.1", 4)
|
||||||
|
config.addDefault("components.area_tiller.max_range_per_tier.2", 5)
|
||||||
|
config.addDefault("components.area_tiller.max_range_per_tier.3", 4)
|
||||||
|
config.addDefault("components.area_tiller.max_range_per_tier.4", 5)
|
||||||
|
|
||||||
config.addDefault("items.auto_feeder.hunger_threshold", 10)
|
config.addDefault("items.auto_feeder.hunger_threshold", 10)
|
||||||
|
|
||||||
config.options().copyDefaults(true)
|
config.options().copyDefaults(true)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,599 @@
|
||||||
|
package net.hareworks.hcu.items.content.components
|
||||||
|
|
||||||
|
import net.hareworks.hcu.items.api.component.ToolComponent
|
||||||
|
import net.hareworks.hcu.items.api.Tier
|
||||||
|
import net.hareworks.hcu.items.config.Config
|
||||||
|
import net.hareworks.hcu.items.events.ComponentEventHandler
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.NamespacedKey
|
||||||
|
import org.bukkit.block.Block
|
||||||
|
import org.bukkit.block.BlockFace
|
||||||
|
import org.bukkit.block.data.type.Farmland
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.entity.BlockDisplay
|
||||||
|
import org.bukkit.entity.Display
|
||||||
|
import org.bukkit.event.Event
|
||||||
|
import org.bukkit.event.entity.EntityExplodeEvent
|
||||||
|
import org.bukkit.event.entity.EntityInteractEvent
|
||||||
|
import org.bukkit.event.block.*
|
||||||
|
import org.bukkit.event.player.PlayerItemHeldEvent
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent
|
||||||
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
import org.bukkit.util.Transformation
|
||||||
|
import org.bukkit.Color
|
||||||
|
import org.bukkit.Particle
|
||||||
|
import org.bukkit.Sound
|
||||||
|
import org.bukkit.Tag
|
||||||
|
import org.bukkit.GameMode
|
||||||
|
import org.bukkit.event.EventHandler
|
||||||
|
import org.bukkit.event.Listener
|
||||||
|
import org.bukkit.event.block.MoistureChangeEvent
|
||||||
|
import org.joml.Vector3f
|
||||||
|
import org.joml.AxisAngle4f
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AreaTillerComponent - 広範囲一括耕地化コンポーネント
|
||||||
|
*
|
||||||
|
* クワを使用時に広範囲の土ブロックを一度に耕地化する。
|
||||||
|
* Tier3以上では、水なしでも湿った状態を保つ特殊な耕地を生成する。
|
||||||
|
*/
|
||||||
|
class AreaTillerComponent(private val plugin: JavaPlugin) : ToolComponent {
|
||||||
|
|
||||||
|
override val key: NamespacedKey = NamespacedKey(plugin, "area_tiller")
|
||||||
|
override val displayName: String = "Area Tiller"
|
||||||
|
override val maxTier: Int = 4
|
||||||
|
override val minTier: Int = 1
|
||||||
|
|
||||||
|
// 耕地化対象ブロック
|
||||||
|
private val tillableBlocks = setOf(
|
||||||
|
Material.DIRT,
|
||||||
|
Material.GRASS_BLOCK,
|
||||||
|
Material.DIRT_PATH,
|
||||||
|
Material.COARSE_DIRT,
|
||||||
|
Material.ROOTED_DIRT
|
||||||
|
)
|
||||||
|
|
||||||
|
// クワのマテリアル
|
||||||
|
private val hoeTag = Tag.ITEMS_HOES
|
||||||
|
|
||||||
|
// 編集モードセッション管理
|
||||||
|
private val editSessions = mutableMapOf<UUID, EditSession>()
|
||||||
|
|
||||||
|
// ハイライトセッション管理
|
||||||
|
private val activeHighlights = mutableMapOf<UUID, HighlightSession>()
|
||||||
|
|
||||||
|
// 再帰防止フラグ(広範囲耕地化中のプレイヤーを追跡)
|
||||||
|
private val processingPlayers = mutableSetOf<UUID>()
|
||||||
|
|
||||||
|
// 永続的な湿った耕地のためのキー
|
||||||
|
private val moistKey = NamespacedKey(plugin, "eternally_moist")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var supportsBlockDisplay: Boolean = false
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
Class.forName("org.bukkit.entity.BlockDisplay")
|
||||||
|
supportsBlockDisplay = true
|
||||||
|
} catch (e: ClassNotFoundException) {
|
||||||
|
supportsBlockDisplay = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 編集セッションデータ (4方向個別の範囲)
|
||||||
|
*/
|
||||||
|
data class EditSession(
|
||||||
|
var north: Int = 1,
|
||||||
|
var south: Int = 1,
|
||||||
|
var east: Int = 1,
|
||||||
|
var west: Int = 1,
|
||||||
|
var lastScrollTime: Long = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ハイライトセッションデータ
|
||||||
|
*/
|
||||||
|
data class HighlightSession(
|
||||||
|
val centerBlock: Block,
|
||||||
|
val entities: List<BlockDisplay>,
|
||||||
|
val lastUpdate: Long,
|
||||||
|
val north: Int,
|
||||||
|
val south: Int,
|
||||||
|
val east: Int,
|
||||||
|
val west: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getEventHandlers(): Map<KClass<out Event>, ComponentEventHandler<*>> {
|
||||||
|
return mapOf(
|
||||||
|
PlayerInteractEvent::class to { event, item ->
|
||||||
|
handleInteract(event as PlayerInteractEvent, item)
|
||||||
|
},
|
||||||
|
PlayerItemHeldEvent::class to { event, item ->
|
||||||
|
handleItemHeld(event as PlayerItemHeldEvent, item)
|
||||||
|
},
|
||||||
|
MoistureChangeEvent::class to { event, _ ->
|
||||||
|
handleMoistureChange(event as MoistureChangeEvent)
|
||||||
|
},
|
||||||
|
BlockBreakEvent::class to { event, _ ->
|
||||||
|
handleGenericBlockCleanup(event as BlockBreakEvent)
|
||||||
|
},
|
||||||
|
BlockExplodeEvent::class to { event, _ ->
|
||||||
|
handleBlockExplodeCleanup(event as BlockExplodeEvent)
|
||||||
|
},
|
||||||
|
EntityExplodeEvent::class to { event, _ ->
|
||||||
|
handleEntityExplodeCleanup(event as EntityExplodeEvent)
|
||||||
|
},
|
||||||
|
EntityInteractEvent::class to { event, _ ->
|
||||||
|
handleEntityInteractCleanup(event as EntityInteractEvent)
|
||||||
|
},
|
||||||
|
BlockFadeEvent::class to { event, _ ->
|
||||||
|
handleGenericBlockCleanup(event as BlockFadeEvent)
|
||||||
|
},
|
||||||
|
BlockFromToEvent::class to { event, _ ->
|
||||||
|
handleBlockFromToCleanup(event as BlockFromToEvent)
|
||||||
|
},
|
||||||
|
BlockPistonExtendEvent::class to { event, _ ->
|
||||||
|
handlePistonExtendCleanup(event as BlockPistonExtendEvent)
|
||||||
|
},
|
||||||
|
BlockPistonRetractEvent::class to { event, _ ->
|
||||||
|
handlePistonRetractCleanup(event as BlockPistonRetractEvent)
|
||||||
|
},
|
||||||
|
BlockPlaceEvent::class to { event, _ ->
|
||||||
|
handleGenericBlockCleanup(event as BlockPlaceEvent)
|
||||||
|
},
|
||||||
|
BlockBurnEvent::class to { event, _ ->
|
||||||
|
handleGenericBlockCleanup(event as BlockBurnEvent)
|
||||||
|
},
|
||||||
|
BlockPhysicsEvent::class to { event, _ ->
|
||||||
|
handleBlockPhysicsCleanup(event as BlockPhysicsEvent)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun apply(item: ItemStack, tier: Tier?) {
|
||||||
|
val meta = item.itemMeta ?: return
|
||||||
|
val tierValue = tier?.level ?: 1
|
||||||
|
meta.persistentDataContainer.set(key, PersistentDataType.INTEGER, tierValue)
|
||||||
|
item.itemMeta = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun has(item: ItemStack): Boolean {
|
||||||
|
if (item.type.isAir) return false
|
||||||
|
return item.itemMeta?.persistentDataContainer?.has(key, PersistentDataType.INTEGER) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(item: ItemStack) {
|
||||||
|
val meta = item.itemMeta ?: return
|
||||||
|
meta.persistentDataContainer.remove(key)
|
||||||
|
item.itemMeta = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アイテムからティア値を取得
|
||||||
|
*/
|
||||||
|
private fun getTier(item: ItemStack): Int {
|
||||||
|
return item.itemMeta?.persistentDataContainer?.get(key, PersistentDataType.INTEGER) ?: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ティアに応じた最大範囲を取得
|
||||||
|
*/
|
||||||
|
private fun getMaxRangeForTier(tier: Int): Int {
|
||||||
|
return Config.areaTiller.maxRangePerTier[tier] ?: Config.areaTiller.defaultRange
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHoldTick(player: Player, item: ItemStack) {
|
||||||
|
val targetBlock = player.getTargetBlockExact(5)
|
||||||
|
|
||||||
|
// ターゲットブロックがない、または耕地化不可の場合はハイライト消去
|
||||||
|
if (targetBlock == null || !tillableBlocks.contains(targetBlock.type)) {
|
||||||
|
clearHighlight(player)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val session = editSessions.getOrPut(player.uniqueId) { EditSession() }
|
||||||
|
val tier = getTier(item)
|
||||||
|
val maxRange = getMaxRangeForTier(tier)
|
||||||
|
|
||||||
|
// 範囲を制限内に収める
|
||||||
|
val n = session.north.coerceIn(0, maxRange)
|
||||||
|
val s = session.south.coerceIn(0, maxRange)
|
||||||
|
val e = session.east.coerceIn(0, maxRange)
|
||||||
|
val w = session.west.coerceIn(0, maxRange)
|
||||||
|
|
||||||
|
// 編集モードチェック(スニーク中)
|
||||||
|
val isEditMode = player.isSneaking
|
||||||
|
|
||||||
|
// 編集モード中はどの方角を調整しているか特定
|
||||||
|
val facing = if (isEditMode) getDirectionFacing(player.location.yaw) else null
|
||||||
|
|
||||||
|
// 編集モード中は軸情報をアクションバーに表示
|
||||||
|
if (isEditMode && facing != null) {
|
||||||
|
player.sendActionBar(
|
||||||
|
net.kyori.adventure.text.Component.text()
|
||||||
|
.append(net.kyori.adventure.text.Component.text("範囲: ", net.kyori.adventure.text.format.NamedTextColor.GRAY))
|
||||||
|
.append(net.kyori.adventure.text.Component.text("北:$n ", if(facing == BlockFace.NORTH) net.kyori.adventure.text.format.NamedTextColor.YELLOW else net.kyori.adventure.text.format.NamedTextColor.WHITE))
|
||||||
|
.append(net.kyori.adventure.text.Component.text("南:$s ", if(facing == BlockFace.SOUTH) net.kyori.adventure.text.format.NamedTextColor.YELLOW else net.kyori.adventure.text.format.NamedTextColor.WHITE))
|
||||||
|
.append(net.kyori.adventure.text.Component.text("東:$e ", if(facing == BlockFace.EAST) net.kyori.adventure.text.format.NamedTextColor.YELLOW else net.kyori.adventure.text.format.NamedTextColor.WHITE))
|
||||||
|
.append(net.kyori.adventure.text.Component.text("西:$w ", if(facing == BlockFace.WEST) net.kyori.adventure.text.format.NamedTextColor.YELLOW else net.kyori.adventure.text.format.NamedTextColor.WHITE))
|
||||||
|
.append(net.kyori.adventure.text.Component.text("(方向を向いてスクロールで調整)", net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ハイライト更新チェック
|
||||||
|
val highlight = activeHighlights[player.uniqueId]
|
||||||
|
if (highlight != null &&
|
||||||
|
highlight.centerBlock == targetBlock &&
|
||||||
|
highlight.north == n && highlight.south == s &&
|
||||||
|
highlight.east == e && highlight.west == w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ハイライト再生成
|
||||||
|
clearHighlight(player)
|
||||||
|
|
||||||
|
val blocksToTill = calculateTillableBlocks(targetBlock, n, s, e, w)
|
||||||
|
if (blocksToTill.isEmpty()) return
|
||||||
|
|
||||||
|
val entities = createHighlightEntities(player, blocksToTill, tier >= 3)
|
||||||
|
activeHighlights[player.uniqueId] = HighlightSession(
|
||||||
|
targetBlock, entities, System.currentTimeMillis(), n, s, e, w
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopHolding(player: Player, item: ItemStack) {
|
||||||
|
clearHighlight(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* インタラクトイベント処理
|
||||||
|
*/
|
||||||
|
private fun handleInteract(event: PlayerInteractEvent, item: ItemStack) {
|
||||||
|
// 再帰防止チェック
|
||||||
|
if (processingPlayers.contains(event.player.uniqueId)) return
|
||||||
|
|
||||||
|
// 右クリックで耕地化
|
||||||
|
if (event.action != Action.RIGHT_CLICK_BLOCK) return
|
||||||
|
if (event.hand != EquipmentSlot.HAND) return
|
||||||
|
|
||||||
|
val block = event.clickedBlock ?: return
|
||||||
|
if (!tillableBlocks.contains(block.type)) return
|
||||||
|
|
||||||
|
// スニーク中は何もしない(ホイール調整のため)
|
||||||
|
if (event.player.isSneaking) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val player = event.player
|
||||||
|
val tier = getTier(item)
|
||||||
|
val session = editSessions[player.uniqueId] ?: EditSession()
|
||||||
|
val maxRange = getMaxRangeForTier(tier)
|
||||||
|
|
||||||
|
val n = session.north.coerceIn(0, maxRange)
|
||||||
|
val s = session.south.coerceIn(0, maxRange)
|
||||||
|
val e = session.east.coerceIn(0, maxRange)
|
||||||
|
val w = session.west.coerceIn(0, maxRange)
|
||||||
|
|
||||||
|
val blocksToTill = calculateTillableBlocks(block, n, s, e, w)
|
||||||
|
if (blocksToTill.isEmpty()) return
|
||||||
|
|
||||||
|
// 元のイベントをキャンセル(単一ブロックの耕地化を防ぐ)
|
||||||
|
event.isCancelled = true
|
||||||
|
|
||||||
|
// 広範囲耕地化実行
|
||||||
|
performAreaTill(player, blocksToTill, item, tier)
|
||||||
|
|
||||||
|
// ハイライトクリア
|
||||||
|
clearHighlight(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* マウスホイール(スロット変更)によるサイズ調整の処理
|
||||||
|
* プレイヤーが向いている方向の範囲を個別に調整する(Blenderの面移動に近い感覚)
|
||||||
|
*/
|
||||||
|
private fun handleItemHeld(event: PlayerItemHeldEvent, item: ItemStack) {
|
||||||
|
val player = event.player
|
||||||
|
|
||||||
|
// スニーク中のみ調整
|
||||||
|
if (!player.isSneaking) return
|
||||||
|
|
||||||
|
val session = editSessions.getOrPut(player.uniqueId) { EditSession() }
|
||||||
|
val tier = getTier(item)
|
||||||
|
val maxRange = getMaxRangeForTier(tier)
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - session.lastScrollTime < 50) return
|
||||||
|
|
||||||
|
session.lastScrollTime = now
|
||||||
|
|
||||||
|
// スクロール方向を判定
|
||||||
|
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)
|
||||||
|
|
||||||
|
// イベントをキャンセルしてスロット切り替えを防ぐ
|
||||||
|
event.isCancelled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* プレイヤーのYawから、N/S/E/Wのどの方角を向いているか取得
|
||||||
|
*/
|
||||||
|
private fun getDirectionFacing(yaw: Float): BlockFace {
|
||||||
|
val normalizedYaw = ((yaw % 360) + 360) % 360
|
||||||
|
return when (normalizedYaw) {
|
||||||
|
in 45f..135f -> BlockFace.WEST
|
||||||
|
in 135f..225f -> BlockFace.NORTH
|
||||||
|
in 225f..315f -> BlockFace.EAST
|
||||||
|
else -> BlockFace.SOUTH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 耕地化対象ブロックを計算 (4方向の範囲を使用)
|
||||||
|
*/
|
||||||
|
private fun calculateTillableBlocks(centerBlock: Block, north: Int, south: Int, east: Int, west: Int): Set<Block> {
|
||||||
|
val blocks = mutableSetOf<Block>()
|
||||||
|
|
||||||
|
// North は -Z, South は +Z, East は +X, West は -X
|
||||||
|
for (x in -west..east) {
|
||||||
|
for (z in -north..south) {
|
||||||
|
val block = centerBlock.getRelative(x, 0, z)
|
||||||
|
if (tillableBlocks.contains(block.type)) {
|
||||||
|
// 上にブロックがないことを確認
|
||||||
|
val above = block.getRelative(BlockFace.UP)
|
||||||
|
if (above.type.isAir || !above.type.isSolid) {
|
||||||
|
blocks.add(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 広範囲耕地化を実行
|
||||||
|
*/
|
||||||
|
private fun performAreaTill(player: Player, blocks: Set<Block>, item: ItemStack, tier: Int) {
|
||||||
|
// 再帰防止フラグをセット
|
||||||
|
processingPlayers.add(player.uniqueId)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val isEternallyMoist = tier >= 3
|
||||||
|
var tilledCount = 0
|
||||||
|
|
||||||
|
for (block in blocks) {
|
||||||
|
// PlayerInteractEventを発火して他のプラグインとの互換性を保つ
|
||||||
|
val fakeEvent = PlayerInteractEvent(
|
||||||
|
player,
|
||||||
|
Action.RIGHT_CLICK_BLOCK,
|
||||||
|
item,
|
||||||
|
block,
|
||||||
|
BlockFace.UP,
|
||||||
|
EquipmentSlot.HAND
|
||||||
|
)
|
||||||
|
plugin.server.pluginManager.callEvent(fakeEvent)
|
||||||
|
|
||||||
|
if (fakeEvent.isCancelled) continue
|
||||||
|
|
||||||
|
// 耕地化
|
||||||
|
block.type = Material.FARMLAND
|
||||||
|
|
||||||
|
// Tier3以上: 永続的な湿った状態
|
||||||
|
if (isEternallyMoist) {
|
||||||
|
val farmlandData = block.blockData as? Farmland
|
||||||
|
farmlandData?.let {
|
||||||
|
it.moisture = it.maximumMoisture
|
||||||
|
block.blockData = it
|
||||||
|
}
|
||||||
|
// 永続的な湿り状態をマーク(別途Tickで維持)
|
||||||
|
markEternallyMoist(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
tilledCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// ツールに耐久ダメージ
|
||||||
|
if (tilledCount > 0) {
|
||||||
|
if(player.gameMode != GameMode.CREATIVE && player.gameMode != GameMode.SPECTATOR)
|
||||||
|
player.damageItemStack(item, tilledCount)
|
||||||
|
// 効果音
|
||||||
|
player.playSound(
|
||||||
|
player.location,
|
||||||
|
Sound.ITEM_HOE_TILL,
|
||||||
|
1.0f,
|
||||||
|
0.8f + (tier * 0.1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tier3以上の特殊エフェクト
|
||||||
|
if (isEternallyMoist) {
|
||||||
|
blocks.forEach { block ->
|
||||||
|
player.spawnParticle(
|
||||||
|
Particle.DRIPPING_WATER,
|
||||||
|
block.location.add(0.5, 1.0, 0.5),
|
||||||
|
3,
|
||||||
|
0.3, 0.1, 0.3,
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 再帰防止フラグをクリア
|
||||||
|
processingPlayers.remove(player.uniqueId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 耕地の水分量変化(乾燥)を監視
|
||||||
|
*/
|
||||||
|
private fun handleMoistureChange(event: MoistureChangeEvent) {
|
||||||
|
val block = event.block
|
||||||
|
|
||||||
|
// 自己浄化: 耕地でなくなった場合、あるいは乾燥しようとしている場合
|
||||||
|
if (block.type != Material.FARMLAND) {
|
||||||
|
removeMoistMark(block)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// チャンクのPDCを確認して「永続的な湿り」マークがあるかチェック
|
||||||
|
if (isMarkedAsEternallyMoist(block)) {
|
||||||
|
// 水分が減るのを阻止(イベントをキャンセル)
|
||||||
|
event.isCancelled = true
|
||||||
|
|
||||||
|
// 念のためnewStateも最大値に固定
|
||||||
|
val data = event.newState
|
||||||
|
if (data is Farmland) {
|
||||||
|
data.moisture = data.maximumMoisture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汎用的なブロック消失クリーンアップ
|
||||||
|
*/
|
||||||
|
private fun handleGenericBlockCleanup(event: Event) {
|
||||||
|
val block = when (event) {
|
||||||
|
is BlockBreakEvent -> event.block
|
||||||
|
is BlockFadeEvent -> event.block
|
||||||
|
is BlockPlaceEvent -> event.block
|
||||||
|
is BlockBurnEvent -> event.block
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
removeMoistMark(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBlockExplodeCleanup(event: BlockExplodeEvent) {
|
||||||
|
event.blockList().forEach { removeMoistMark(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEntityExplodeCleanup(event: EntityExplodeEvent) {
|
||||||
|
event.blockList().forEach { removeMoistMark(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEntityInteractCleanup(event: EntityInteractEvent) {
|
||||||
|
// 耕地を踏みつけた時など、耕地でなくなる可能性が高いイベント
|
||||||
|
removeMoistMark(event.block)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBlockFromToCleanup(event: BlockFromToEvent) {
|
||||||
|
// 水や溶岩が流れ込んで耕地が消える場合
|
||||||
|
removeMoistMark(event.block)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePistonExtendCleanup(event: BlockPistonExtendEvent) {
|
||||||
|
event.blocks.forEach { removeMoistMark(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePistonRetractCleanup(event: BlockPistonRetractEvent) {
|
||||||
|
event.blocks.forEach { removeMoistMark(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBlockPhysicsCleanup(event: BlockPhysicsEvent) {
|
||||||
|
// 全ての物理更新時に、もし「以前耕地でマークされていたのに、今は耕地でない」なら削除
|
||||||
|
val block = event.block
|
||||||
|
if (block.type != Material.FARMLAND) {
|
||||||
|
removeMoistMark(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ブロックが永続的な湿り状態としてマークされているか確認
|
||||||
|
*/
|
||||||
|
private fun isMarkedAsEternallyMoist(block: Block): Boolean {
|
||||||
|
val chunk = block.chunk
|
||||||
|
val pdc = chunk.persistentDataContainer
|
||||||
|
val key = NamespacedKey(plugin, "moist_${block.x}_${block.y}_${block.z}")
|
||||||
|
return pdc.has(key, PersistentDataType.BYTE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ブロックを永続的な湿り状態としてマーク
|
||||||
|
*/
|
||||||
|
private fun markEternallyMoist(block: Block) {
|
||||||
|
val chunk = block.chunk
|
||||||
|
val pdc = chunk.persistentDataContainer
|
||||||
|
val key = NamespacedKey(plugin, "moist_${block.x}_${block.y}_${block.z}")
|
||||||
|
pdc.set(key, PersistentDataType.BYTE, 1.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeMoistMark(block: Block) {
|
||||||
|
val chunk = block.chunk
|
||||||
|
val pdc = chunk.persistentDataContainer
|
||||||
|
val key = NamespacedKey(plugin, "moist_${block.x}_${block.y}_${block.z}")
|
||||||
|
if (pdc.has(key, PersistentDataType.BYTE)) {
|
||||||
|
pdc.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ハイライトエンティティを作成
|
||||||
|
*/
|
||||||
|
private fun createHighlightEntities(
|
||||||
|
player: Player,
|
||||||
|
blocks: Set<Block>,
|
||||||
|
isEternallyMoist: Boolean
|
||||||
|
): List<BlockDisplay> {
|
||||||
|
if (!supportsBlockDisplay) return emptyList()
|
||||||
|
|
||||||
|
val entities = mutableListOf<BlockDisplay>()
|
||||||
|
val highlightColor = if (isEternallyMoist) Color.AQUA else Color.LIME
|
||||||
|
|
||||||
|
for (block in blocks) {
|
||||||
|
val loc = block.location.add(0.5, 0.5, 0.5)
|
||||||
|
try {
|
||||||
|
val display = block.world.spawn(loc, BlockDisplay::class.java) { e ->
|
||||||
|
e.block = Material.FARMLAND.createBlockData()
|
||||||
|
e.transformation = Transformation(
|
||||||
|
Vector3f(-0.5005f, -0.5005f, -0.5005f),
|
||||||
|
AxisAngle4f(0f, 0f, 0f, 1f),
|
||||||
|
Vector3f(1.001f, 1.001f, 1.001f),
|
||||||
|
AxisAngle4f(0f, 0f, 0f, 1f)
|
||||||
|
)
|
||||||
|
e.isGlowing = true
|
||||||
|
e.glowColorOverride = highlightColor
|
||||||
|
e.brightness = Display.Brightness(15, 15)
|
||||||
|
e.isVisibleByDefault = false
|
||||||
|
}
|
||||||
|
player.showEntity(plugin, display)
|
||||||
|
entities.add(display)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// BlockDisplay非対応バージョン
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ハイライトをクリア
|
||||||
|
*/
|
||||||
|
private fun clearHighlight(player: Player) {
|
||||||
|
val session = activeHighlights.remove(player.uniqueId) ?: return
|
||||||
|
for (e in session.entities) {
|
||||||
|
e.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,9 @@ import org.bukkit.event.Event
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.Listener
|
import org.bukkit.event.Listener
|
||||||
import org.bukkit.event.entity.ProjectileHitEvent
|
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.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import org.bukkit.plugin.EventExecutor
|
import org.bukkit.plugin.EventExecutor
|
||||||
import org.bukkit.plugin.Plugin
|
import org.bukkit.plugin.Plugin
|
||||||
|
|
@ -52,6 +55,81 @@ class EventListener(private val plugin: Plugin) : Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MoistureChangeEvent - 耕地の水分変化(乾燥)を監視
|
||||||
|
* これもアイテムに紐付かないグローバルなイベントとして処理する。
|
||||||
|
*/
|
||||||
|
@EventHandler
|
||||||
|
fun onMoistureChange(event: MoistureChangeEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockBreakGlobal(event: BlockBreakEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockExplodeGlobal(event: BlockExplodeEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onEntityExplodeGlobal(event: EntityExplodeEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onEntityInteractGlobal(event: EntityInteractEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockFadeGlobal(event: BlockFadeEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockFromToGlobal(event: BlockFromToEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockPistonExtendGlobal(event: BlockPistonExtendEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockPistonRetractGlobal(event: BlockPistonRetractEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockPlaceGlobal(event: BlockPlaceEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockBurnGlobal(event: BlockBurnEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
fun onBlockPhysicsGlobal(event: BlockPhysicsEvent) {
|
||||||
|
dispatchGlobalToComponents(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Event> dispatchGlobalToComponents(event: T) {
|
||||||
|
val eventClass = event.javaClass.kotlin
|
||||||
|
ComponentRegistry.getAll().forEach { component ->
|
||||||
|
val handler = component.getEventHandlers()[eventClass]
|
||||||
|
if (handler != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(handler as (T, ItemStack) -> Unit).invoke(event, ItemStack(org.bukkit.Material.AIR))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun registerDynamicListeners() {
|
private fun registerDynamicListeners() {
|
||||||
val registeredEvents = mutableSetOf<KClass<out Event>>()
|
val registeredEvents = mutableSetOf<KClass<out Event>>()
|
||||||
|
|
@ -66,9 +144,20 @@ class EventListener(private val plugin: Plugin) : Listener {
|
||||||
|
|
||||||
// Register a listener for each event type
|
// Register a listener for each event type
|
||||||
for (eventClass in registeredEvents) {
|
for (eventClass in registeredEvents) {
|
||||||
// Skip ProjectileHitEvent as it is handled manually
|
// Skip events that are handled manually or globally
|
||||||
if (eventClass == ProjectileHitEvent::class) continue
|
if (eventClass == ProjectileHitEvent::class ||
|
||||||
|
eventClass == MoistureChangeEvent::class ||
|
||||||
|
eventClass == EntityExplodeEvent::class ||
|
||||||
|
eventClass == BlockExplodeEvent::class ||
|
||||||
|
eventClass == EntityInteractEvent::class ||
|
||||||
|
eventClass == BlockFadeEvent::class ||
|
||||||
|
eventClass == BlockFromToEvent::class ||
|
||||||
|
eventClass == BlockPistonExtendEvent::class ||
|
||||||
|
eventClass == BlockPistonRetractEvent::class ||
|
||||||
|
eventClass == BlockPlaceEvent::class ||
|
||||||
|
eventClass == BlockBurnEvent::class ||
|
||||||
|
eventClass == BlockPhysicsEvent::class) continue
|
||||||
|
|
||||||
val strategy = EventStrategyRegistry.get(eventClass)
|
val strategy = EventStrategyRegistry.get(eventClass)
|
||||||
if (strategy == null) {
|
if (strategy == null) {
|
||||||
plugin.logger.warning("No strategy found for event ${eventClass.simpleName} but it is used by an item/component.")
|
plugin.logger.warning("No strategy found for event ${eventClass.simpleName} but it is used by an item/component.")
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,13 @@ components:
|
||||||
- "minecraft:shroomlight"
|
- "minecraft:shroomlight"
|
||||||
- "minecraft:hay_block"
|
- "minecraft:hay_block"
|
||||||
- "#minecraft:wart_blocks"
|
- "#minecraft:wart_blocks"
|
||||||
|
area_tiller:
|
||||||
|
default_range: 3 # デフォルトの耕地化範囲
|
||||||
|
max_range_per_tier:
|
||||||
|
1: 4 # Tier1: 4x4
|
||||||
|
2: 5 # Tier2: 5x5
|
||||||
|
3: 4 # Tier3: 4x4 (永続湿り耕地)
|
||||||
|
4: 5 # Tier4: 5x5 (永続湿り耕地)
|
||||||
|
|
||||||
items:
|
items:
|
||||||
auto_feeder:
|
auto_feeder:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user