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.BlinkComponent
|
||||
import net.hareworks.hcu.items.content.components.VeinMinerComponent
|
||||
import net.hareworks.hcu.items.content.components.AreaTillerComponent
|
||||
|
||||
import org.bukkit.permissions.PermissionDefault
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
|
@ -48,6 +49,7 @@ public class App : JavaPlugin() {
|
|||
ComponentRegistry.register(DoubleJumpComponent(this))
|
||||
ComponentRegistry.register(BlinkComponent(this))
|
||||
ComponentRegistry.register(VeinMinerComponent(this))
|
||||
ComponentRegistry.register(AreaTillerComponent(this))
|
||||
|
||||
server.pluginManager.registerEvents(net.hareworks.hcu.items.listeners.EventListener(this), this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ object Config {
|
|||
var blink = BlinkSettings()
|
||||
var doubleJump = DoubleJumpSettings()
|
||||
var veinMiner = VeinMinerSettings()
|
||||
var areaTiller = AreaTillerSettings()
|
||||
var magnet = MagnetSettings()
|
||||
var autoFeeder = AutoFeederSettings()
|
||||
|
||||
|
|
@ -62,6 +63,14 @@ object Config {
|
|||
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コンポーネント設定
|
||||
*/
|
||||
|
|
@ -108,6 +117,7 @@ object Config {
|
|||
loadDoubleJumpSettings(config)
|
||||
loadVeinMinerSettings(plugin, config)
|
||||
loadMagnetSettings(config)
|
||||
loadAreaTillerSettings(config)
|
||||
loadAutoFeederSettings(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_per_tier", 5.0)
|
||||
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.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.Listener
|
||||
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.plugin.EventExecutor
|
||||
import org.bukkit.plugin.Plugin
|
||||
|
|
@ -53,6 +56,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() {
|
||||
val registeredEvents = mutableSetOf<KClass<out Event>>()
|
||||
|
||||
|
|
@ -66,8 +144,19 @@ class EventListener(private val plugin: Plugin) : Listener {
|
|||
|
||||
// Register a listener for each event type
|
||||
for (eventClass in registeredEvents) {
|
||||
// Skip ProjectileHitEvent as it is handled manually
|
||||
if (eventClass == ProjectileHitEvent::class) continue
|
||||
// Skip events that are handled manually or globally
|
||||
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)
|
||||
if (strategy == null) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ components:
|
|||
- "minecraft:shroomlight"
|
||||
- "minecraft:hay_block"
|
||||
- "#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:
|
||||
auto_feeder:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user